diff options
author | Rob Mensching <rob@firegiant.com> | 2022-07-14 15:19:53 -0700 |
---|---|---|
committer | Rob Mensching <rob@firegiant.com> | 2022-07-14 16:02:24 -0700 |
commit | 229242cf7c328b89b5aa65ed7a04e33c8b93b393 (patch) | |
tree | de0a9547e73e46490b0946d6850228d5b30258b8 /src/tools/Dtf/Inventory | |
parent | f46ca6a9dce91607ffc9855270dd6998216e1a8b (diff) | |
download | wix-229242cf7c328b89b5aa65ed7a04e33c8b93b393.tar.gz wix-229242cf7c328b89b5aa65ed7a04e33c8b93b393.tar.bz2 wix-229242cf7c328b89b5aa65ed7a04e33c8b93b393.zip |
Rename "samples" segment to "tools"
This segment is a bit of a "miscellaneous section" in the WiX repo.
As such it has been difficult to name. I originally eschewed the
name "tools" because what is in the "wix" segment was once called
"tools". However, now that wix.exe is firmly established as the
entry point for WiX operations, I've become comfortable with its
segment being named "wix". That meant "tools" was again available
and "tools" better describes the content of this section.
Diffstat (limited to 'src/tools/Dtf/Inventory')
-rw-r--r-- | src/tools/Dtf/Inventory/Columns.resx | 252 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/Features.cs | 107 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/IInventoryDataProvider.cs | 67 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/Inventory.cs | 1231 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/Inventory.csproj | 42 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/Inventory.ico | bin | 0 -> 4710 bytes | |||
-rw-r--r-- | src/tools/Dtf/Inventory/Inventory.resx | 265 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/components.cs | 626 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/msiutils.cs | 46 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/patches.cs | 227 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/products.cs | 145 | ||||
-rw-r--r-- | src/tools/Dtf/Inventory/xp.manifest | 15 |
12 files changed, 3023 insertions, 0 deletions
diff --git a/src/tools/Dtf/Inventory/Columns.resx b/src/tools/Dtf/Inventory/Columns.resx new file mode 100644 index 00000000..cfeb11e3 --- /dev/null +++ b/src/tools/Dtf/Inventory/Columns.resx | |||
@@ -0,0 +1,252 @@ | |||
1 | <?xml version="1.0" encoding="utf-8"?> | ||
2 | <root> | ||
3 | <!-- | ||
4 | Microsoft ResX Schema | ||
5 | |||
6 | Version 2.0 | ||
7 | |||
8 | The primary goals of this format is to allow a simple XML format | ||
9 | that is mostly human readable. The generation and parsing of the | ||
10 | various data types are done through the TypeConverter classes | ||
11 | associated with the data types. | ||
12 | |||
13 | Example: | ||
14 | |||
15 | ... ado.net/XML headers & schema ... | ||
16 | <resheader name="resmimetype">text/microsoft-resx</resheader> | ||
17 | <resheader name="version">2.0</resheader> | ||
18 | <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||
19 | <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||
20 | <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> | ||
21 | <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||
22 | <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||
23 | <value>[base64 mime encoded serialized .NET Framework object]</value> | ||
24 | </data> | ||
25 | <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||
26 | <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> | ||
27 | <comment>This is a comment</comment> | ||
28 | </data> | ||
29 | |||
30 | There are any number of "resheader" rows that contain simple | ||
31 | name/value pairs. | ||
32 | |||
33 | Each data row contains a name, and value. The row also contains a | ||
34 | type or mimetype. Type corresponds to a .NET class that support | ||
35 | text/value conversion through the TypeConverter architecture. | ||
36 | Classes that don't support this are serialized and stored with the | ||
37 | mimetype set. | ||
38 | |||
39 | The mimetype is used for serialized objects, and tells the | ||
40 | ResXResourceReader how to depersist the object. This is currently not | ||
41 | extensible. For a given mimetype the value must be set accordingly: | ||
42 | |||
43 | Note - application/x-microsoft.net.object.binary.base64 is the format | ||
44 | that the ResXResourceWriter will generate, however the reader can | ||
45 | read any of the formats listed below. | ||
46 | |||
47 | mimetype: application/x-microsoft.net.object.binary.base64 | ||
48 | value : The object must be serialized with | ||
49 | : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter | ||
50 | : and then encoded with base64 encoding. | ||
51 | |||
52 | mimetype: application/x-microsoft.net.object.soap.base64 | ||
53 | value : The object must be serialized with | ||
54 | : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||
55 | : and then encoded with base64 encoding. | ||
56 | |||
57 | mimetype: application/x-microsoft.net.object.bytearray.base64 | ||
58 | value : The object must be serialized into a byte array | ||
59 | : using a System.ComponentModel.TypeConverter | ||
60 | : and then encoded with base64 encoding. | ||
61 | --> | ||
62 | <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||
63 | <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> | ||
64 | <xsd:element name="root" msdata:IsDataSet="true"> | ||
65 | <xsd:complexType> | ||
66 | <xsd:choice maxOccurs="unbounded"> | ||
67 | <xsd:element name="metadata"> | ||
68 | <xsd:complexType> | ||
69 | <xsd:sequence> | ||
70 | <xsd:element name="value" type="xsd:string" minOccurs="0" /> | ||
71 | </xsd:sequence> | ||
72 | <xsd:attribute name="name" use="required" type="xsd:string" /> | ||
73 | <xsd:attribute name="type" type="xsd:string" /> | ||
74 | <xsd:attribute name="mimetype" type="xsd:string" /> | ||
75 | <xsd:attribute ref="xml:space" /> | ||
76 | </xsd:complexType> | ||
77 | </xsd:element> | ||
78 | <xsd:element name="assembly"> | ||
79 | <xsd:complexType> | ||
80 | <xsd:attribute name="alias" type="xsd:string" /> | ||
81 | <xsd:attribute name="name" type="xsd:string" /> | ||
82 | </xsd:complexType> | ||
83 | </xsd:element> | ||
84 | <xsd:element name="data"> | ||
85 | <xsd:complexType> | ||
86 | <xsd:sequence> | ||
87 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||
88 | <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||
89 | </xsd:sequence> | ||
90 | <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> | ||
91 | <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||
92 | <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||
93 | <xsd:attribute ref="xml:space" /> | ||
94 | </xsd:complexType> | ||
95 | </xsd:element> | ||
96 | <xsd:element name="resheader"> | ||
97 | <xsd:complexType> | ||
98 | <xsd:sequence> | ||
99 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||
100 | </xsd:sequence> | ||
101 | <xsd:attribute name="name" type="xsd:string" use="required" /> | ||
102 | </xsd:complexType> | ||
103 | </xsd:element> | ||
104 | </xsd:choice> | ||
105 | </xsd:complexType> | ||
106 | </xsd:element> | ||
107 | </xsd:schema> | ||
108 | <resheader name="resmimetype"> | ||
109 | <value>text/microsoft-resx</value> | ||
110 | </resheader> | ||
111 | <resheader name="version"> | ||
112 | <value>2.0</value> | ||
113 | </resheader> | ||
114 | <resheader name="reader"> | ||
115 | <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||
116 | </resheader> | ||
117 | <resheader name="writer"> | ||
118 | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||
119 | </resheader> | ||
120 | <data name="ProductsProductName" xml:space="preserve"> | ||
121 | <value>Product Name,250</value> | ||
122 | </data> | ||
123 | <data name="ProductsProductCode" xml:space="preserve"> | ||
124 | <value>Product Code,250</value> | ||
125 | </data> | ||
126 | <data name="ProductPropertiesProperty" xml:space="preserve"> | ||
127 | <value>Property,100</value> | ||
128 | </data> | ||
129 | <data name="ProductPropertiesValue" xml:space="preserve"> | ||
130 | <value>Value,300</value> | ||
131 | </data> | ||
132 | <data name="ProductFeaturesFeatureTitle" xml:space="preserve"> | ||
133 | <value>Feature Title,230</value> | ||
134 | </data> | ||
135 | <data name="ProductFeaturesFeatureName" xml:space="preserve"> | ||
136 | <value>Feature,200</value> | ||
137 | </data> | ||
138 | <data name="ProductFeaturesInstallState" xml:space="preserve"> | ||
139 | <value>Install State,70</value> | ||
140 | </data> | ||
141 | <data name="ProductFeatureComponentsComponentName" xml:space="preserve"> | ||
142 | <value>Component,250</value> | ||
143 | </data> | ||
144 | <data name="ProductFeatureComponentsComponentID" xml:space="preserve"> | ||
145 | <value>Component ID,250</value> | ||
146 | </data> | ||
147 | <data name="ProductComponentsComponentName" xml:space="preserve"> | ||
148 | <value>Component,180</value> | ||
149 | </data> | ||
150 | <data name="ProductComponentsComponentID" xml:space="preserve"> | ||
151 | <value>Component ID,250</value> | ||
152 | </data> | ||
153 | <data name="ProductComponentsInstallState" xml:space="preserve"> | ||
154 | <value>Install State,70</value> | ||
155 | </data> | ||
156 | <data name="ComponentProductsProductName" xml:space="preserve"> | ||
157 | <value>Product Name,250</value> | ||
158 | </data> | ||
159 | <data name="ComponentProductsProductCode" xml:space="preserve"> | ||
160 | <value>Product Code,250</value> | ||
161 | </data> | ||
162 | <data name="ComponentProductsComponentPath" xml:space="preserve"> | ||
163 | <value>Component Path,300</value> | ||
164 | </data> | ||
165 | <data name="ProductComponentItemsIsKey" xml:space="preserve"> | ||
166 | <value>Key,35</value> | ||
167 | </data> | ||
168 | <data name="ProductComponentItemsKey" xml:space="preserve"> | ||
169 | <value>Name,250</value> | ||
170 | </data> | ||
171 | <data name="ProductComponentItemsPath" xml:space="preserve"> | ||
172 | <value>Install Path,350</value> | ||
173 | </data> | ||
174 | <data name="ProductComponentItemsExists" xml:space="preserve"> | ||
175 | <value>Exists,40</value> | ||
176 | </data> | ||
177 | <data name="ProductComponentItemsDbVersion" xml:space="preserve"> | ||
178 | <value>Version in Database,100</value> | ||
179 | </data> | ||
180 | <data name="ProductComponentItemsInstalledVersion" xml:space="preserve"> | ||
181 | <value>Version Installed,100</value> | ||
182 | </data> | ||
183 | <data name="ProductComponentItemsInstalledMatch" xml:space="preserve"> | ||
184 | <value>Match,40</value> | ||
185 | </data> | ||
186 | <data name="ProductFilesIsKey" xml:space="preserve"> | ||
187 | <value>Key,35</value> | ||
188 | </data> | ||
189 | <data name="ProductFilesKey" xml:space="preserve"> | ||
190 | <value>Name,250</value> | ||
191 | </data> | ||
192 | <data name="ProductFilesPath" xml:space="preserve"> | ||
193 | <value>Install Path,350</value> | ||
194 | </data> | ||
195 | <data name="ProductFilesExists" xml:space="preserve"> | ||
196 | <value>Exists,40</value> | ||
197 | </data> | ||
198 | <data name="ProductFilesDbVersion" xml:space="preserve"> | ||
199 | <value>Version in Database,120</value> | ||
200 | </data> | ||
201 | <data name="ProductFilesInstalledVersion" xml:space="preserve"> | ||
202 | <value>Version Installed,120</value> | ||
203 | </data> | ||
204 | <data name="ProductFilesInstalledMatch" xml:space="preserve"> | ||
205 | <value>Match,40</value> | ||
206 | </data> | ||
207 | <data name="ProductFilesComponentID" xml:space="preserve"> | ||
208 | <value>Component ID,250</value> | ||
209 | </data> | ||
210 | <data name="ProductRegistryIsKey" xml:space="preserve"> | ||
211 | <value>Key,35</value> | ||
212 | </data> | ||
213 | <data name="ProductRegistryKey" xml:space="preserve"> | ||
214 | <value>Name,250</value> | ||
215 | </data> | ||
216 | <data name="ProductRegistryPath" xml:space="preserve"> | ||
217 | <value>Install Path,350</value> | ||
218 | </data> | ||
219 | <data name="ProductRegistryExists" xml:space="preserve"> | ||
220 | <value>Exists,40</value> | ||
221 | </data> | ||
222 | <data name="ProductRegistryDbVersion" xml:space="preserve"> | ||
223 | <value>Value in Database,120</value> | ||
224 | </data> | ||
225 | <data name="ProductRegistryInstalledVersion" xml:space="preserve"> | ||
226 | <value>Value Installed,120</value> | ||
227 | </data> | ||
228 | <data name="ProductRegistryInstalledMatch" xml:space="preserve"> | ||
229 | <value>Match,40</value> | ||
230 | </data> | ||
231 | <data name="ProductRegistryComponentID" xml:space="preserve"> | ||
232 | <value>Component ID,250</value> | ||
233 | </data> | ||
234 | <data name="PatchesPatchCode" xml:space="preserve"> | ||
235 | <value>Patch Code,250</value> | ||
236 | </data> | ||
237 | <data name="ProductPatchesPatchCode" xml:space="preserve"> | ||
238 | <value>Patch Code,250</value> | ||
239 | </data> | ||
240 | <data name="PatchPropertiesProperty" xml:space="preserve"> | ||
241 | <value>Property,130</value> | ||
242 | </data> | ||
243 | <data name="PatchPropertiesValue" xml:space="preserve"> | ||
244 | <value>Value,360</value> | ||
245 | </data> | ||
246 | <data name="PatchTargetsProductName" xml:space="preserve"> | ||
247 | <value>Product Name,360</value> | ||
248 | </data> | ||
249 | <data name="PatchTargetsProductCode" xml:space="preserve"> | ||
250 | <value>Product Code,360</value> | ||
251 | </data> | ||
252 | </root> \ No newline at end of file | ||
diff --git a/src/tools/Dtf/Inventory/Features.cs b/src/tools/Dtf/Inventory/Features.cs new file mode 100644 index 00000000..9d2747ba --- /dev/null +++ b/src/tools/Dtf/Inventory/Features.cs | |||
@@ -0,0 +1,107 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.IO; | ||
5 | using System.Data; | ||
6 | using System.Collections; | ||
7 | using System.Collections.Generic; | ||
8 | using System.Globalization; | ||
9 | using System.Windows.Forms; | ||
10 | using WixToolset.Dtf.WindowsInstaller; | ||
11 | |||
12 | namespace WixToolset.Dtf.Tools.Inventory | ||
13 | { | ||
14 | /// <summary> | ||
15 | /// Provides inventory data about features of products installed on the system. | ||
16 | /// </summary> | ||
17 | public class FeaturesInventory : IInventoryDataProvider | ||
18 | { | ||
19 | private static object syncRoot = new object(); | ||
20 | |||
21 | public FeaturesInventory() | ||
22 | { | ||
23 | } | ||
24 | |||
25 | public string Description | ||
26 | { | ||
27 | get { return "Features of installed products"; } | ||
28 | } | ||
29 | |||
30 | public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) | ||
31 | { | ||
32 | statusCallback(0, @"Products\...\Features"); | ||
33 | ArrayList nodes = new ArrayList(); | ||
34 | foreach (ProductInstallation product in ProductInstallation.AllProducts) | ||
35 | { | ||
36 | nodes.Add(String.Format(@"Products\{0}\Features", MsiUtils.GetProductName(product.ProductCode))); | ||
37 | } | ||
38 | statusCallback(nodes.Count, String.Empty); | ||
39 | return (string[]) nodes.ToArray(typeof(string)); | ||
40 | } | ||
41 | |||
42 | public bool IsNodeSearchable(string searchRoot, string searchNode) | ||
43 | { | ||
44 | return true; | ||
45 | } | ||
46 | |||
47 | public DataView GetData(string nodePath) | ||
48 | { | ||
49 | string[] path = nodePath.Split('\\'); | ||
50 | |||
51 | if(path.Length == 3 && path[0] == "Products" && path[2] == "Features") | ||
52 | { | ||
53 | return GetProductFeaturesData(MsiUtils.GetProductCode(path[1])); | ||
54 | } | ||
55 | return null; | ||
56 | } | ||
57 | |||
58 | public DataView GetProductFeaturesData(string productCode) | ||
59 | { | ||
60 | DataTable table = new DataTable("ProductFeatures"); | ||
61 | table.Locale = CultureInfo.InvariantCulture; | ||
62 | table.Columns.Add("ProductFeaturesFeatureTitle", typeof(string)); | ||
63 | table.Columns.Add("ProductFeaturesFeatureName", typeof(string)); | ||
64 | table.Columns.Add("ProductFeaturesInstallState", typeof(string)); | ||
65 | |||
66 | try | ||
67 | { | ||
68 | IntPtr hWnd = IntPtr.Zero; | ||
69 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
70 | lock(syncRoot) // Only one Installer session can be active at a time | ||
71 | { | ||
72 | using(Session session = Installer.OpenProduct(productCode)) | ||
73 | { | ||
74 | session.DoAction("CostInitialize"); | ||
75 | session.DoAction("FileCost"); | ||
76 | session.DoAction("CostFinalize"); | ||
77 | |||
78 | IList<string> featuresAndTitles = session.Database.ExecuteStringQuery( | ||
79 | "SELECT `Title`, `Feature` FROM `Feature`"); | ||
80 | |||
81 | for(int i = 0; i < featuresAndTitles.Count; i += 2) | ||
82 | { | ||
83 | InstallState featureState = session.Features[featuresAndTitles[i + 1]].CurrentState; | ||
84 | table.Rows.Add(new object[] { featuresAndTitles[i], featuresAndTitles[i+1], | ||
85 | (featureState == InstallState.Advertised ? "Advertised" : featureState.ToString()) }); | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | return new DataView(table, "", "ProductFeaturesFeatureTitle ASC", DataViewRowState.CurrentRows); | ||
90 | } | ||
91 | catch(InstallerException) { } | ||
92 | catch(IOException) { } | ||
93 | return null; | ||
94 | } | ||
95 | |||
96 | public string GetLink(string nodePath, DataRow row) | ||
97 | { | ||
98 | string[] path = nodePath.Split('\\'); | ||
99 | |||
100 | if(path.Length == 3 && path[0] == "Products" && path[2] == "Features") | ||
101 | { | ||
102 | return String.Format(@"Products\{0}\Features\{1}", path[1], row["ProductFeaturesFeatureName"]); | ||
103 | } | ||
104 | return null; | ||
105 | } | ||
106 | } | ||
107 | } | ||
diff --git a/src/tools/Dtf/Inventory/IInventoryDataProvider.cs b/src/tools/Dtf/Inventory/IInventoryDataProvider.cs new file mode 100644 index 00000000..affcb358 --- /dev/null +++ b/src/tools/Dtf/Inventory/IInventoryDataProvider.cs | |||
@@ -0,0 +1,67 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.Data; | ||
5 | |||
6 | namespace WixToolset.Dtf.Tools.Inventory | ||
7 | { | ||
8 | /// <summary> | ||
9 | /// Reports the total number of items loaded so far by <see cref="IInventoryDataProvider.GetNodes"/>. | ||
10 | /// </summary> | ||
11 | public delegate void InventoryDataLoadStatusCallback(int itemsLoaded, string currentNode); | ||
12 | |||
13 | /// <summary> | ||
14 | /// Inventory data providers implement this interface to provide a particular type of data. | ||
15 | /// Implementors must provide a parameterless constructor. | ||
16 | /// </summary> | ||
17 | public interface IInventoryDataProvider | ||
18 | { | ||
19 | /// <summary> | ||
20 | /// Gets a description of the data provided. This description allows | ||
21 | /// the user to choose what type of data to gather. | ||
22 | /// </summary> | ||
23 | string Description { get; } | ||
24 | |||
25 | /// <summary> | ||
26 | /// Gets the paths of all nodes for which this object provides data. | ||
27 | /// </summary> | ||
28 | /// <param name="statusCallback">Callback for reporting status. | ||
29 | /// The callback should not necessarily be invoked for every individual | ||
30 | /// node loaded, rather only every significant chunk.</param> | ||
31 | /// <returns>An array of node paths. The parts of the node paths | ||
32 | /// are delimited by backslashes (\).</returns> | ||
33 | string[] GetNodes(InventoryDataLoadStatusCallback statusCallback); | ||
34 | |||
35 | /// <summary> | ||
36 | /// When related nodes of a tree consist of duplicate data, it's | ||
37 | /// inefficient to search them all. This method indicates which | ||
38 | /// nodes should be search and which should be ignored. | ||
39 | /// </summary> | ||
40 | /// <param name="searchRoot">Root node of the subtree-search.</param> | ||
41 | /// <param name="searchNode">Node which may or may not be searched.</param> | ||
42 | /// <returns>True if the node should be searched, false otherwise.</returns> | ||
43 | bool IsNodeSearchable(string searchRoot, string searchNode); | ||
44 | |||
45 | /// <summary> | ||
46 | /// Gets the data for a particular node. | ||
47 | /// </summary> | ||
48 | /// <param name="nodePath">Path of the node for which data is requested. | ||
49 | /// This is one of the paths returned by <see cref="GetNodes"/>.</param> | ||
50 | /// <returns>DataView of a table filled with data, or null if data is | ||
51 | /// not available.</returns> | ||
52 | DataView GetData(string nodePath); | ||
53 | |||
54 | /// <summary> | ||
55 | /// Gets the path of another node which provides more details about | ||
56 | /// a particular data row. | ||
57 | /// </summary> | ||
58 | /// <param name="nodePath">Path of the node containing the data | ||
59 | /// row being queried.</param> | ||
60 | /// <param name="row">Data row being queried.</param> | ||
61 | /// <returns>Path to another node. This is not necessarily | ||
62 | /// one of the nodes returned by <see cref="GetNodes"/>. If the | ||
63 | /// node path is unknown, it will be ignored. This method may | ||
64 | /// return null if there is no detail node for the row.</returns> | ||
65 | string GetLink(string nodePath, DataRow row); | ||
66 | } | ||
67 | } | ||
diff --git a/src/tools/Dtf/Inventory/Inventory.cs b/src/tools/Dtf/Inventory/Inventory.cs new file mode 100644 index 00000000..07735086 --- /dev/null +++ b/src/tools/Dtf/Inventory/Inventory.cs | |||
@@ -0,0 +1,1231 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.IO; | ||
5 | using System.Drawing; | ||
6 | using System.Collections; | ||
7 | using System.ComponentModel; | ||
8 | using System.Diagnostics.CodeAnalysis; | ||
9 | using System.Windows.Forms; | ||
10 | using System.Globalization; | ||
11 | using System.Reflection; | ||
12 | using System.Resources; | ||
13 | using System.Threading; | ||
14 | using System.Security.Permissions; | ||
15 | using System.Data; | ||
16 | |||
17 | |||
18 | [assembly: AssemblyDescription("Shows a hierarchical, relational, searchable " + | ||
19 | " view of all of the product, feature, component, file, and patch data managed " + | ||
20 | "by MSI, for all products installed on the system.")] | ||
21 | |||
22 | [assembly: SecurityPermission(SecurityAction.RequestMinimum, UnmanagedCode=true)] | ||
23 | |||
24 | |||
25 | namespace WixToolset.Dtf.Tools.Inventory | ||
26 | { | ||
27 | public class Inventory : System.Windows.Forms.Form | ||
28 | { | ||
29 | [STAThread] | ||
30 | public static void Main() | ||
31 | { | ||
32 | if (WixToolset.Dtf.WindowsInstaller.Installer.Version < new Version(3, 0)) | ||
33 | { | ||
34 | MessageBox.Show("This application requires Windows Installer version 3.0 or later.", | ||
35 | "Inventory", MessageBoxButtons.OK, MessageBoxIcon.Error); | ||
36 | return; | ||
37 | } | ||
38 | |||
39 | Application.Run(new Inventory()); | ||
40 | } | ||
41 | |||
42 | private IInventoryDataProvider[] dataProviders; | ||
43 | private Hashtable dataProviderMap; | ||
44 | private Hashtable data; | ||
45 | private ArrayList tablesLoading; | ||
46 | private bool searching; | ||
47 | private bool stopSearch; | ||
48 | private bool navigating; | ||
49 | private string continueSearchRoot; | ||
50 | private string continueSearchPath; | ||
51 | private DataGridCell continueSearchCell; | ||
52 | private DataGridCell continueSearchEndCell; | ||
53 | private bool mouseOverGridLink = false; | ||
54 | private Stack historyBack; | ||
55 | private Stack historyForward; | ||
56 | private Stack cellHistoryBack; | ||
57 | private Stack cellHistoryForward; | ||
58 | private static readonly DataGridCell anyCell = new DataGridCell(-1,-1); | ||
59 | private static readonly DataGridCell zeroCell = new DataGridCell(0,0); | ||
60 | private static object syncRoot = new object(); | ||
61 | |||
62 | private System.Windows.Forms.DataGrid dataGrid; | ||
63 | private System.Windows.Forms.TreeView treeView; | ||
64 | private System.Windows.Forms.Panel toolPanel; | ||
65 | private System.Windows.Forms.Splitter splitter; | ||
66 | private System.Windows.Forms.Panel dataPanel; | ||
67 | private System.Windows.Forms.Button backButton; | ||
68 | private System.Windows.Forms.Button forwardButton; | ||
69 | private System.Windows.Forms.Button findButton; | ||
70 | private System.Windows.Forms.TextBox findTextBox; | ||
71 | private System.Windows.Forms.Button refreshButton; | ||
72 | private System.Windows.Forms.Button findStopButton; | ||
73 | private System.Windows.Forms.CheckBox searchTreeCheckBox; | ||
74 | private System.Windows.Forms.ToolTip gridLinkTip; | ||
75 | private System.ComponentModel.IContainer components; | ||
76 | |||
77 | public Inventory() | ||
78 | { | ||
79 | InitializeComponent(); | ||
80 | |||
81 | this.gridLinkTip.InitialDelay = 0; | ||
82 | this.gridLinkTip.ReshowDelay = 0; | ||
83 | |||
84 | this.dataProviderMap = new Hashtable(); | ||
85 | this.data = new Hashtable(); | ||
86 | this.tablesLoading = new ArrayList(); | ||
87 | this.historyBack = new Stack(); | ||
88 | this.historyForward = new Stack(); | ||
89 | this.cellHistoryBack = new Stack(); | ||
90 | this.cellHistoryForward = new Stack(); | ||
91 | } | ||
92 | |||
93 | protected override void Dispose(bool disposing) | ||
94 | { | ||
95 | if(disposing) | ||
96 | { | ||
97 | if(components != null) | ||
98 | { | ||
99 | components.Dispose(); | ||
100 | } | ||
101 | } | ||
102 | base.Dispose(disposing); | ||
103 | } | ||
104 | |||
105 | #region Windows Form Designer generated code | ||
106 | /// <summary> | ||
107 | /// Required method for Designer support - do not modify | ||
108 | /// the contents of this method with the code editor. | ||
109 | /// </summary> | ||
110 | private void InitializeComponent() | ||
111 | { | ||
112 | this.components = new System.ComponentModel.Container(); | ||
113 | this.dataGrid = new System.Windows.Forms.DataGrid(); | ||
114 | this.treeView = new System.Windows.Forms.TreeView(); | ||
115 | this.toolPanel = new System.Windows.Forms.Panel(); | ||
116 | this.findStopButton = new System.Windows.Forms.Button(); | ||
117 | this.findButton = new System.Windows.Forms.Button(); | ||
118 | this.searchTreeCheckBox = new System.Windows.Forms.CheckBox(); | ||
119 | this.findTextBox = new System.Windows.Forms.TextBox(); | ||
120 | this.refreshButton = new System.Windows.Forms.Button(); | ||
121 | this.forwardButton = new System.Windows.Forms.Button(); | ||
122 | this.backButton = new System.Windows.Forms.Button(); | ||
123 | this.dataPanel = new System.Windows.Forms.Panel(); | ||
124 | this.splitter = new System.Windows.Forms.Splitter(); | ||
125 | this.gridLinkTip = new System.Windows.Forms.ToolTip(this.components); | ||
126 | ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).BeginInit(); | ||
127 | this.toolPanel.SuspendLayout(); | ||
128 | this.dataPanel.SuspendLayout(); | ||
129 | this.SuspendLayout(); | ||
130 | // | ||
131 | // dataGrid | ||
132 | // | ||
133 | this.dataGrid.DataMember = ""; | ||
134 | this.dataGrid.Dock = System.Windows.Forms.DockStyle.Fill; | ||
135 | this.dataGrid.HeaderForeColor = System.Drawing.SystemColors.ControlText; | ||
136 | this.dataGrid.Location = new System.Drawing.Point(230, 0); | ||
137 | this.dataGrid.Name = "dataGrid"; | ||
138 | this.dataGrid.ReadOnly = true; | ||
139 | this.dataGrid.SelectionBackColor = System.Drawing.SystemColors.Highlight; | ||
140 | this.dataGrid.Size = new System.Drawing.Size(562, 432); | ||
141 | this.dataGrid.TabIndex = 1; | ||
142 | this.dataGrid.KeyDown += new System.Windows.Forms.KeyEventHandler(this.dataGrid_KeyDown); | ||
143 | this.dataGrid.MouseDown += new System.Windows.Forms.MouseEventHandler(this.dataGrid_MouseDown); | ||
144 | this.dataGrid.KeyUp += new System.Windows.Forms.KeyEventHandler(this.dataGrid_KeyUp); | ||
145 | this.dataGrid.MouseMove += new System.Windows.Forms.MouseEventHandler(this.dataGrid_MouseMove); | ||
146 | this.dataGrid.MouseLeave += new System.EventHandler(this.dataGrid_MouseLeave); | ||
147 | // | ||
148 | // treeView | ||
149 | // | ||
150 | this.treeView.Dock = System.Windows.Forms.DockStyle.Left; | ||
151 | this.treeView.HideSelection = false; | ||
152 | this.treeView.ImageIndex = -1; | ||
153 | this.treeView.Location = new System.Drawing.Point(0, 0); | ||
154 | this.treeView.Name = "treeView"; | ||
155 | this.treeView.SelectedImageIndex = -1; | ||
156 | this.treeView.Size = new System.Drawing.Size(224, 432); | ||
157 | this.treeView.TabIndex = 0; | ||
158 | this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyDown); | ||
159 | this.treeView.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Inventory_MouseDown); | ||
160 | this.treeView.KeyUp += new System.Windows.Forms.KeyEventHandler(this.treeView_KeyUp); | ||
161 | this.treeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.treeView_AfterSelect); | ||
162 | // | ||
163 | // toolPanel | ||
164 | // | ||
165 | this.toolPanel.Controls.Add(this.findStopButton); | ||
166 | this.toolPanel.Controls.Add(this.findButton); | ||
167 | this.toolPanel.Controls.Add(this.searchTreeCheckBox); | ||
168 | this.toolPanel.Controls.Add(this.findTextBox); | ||
169 | this.toolPanel.Controls.Add(this.refreshButton); | ||
170 | this.toolPanel.Controls.Add(this.forwardButton); | ||
171 | this.toolPanel.Controls.Add(this.backButton); | ||
172 | this.toolPanel.Dock = System.Windows.Forms.DockStyle.Top; | ||
173 | this.toolPanel.Location = new System.Drawing.Point(0, 0); | ||
174 | this.toolPanel.Name = "toolPanel"; | ||
175 | this.toolPanel.Size = new System.Drawing.Size(792, 40); | ||
176 | this.toolPanel.TabIndex = 2; | ||
177 | // | ||
178 | // findStopButton | ||
179 | // | ||
180 | this.findStopButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); | ||
181 | this.findStopButton.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
182 | this.findStopButton.Location = new System.Drawing.Point(704, 8); | ||
183 | this.findStopButton.Name = "findStopButton"; | ||
184 | this.findStopButton.Size = new System.Drawing.Size(72, 25); | ||
185 | this.findStopButton.TabIndex = 6; | ||
186 | this.findStopButton.Text = "Stop"; | ||
187 | this.findStopButton.Visible = false; | ||
188 | this.findStopButton.Click += new System.EventHandler(this.findStopButton_Click); | ||
189 | // | ||
190 | // findButton | ||
191 | // | ||
192 | this.findButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); | ||
193 | this.findButton.Enabled = false; | ||
194 | this.findButton.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
195 | this.findButton.Location = new System.Drawing.Point(624, 8); | ||
196 | this.findButton.Name = "findButton"; | ||
197 | this.findButton.Size = new System.Drawing.Size(72, 25); | ||
198 | this.findButton.TabIndex = 4; | ||
199 | this.findButton.Text = "Find"; | ||
200 | this.findButton.Click += new System.EventHandler(this.findButton_Click); | ||
201 | // | ||
202 | // searchTreeCheckBox | ||
203 | // | ||
204 | this.searchTreeCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); | ||
205 | this.searchTreeCheckBox.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
206 | this.searchTreeCheckBox.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((System.Byte)(0))); | ||
207 | this.searchTreeCheckBox.Location = new System.Drawing.Point(704, 10); | ||
208 | this.searchTreeCheckBox.Name = "searchTreeCheckBox"; | ||
209 | this.searchTreeCheckBox.Size = new System.Drawing.Size(80, 22); | ||
210 | this.searchTreeCheckBox.TabIndex = 5; | ||
211 | this.searchTreeCheckBox.Text = "In Subtree"; | ||
212 | this.searchTreeCheckBox.CheckedChanged += new System.EventHandler(this.searchTreeCheckBox_CheckedChanged); | ||
213 | // | ||
214 | // findTextBox | ||
215 | // | ||
216 | this.findTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); | ||
217 | this.findTextBox.Location = new System.Drawing.Point(344, 10); | ||
218 | this.findTextBox.Name = "findTextBox"; | ||
219 | this.findTextBox.Size = new System.Drawing.Size(272, 20); | ||
220 | this.findTextBox.TabIndex = 3; | ||
221 | this.findTextBox.Text = ""; | ||
222 | this.findTextBox.TextChanged += new System.EventHandler(this.findTextBox_TextChanged); | ||
223 | this.findTextBox.Enter += new System.EventHandler(this.findTextBox_Enter); | ||
224 | // | ||
225 | // refreshButton | ||
226 | // | ||
227 | this.refreshButton.Enabled = false; | ||
228 | this.refreshButton.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
229 | this.refreshButton.Location = new System.Drawing.Point(160, 8); | ||
230 | this.refreshButton.Name = "refreshButton"; | ||
231 | this.refreshButton.Size = new System.Drawing.Size(72, 25); | ||
232 | this.refreshButton.TabIndex = 2; | ||
233 | this.refreshButton.Text = "Refresh"; | ||
234 | this.refreshButton.Click += new System.EventHandler(this.refreshButton_Click); | ||
235 | // | ||
236 | // forwardButton | ||
237 | // | ||
238 | this.forwardButton.Enabled = false; | ||
239 | this.forwardButton.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
240 | this.forwardButton.Location = new System.Drawing.Point(80, 8); | ||
241 | this.forwardButton.Name = "forwardButton"; | ||
242 | this.forwardButton.Size = new System.Drawing.Size(72, 25); | ||
243 | this.forwardButton.TabIndex = 1; | ||
244 | this.forwardButton.Text = "Forward"; | ||
245 | this.forwardButton.Click += new System.EventHandler(this.forwardButton_Click); | ||
246 | // | ||
247 | // backButton | ||
248 | // | ||
249 | this.backButton.Enabled = false; | ||
250 | this.backButton.FlatStyle = System.Windows.Forms.FlatStyle.System; | ||
251 | this.backButton.Location = new System.Drawing.Point(8, 8); | ||
252 | this.backButton.Name = "backButton"; | ||
253 | this.backButton.Size = new System.Drawing.Size(72, 25); | ||
254 | this.backButton.TabIndex = 0; | ||
255 | this.backButton.Text = "Back"; | ||
256 | this.backButton.Click += new System.EventHandler(this.backButton_Click); | ||
257 | // | ||
258 | // dataPanel | ||
259 | // | ||
260 | this.dataPanel.Controls.Add(this.dataGrid); | ||
261 | this.dataPanel.Controls.Add(this.splitter); | ||
262 | this.dataPanel.Controls.Add(this.treeView); | ||
263 | this.dataPanel.Dock = System.Windows.Forms.DockStyle.Fill; | ||
264 | this.dataPanel.Location = new System.Drawing.Point(0, 40); | ||
265 | this.dataPanel.Name = "dataPanel"; | ||
266 | this.dataPanel.Size = new System.Drawing.Size(792, 432); | ||
267 | this.dataPanel.TabIndex = 1; | ||
268 | // | ||
269 | // splitter | ||
270 | // | ||
271 | this.splitter.Location = new System.Drawing.Point(224, 0); | ||
272 | this.splitter.Name = "splitter"; | ||
273 | this.splitter.Size = new System.Drawing.Size(6, 432); | ||
274 | this.splitter.TabIndex = 2; | ||
275 | this.splitter.TabStop = false; | ||
276 | // | ||
277 | // Inventory | ||
278 | // | ||
279 | this.AcceptButton = this.findButton; | ||
280 | this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); | ||
281 | this.ClientSize = new System.Drawing.Size(792, 472); | ||
282 | this.Controls.Add(this.dataPanel); | ||
283 | this.Controls.Add(this.toolPanel); | ||
284 | this.MinimumSize = new System.Drawing.Size(700, 0); | ||
285 | this.Name = "Inventory"; | ||
286 | this.Text = "MSI Inventory"; | ||
287 | this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.Inventory_KeyDown); | ||
288 | this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Inventory_MouseDown); | ||
289 | this.Load += new System.EventHandler(this.Inventory_Load); | ||
290 | this.KeyUp += new System.Windows.Forms.KeyEventHandler(this.Inventory_KeyUp); | ||
291 | ((System.ComponentModel.ISupportInitialize)(this.dataGrid)).EndInit(); | ||
292 | this.toolPanel.ResumeLayout(false); | ||
293 | this.dataPanel.ResumeLayout(false); | ||
294 | this.ResumeLayout(false); | ||
295 | |||
296 | } | ||
297 | #endregion | ||
298 | |||
299 | |||
300 | #region DataProviders | ||
301 | |||
302 | private IInventoryDataProvider[] DataProviders | ||
303 | { | ||
304 | get | ||
305 | { | ||
306 | if(this.dataProviders == null) | ||
307 | { | ||
308 | ArrayList providerList = new ArrayList(); | ||
309 | providerList.AddRange(FindDataProviders(Assembly.GetExecutingAssembly())); | ||
310 | |||
311 | Uri codebase = new Uri(Assembly.GetExecutingAssembly().CodeBase); | ||
312 | if(codebase.IsFile) | ||
313 | { | ||
314 | foreach(string module in Directory.GetFiles(Path.GetDirectoryName(codebase.LocalPath), "*Inventory.dll")) | ||
315 | { | ||
316 | try | ||
317 | { | ||
318 | providerList.AddRange(FindDataProviders(Assembly.LoadFrom(module))); | ||
319 | } | ||
320 | catch(Exception) { } | ||
321 | } | ||
322 | } | ||
323 | |||
324 | this.dataProviders = (IInventoryDataProvider[]) providerList.ToArray(typeof(IInventoryDataProvider)); | ||
325 | } | ||
326 | return this.dataProviders; | ||
327 | } | ||
328 | } | ||
329 | |||
330 | private static IList FindDataProviders(Assembly assembly) | ||
331 | { | ||
332 | ArrayList providerList = new ArrayList(); | ||
333 | foreach(Type type in assembly.GetTypes()) | ||
334 | { | ||
335 | if(type.IsClass) | ||
336 | { | ||
337 | foreach(Type implementedInterface in type.GetInterfaces()) | ||
338 | { | ||
339 | if(implementedInterface.Equals(typeof(IInventoryDataProvider))) | ||
340 | { | ||
341 | try | ||
342 | { | ||
343 | providerList.Add(assembly.CreateInstance(type.FullName)); | ||
344 | } | ||
345 | catch(Exception) | ||
346 | { | ||
347 | // Data provider's constructor threw an exception for some reason. | ||
348 | // Well, now we can't get any data from that one. | ||
349 | } | ||
350 | } | ||
351 | } | ||
352 | } | ||
353 | } | ||
354 | return providerList; | ||
355 | } | ||
356 | |||
357 | #endregion | ||
358 | |||
359 | private void GoTo(string nodePath, DataGridCell cell) | ||
360 | { | ||
361 | lock(syncRoot) | ||
362 | { | ||
363 | if(this.tablesLoading == null) return; // The tree is being loaded | ||
364 | if(this.navigating) return; // This method is already on the callstack | ||
365 | |||
366 | DataView table = (DataView) this.data[nodePath]; | ||
367 | if(table != null && table == this.dataGrid.DataSource) | ||
368 | { | ||
369 | // Grid is already in view | ||
370 | if(!cell.Equals(anyCell)) this.dataGrid.CurrentCell = cell; | ||
371 | return; | ||
372 | } | ||
373 | if(cell.Equals(anyCell)) cell = zeroCell; | ||
374 | |||
375 | if(this.historyBack.Count == 0 || nodePath != (string) this.historyBack.Peek()) | ||
376 | { | ||
377 | this.historyBack.Push(nodePath); | ||
378 | if(this.cellHistoryBack.Count > 0 && this.historyForward != null) | ||
379 | { | ||
380 | this.cellHistoryBack.Pop(); | ||
381 | this.cellHistoryBack.Push(this.dataGrid.CurrentCell); | ||
382 | } | ||
383 | this.cellHistoryBack.Push(cell); | ||
384 | } | ||
385 | if(this.historyForward != null) | ||
386 | { | ||
387 | this.historyForward.Clear(); | ||
388 | this.cellHistoryForward.Clear(); | ||
389 | } | ||
390 | |||
391 | if(table != null || nodePath.Length == 0 || this.dataProviderMap[nodePath] == null) | ||
392 | { | ||
393 | this.dataGrid.CaptionText = nodePath; | ||
394 | this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; | ||
395 | this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; | ||
396 | this.dataGrid.DataSource = table; | ||
397 | this.dataGrid.CurrentCell = cell; | ||
398 | this.dataGrid.Focus(); | ||
399 | } | ||
400 | else | ||
401 | { | ||
402 | this.dataGrid.CaptionText = nodePath + " (loading...)"; | ||
403 | this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; | ||
404 | this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; | ||
405 | this.dataGrid.DataSource = table; | ||
406 | if(!this.tablesLoading.Contains(nodePath)) | ||
407 | { | ||
408 | this.tablesLoading.Add(nodePath); | ||
409 | this.SetCursor(); | ||
410 | #if SINGLETHREAD | ||
411 | this.LoadTable(nodePath); | ||
412 | #else | ||
413 | new WaitCallback(this.LoadTable).BeginInvoke(nodePath, null, null); | ||
414 | #endif | ||
415 | } | ||
416 | } | ||
417 | |||
418 | this.findButton.Enabled = this.findTextBox.Text.Length > 0 && !searching; | ||
419 | |||
420 | TreeNode treeNode = this.FindNode(nodePath); | ||
421 | if(treeNode != this.treeView.SelectedNode) | ||
422 | { | ||
423 | this.navigating = true; | ||
424 | this.treeView.SelectedNode = treeNode; | ||
425 | this.navigating = false; | ||
426 | } | ||
427 | } | ||
428 | } | ||
429 | |||
430 | private void LoadTable(object nodePathObj) | ||
431 | { | ||
432 | string nodePath = (string) nodePathObj; | ||
433 | IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; | ||
434 | DataView table = null; | ||
435 | if(dataProvider != null) | ||
436 | { | ||
437 | try | ||
438 | { | ||
439 | table = dataProvider.GetData(nodePath); | ||
440 | } | ||
441 | catch(Exception) | ||
442 | { | ||
443 | // Data provider threw an exception for some reason. | ||
444 | // Treat it like it returned no data. | ||
445 | } | ||
446 | } | ||
447 | |||
448 | lock(syncRoot) | ||
449 | { | ||
450 | if(this.tablesLoading == null || !tablesLoading.Contains(nodePath)) return; | ||
451 | if(table == null) | ||
452 | { | ||
453 | this.dataProviderMap.Remove(nodePath); | ||
454 | } | ||
455 | else | ||
456 | { | ||
457 | this.data[nodePath] = table; | ||
458 | } | ||
459 | this.tablesLoading.Remove(nodePath); | ||
460 | } | ||
461 | #if SINGLETHREAD | ||
462 | this.TableLoaded(nodePath); | ||
463 | #else | ||
464 | this.Invoke(new WaitCallback(this.TableLoaded), new object[] { nodePath }); | ||
465 | #endif | ||
466 | } | ||
467 | |||
468 | private void TableLoaded(object nodePathObj) | ||
469 | { | ||
470 | string nodePath = (string) nodePathObj; | ||
471 | lock(syncRoot) | ||
472 | { | ||
473 | this.LoadTableStyle(nodePath); | ||
474 | if(nodePath == this.CurrentNodePath) | ||
475 | { | ||
476 | this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; | ||
477 | this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; | ||
478 | this.dataGrid.CaptionText = nodePath; | ||
479 | this.dataGrid.DataSource = this.CurrentTable; | ||
480 | this.dataGrid.CurrentCell = (DataGridCell) this.cellHistoryBack.Peek(); | ||
481 | this.dataGrid.Focus(); | ||
482 | } | ||
483 | this.SetCursor(); | ||
484 | } | ||
485 | } | ||
486 | |||
487 | private void RefreshData() | ||
488 | { | ||
489 | lock(syncRoot) | ||
490 | { | ||
491 | this.GoTo("", zeroCell); | ||
492 | this.treeView.Nodes.Clear(); | ||
493 | this.dataGrid.TableStyles.Clear(); | ||
494 | this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; | ||
495 | this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; | ||
496 | this.SetControlsEnabled(false); | ||
497 | this.treeView.BeginUpdate(); | ||
498 | #if SINGLETHREAD | ||
499 | this.LoadTree(); | ||
500 | #else | ||
501 | new ThreadStart(this.LoadTree).BeginInvoke(null, null); | ||
502 | #endif | ||
503 | } | ||
504 | } | ||
505 | |||
506 | private void SetControlsEnabled(bool enabled) | ||
507 | { | ||
508 | this.backButton.Enabled = enabled && this.historyBack.Count > 1; | ||
509 | this.forwardButton.Enabled = enabled && this.historyForward.Count > 0; | ||
510 | this.refreshButton.Enabled = enabled; | ||
511 | this.findButton.Enabled = enabled && this.findTextBox.Text.Length > 0 && !searching; | ||
512 | } | ||
513 | |||
514 | private WaitCallback treeStatusCallback; | ||
515 | private int treeNodesLoaded; | ||
516 | private int treeNodesLoadedBase; | ||
517 | private string treeNodesLoading; | ||
518 | private void TreeLoadDataProviderStatus(int status, string currentNode) | ||
519 | { | ||
520 | if (currentNode != null) | ||
521 | { | ||
522 | this.treeNodesLoading = currentNode; | ||
523 | } | ||
524 | |||
525 | this.treeNodesLoaded = treeNodesLoadedBase + status; | ||
526 | string statusString = String.Format("Loading tree... " + this.treeNodesLoaded); | ||
527 | if (!String.IsNullOrEmpty(this.treeNodesLoading)) | ||
528 | { | ||
529 | statusString += ": " + treeNodesLoading; | ||
530 | } | ||
531 | |||
532 | #if SINGLETHREAD | ||
533 | treeStatusCallback(statusString); | ||
534 | #else | ||
535 | this.Invoke(treeStatusCallback, new object[] { statusString }); | ||
536 | #endif | ||
537 | } | ||
538 | |||
539 | private void UpdateTreeLoadStatus(object status) | ||
540 | { | ||
541 | if(status == null) | ||
542 | { | ||
543 | // Loading is complete. | ||
544 | this.treeView.EndUpdate(); | ||
545 | this.SetCursor(); | ||
546 | this.GoTo("Products", new DataGridCell(0, 0)); | ||
547 | this.SetControlsEnabled(true); | ||
548 | } | ||
549 | else | ||
550 | { | ||
551 | this.dataGrid.CaptionText = (string) status; | ||
552 | } | ||
553 | } | ||
554 | |||
555 | private void LoadTree() | ||
556 | { | ||
557 | lock(syncRoot) | ||
558 | { | ||
559 | if(this.tablesLoading == null) return; | ||
560 | this.tablesLoading = null; | ||
561 | this.dataProviderMap.Clear(); | ||
562 | this.data.Clear(); | ||
563 | this.Invoke(new ThreadStart(this.SetCursor)); | ||
564 | } | ||
565 | |||
566 | this.treeStatusCallback = new WaitCallback(UpdateTreeLoadStatus); | ||
567 | this.LoadTreeNodes(); | ||
568 | this.RenderTreeNodes(); | ||
569 | |||
570 | lock(syncRoot) | ||
571 | { | ||
572 | this.tablesLoading = new ArrayList(); | ||
573 | } | ||
574 | // Use a status of null to signal loading complete. | ||
575 | #if SINGLETHREAD | ||
576 | this.UpdateTreeLoadStatus(null); | ||
577 | #else | ||
578 | this.Invoke(new WaitCallback(this.UpdateTreeLoadStatus), new object[] { null }); | ||
579 | #endif | ||
580 | } | ||
581 | |||
582 | private void LoadTreeNodes() | ||
583 | { | ||
584 | #if SINGLETHREAD | ||
585 | this.treeStatusCallback("Loading tree... "); | ||
586 | #else | ||
587 | this.Invoke(this.treeStatusCallback, new object[] { "Loading tree... " }); | ||
588 | #endif | ||
589 | this.treeNodesLoaded = 0; | ||
590 | this.treeNodesLoading = null; | ||
591 | foreach(IInventoryDataProvider dataProvider in this.DataProviders) | ||
592 | { | ||
593 | this.treeNodesLoadedBase = this.treeNodesLoaded; | ||
594 | string[] nodePaths = null; | ||
595 | try | ||
596 | { | ||
597 | nodePaths = dataProvider.GetNodes(new InventoryDataLoadStatusCallback(this.TreeLoadDataProviderStatus)); | ||
598 | } | ||
599 | catch(Exception) | ||
600 | { | ||
601 | // Data provider threw an exception for some reason. | ||
602 | // Treat it like it returned no data. | ||
603 | } | ||
604 | if(nodePaths != null) | ||
605 | { | ||
606 | foreach(string nodePath in nodePaths) | ||
607 | { | ||
608 | if(!this.dataProviderMap.Contains(nodePath)) | ||
609 | { | ||
610 | this.dataProviderMap.Add(nodePath, dataProvider); | ||
611 | } | ||
612 | } | ||
613 | } | ||
614 | } | ||
615 | } | ||
616 | |||
617 | private void RenderTreeNodes() | ||
618 | { | ||
619 | #if SINGLETHREAD | ||
620 | this.treeStatusCallback("Rendering tree... "); | ||
621 | #else | ||
622 | this.Invoke(this.treeStatusCallback, new object[] { "Rendering tree... " }); | ||
623 | #endif | ||
624 | this.treeNodesLoaded = 0; | ||
625 | foreach(DictionaryEntry nodePathAndProvider in this.dataProviderMap) | ||
626 | { | ||
627 | string nodePath = (string) nodePathAndProvider.Key; | ||
628 | #if SINGLETHREAD | ||
629 | this.AddNode(nodePath); | ||
630 | #else | ||
631 | this.Invoke(new WaitCallback(this.AddNode), new object[] { nodePath }); | ||
632 | #endif | ||
633 | } | ||
634 | } | ||
635 | |||
636 | private void LoadTableStyle(string nodePath) | ||
637 | { | ||
638 | DataView table = (DataView) this.data[nodePath]; | ||
639 | if(table != null) | ||
640 | { | ||
641 | DataGridTableStyle tableStyle = this.dataGrid.TableStyles[table.Table.TableName]; | ||
642 | if(tableStyle == null) | ||
643 | { | ||
644 | tableStyle = new DataGridTableStyle(); | ||
645 | tableStyle.MappingName = table.Table.TableName; | ||
646 | tableStyle.RowHeadersVisible = true; | ||
647 | this.dataGrid.TableStyles.Add(tableStyle); | ||
648 | } | ||
649 | foreach(DataColumn column in table.Table.Columns) | ||
650 | { | ||
651 | if(!tableStyle.GridColumnStyles.Contains(column.ColumnName)) | ||
652 | { | ||
653 | string colStyle = (string) ColumnResources.GetObject(column.ColumnName, CultureInfo.InvariantCulture); | ||
654 | if(colStyle != null) | ||
655 | { | ||
656 | string[] colStyleParts = colStyle.Split(','); | ||
657 | DataGridColumnStyle columnStyle = (colStyleParts.Length > 2 && colStyleParts[2] == "bool" | ||
658 | ? (DataGridColumnStyle) new DataGridBoolColumn() : (DataGridColumnStyle) new DataGridTextBoxColumn()); | ||
659 | try { if(colStyleParts.Length > 1) columnStyle.Width = Int32.Parse(colStyleParts[1]); } | ||
660 | catch(FormatException) { } | ||
661 | columnStyle.HeaderText = colStyleParts[0]; | ||
662 | columnStyle.MappingName = column.ColumnName; | ||
663 | tableStyle.GridColumnStyles.Add(columnStyle); | ||
664 | } | ||
665 | } | ||
666 | } | ||
667 | } | ||
668 | } | ||
669 | |||
670 | private static ResourceManager ColumnResources | ||
671 | { | ||
672 | get | ||
673 | { | ||
674 | if(columnResources == null) | ||
675 | { | ||
676 | columnResources = new ResourceManager(typeof(Inventory).Name + ".Columns", typeof(Inventory).Assembly); | ||
677 | } | ||
678 | return columnResources; | ||
679 | } | ||
680 | } | ||
681 | private static ResourceManager columnResources; | ||
682 | |||
683 | private void AddNode(object nodePathObj) | ||
684 | { | ||
685 | string nodePath = (string) nodePathObj; | ||
686 | string[] path = nodePath.Split('\\'); | ||
687 | TreeNodeCollection nodes = this.treeView.Nodes; | ||
688 | TreeNode node = null; | ||
689 | foreach(string pathPart in path) | ||
690 | { | ||
691 | node = null; | ||
692 | for(int i = 0; i < nodes.Count; i++) | ||
693 | { | ||
694 | int c = string.CompareOrdinal(nodes[i].Text, pathPart); | ||
695 | if(c == 0) | ||
696 | { | ||
697 | node = nodes[i]; | ||
698 | break; | ||
699 | } | ||
700 | else if(c > 0) | ||
701 | { | ||
702 | node = new TreeNode(pathPart); | ||
703 | nodes.Insert(i, node); | ||
704 | break; | ||
705 | } | ||
706 | } | ||
707 | if(node == null) | ||
708 | { | ||
709 | node = new TreeNode(pathPart); | ||
710 | nodes.Add(node); | ||
711 | } | ||
712 | nodes = node.Nodes; | ||
713 | } | ||
714 | if(++this.treeNodesLoaded % 1000 == 0) | ||
715 | { | ||
716 | this.UpdateTreeLoadStatus("Rendering tree... " + | ||
717 | (100 * this.treeNodesLoaded / this.dataProviderMap.Count) + "%"); | ||
718 | } | ||
719 | } | ||
720 | |||
721 | public string CurrentNodePath | ||
722 | { | ||
723 | get | ||
724 | { | ||
725 | TreeNode currentNode = this.treeView.SelectedNode; | ||
726 | return currentNode != null ? currentNode.FullPath : null; | ||
727 | } | ||
728 | } | ||
729 | |||
730 | public DataView CurrentTable | ||
731 | { | ||
732 | get | ||
733 | { | ||
734 | string currentNodePath = this.CurrentNodePath; | ||
735 | return currentNodePath != null ? (DataView) this.data[this.CurrentNodePath] : null; | ||
736 | } | ||
737 | } | ||
738 | |||
739 | private TreeNode FindNode(string nodePath) | ||
740 | { | ||
741 | if(nodePath == null) return null; | ||
742 | string[] path = nodePath.Split('\\'); | ||
743 | TreeNodeCollection nodes = this.treeView.Nodes; | ||
744 | TreeNode node = null; | ||
745 | foreach(string pathPart in path) | ||
746 | { | ||
747 | node = null; | ||
748 | for(int i = 0; i < nodes.Count; i++) | ||
749 | { | ||
750 | if(nodes[i].Text == pathPart) | ||
751 | { | ||
752 | node = nodes[i]; | ||
753 | break; | ||
754 | } | ||
755 | } | ||
756 | if(node != null) | ||
757 | { | ||
758 | nodes = node.Nodes; | ||
759 | } | ||
760 | } | ||
761 | return node; | ||
762 | } | ||
763 | |||
764 | private void dataGrid_MouseDown(object sender, MouseEventArgs e) | ||
765 | { | ||
766 | Keys modKeys = Control.ModifierKeys; | ||
767 | if(e.Button == MouseButtons.Left && (modKeys & (Keys.Shift | Keys.Control)) == 0) | ||
768 | { | ||
769 | DataGrid.HitTestInfo hit = this.dataGrid.HitTest(e.X, e.Y); | ||
770 | string link = this.GetLinkForGridHit(hit); | ||
771 | if(link != null) | ||
772 | { | ||
773 | TreeNode node = this.FindNode(link); | ||
774 | if(node != null) | ||
775 | { | ||
776 | this.treeView.SelectedNode = node; | ||
777 | node.Expand(); | ||
778 | } | ||
779 | } | ||
780 | } | ||
781 | this.Inventory_MouseDown(sender, e); | ||
782 | } | ||
783 | |||
784 | private void dataGrid_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) | ||
785 | { | ||
786 | //this.gridLinkTip.SetToolTip(this.dataGrid, null); | ||
787 | DataGrid.HitTestInfo hit = this.dataGrid.HitTest(e.X, e.Y); | ||
788 | if(hit.Type == DataGrid.HitTestType.RowHeader) | ||
789 | { | ||
790 | string link = this.GetLinkForGridHit(hit); | ||
791 | if(link != null) | ||
792 | { | ||
793 | this.mouseOverGridLink = true; | ||
794 | this.SetCursor(); | ||
795 | return; | ||
796 | } | ||
797 | } | ||
798 | else if(this.mouseOverGridLink) | ||
799 | { | ||
800 | this.mouseOverGridLink = false; | ||
801 | this.SetCursor(); | ||
802 | } | ||
803 | } | ||
804 | |||
805 | private void dataGrid_MouseLeave(object sender, System.EventArgs e) | ||
806 | { | ||
807 | this.mouseOverGridLink = false; | ||
808 | this.SetCursor(); | ||
809 | } | ||
810 | |||
811 | private string GetLinkForGridHit(DataGrid.HitTestInfo hit) | ||
812 | { | ||
813 | if(hit.Type == DataGrid.HitTestType.RowHeader && this.tablesLoading != null) | ||
814 | { | ||
815 | string nodePath = this.CurrentNodePath; | ||
816 | DataView table = (DataView) this.data[nodePath]; | ||
817 | if(table != null) | ||
818 | { | ||
819 | DataRow row = table[hit.Row].Row; | ||
820 | IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; | ||
821 | return dataProvider.GetLink(nodePath, table[hit.Row].Row); | ||
822 | } | ||
823 | } | ||
824 | return null; | ||
825 | } | ||
826 | |||
827 | private void HistoryBack() | ||
828 | { | ||
829 | lock(syncRoot) | ||
830 | { | ||
831 | if(this.historyBack.Count > 1) | ||
832 | { | ||
833 | string nodePath = (string) this.historyBack.Pop(); | ||
834 | this.cellHistoryBack.Pop(); | ||
835 | DataGridCell cell = this.dataGrid.CurrentCell; | ||
836 | Stack saveForward = this.historyForward; | ||
837 | this.historyForward = null; | ||
838 | this.GoTo((string) this.historyBack.Pop(), (DataGridCell) this.cellHistoryBack.Pop()); | ||
839 | this.historyForward = saveForward; | ||
840 | this.historyForward.Push(nodePath); | ||
841 | this.cellHistoryForward.Push(cell); | ||
842 | this.backButton.Enabled = this.historyBack.Count > 1; | ||
843 | this.forwardButton.Enabled = this.historyForward.Count > 0; | ||
844 | } | ||
845 | } | ||
846 | } | ||
847 | |||
848 | private void HistoryForward() | ||
849 | { | ||
850 | lock(syncRoot) | ||
851 | { | ||
852 | if(this.historyForward.Count > 0) | ||
853 | { | ||
854 | string nodePath = (string) this.historyForward.Pop(); | ||
855 | DataGridCell cell = (DataGridCell) this.cellHistoryForward.Pop(); | ||
856 | Stack saveForward = this.historyForward; | ||
857 | this.historyForward = null; | ||
858 | this.GoTo(nodePath, cell); | ||
859 | this.historyForward = saveForward; | ||
860 | this.backButton.Enabled = this.historyBack.Count > 1; | ||
861 | this.forwardButton.Enabled = this.historyForward.Count > 0; | ||
862 | } | ||
863 | } | ||
864 | } | ||
865 | |||
866 | #region Find | ||
867 | |||
868 | private void Find() | ||
869 | { | ||
870 | this.BeginFind(); | ||
871 | object[] findNextArgs = new object[] { this.CurrentNodePath, this.dataGrid.CurrentCell, this.treeView.SelectedNode }; | ||
872 | #if SINGLETHREAD | ||
873 | this.FindNext(findNextArgs); | ||
874 | #else | ||
875 | new WaitCallback(this.FindNext).BeginInvoke(findNextArgs, null, null); | ||
876 | #endif | ||
877 | } | ||
878 | |||
879 | [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] | ||
880 | private void FindNext(object start) | ||
881 | { | ||
882 | string nodePath = (string) ((object[]) start)[0]; | ||
883 | DataGridCell startCell = (DataGridCell) ((object[]) start)[1]; | ||
884 | TreeNode searchNode = (TreeNode) ((object[]) start)[2]; | ||
885 | DataGridCell endCell = startCell; | ||
886 | |||
887 | string searchString = this.findTextBox.Text; | ||
888 | if(searchString.Length == 0) return; | ||
889 | |||
890 | bool ignoreCase = true; // TODO: make this a configurable option? | ||
891 | if(ignoreCase) searchString = searchString.ToLowerInvariant(); | ||
892 | |||
893 | if(!this.searchTreeCheckBox.Checked) | ||
894 | { | ||
895 | DataGridCell foundCell; | ||
896 | startCell.ColumnNumber++; | ||
897 | if(FindInTable((DataView) this.data[nodePath], searchString, ignoreCase, | ||
898 | startCell, startCell, true, out foundCell)) | ||
899 | { | ||
900 | #if SINGLETHREAD | ||
901 | this.EndFind(new object[] { nodePath, foundCell }); | ||
902 | #else | ||
903 | this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { nodePath, foundCell } }); | ||
904 | #endif | ||
905 | return; | ||
906 | } | ||
907 | } | ||
908 | else | ||
909 | { | ||
910 | if(this.continueSearchRoot != null) | ||
911 | { | ||
912 | searchNode = this.FindNode(this.continueSearchRoot); | ||
913 | startCell = this.continueSearchCell; | ||
914 | endCell = this.continueSearchEndCell; | ||
915 | } | ||
916 | else | ||
917 | { | ||
918 | this.continueSearchRoot = searchNode.FullPath; | ||
919 | this.continueSearchPath = this.continueSearchRoot; | ||
920 | this.continueSearchEndCell = endCell; | ||
921 | } | ||
922 | //if(searchNode == null) return; | ||
923 | ArrayList nodesList = new ArrayList(); | ||
924 | nodesList.Add(searchNode); | ||
925 | this.GetFlatTreeNodes(searchNode.Nodes, nodesList, true, this.continueSearchRoot); | ||
926 | TreeNode[] nodes = (TreeNode[]) nodesList.ToArray(typeof(TreeNode)); | ||
927 | int startNode = nodesList.IndexOf(this.FindNode(this.continueSearchPath)); | ||
928 | DataGridCell foundCell; | ||
929 | startCell.ColumnNumber++; | ||
930 | for(int i = startNode; i < nodes.Length; i++) | ||
931 | { | ||
932 | if(this.stopSearch) break; | ||
933 | DataGridCell startCellOnThisNode = zeroCell; | ||
934 | if(i == startNode) startCellOnThisNode = startCell; | ||
935 | DataView table = this.GetTableForSearch(nodes[i].FullPath); | ||
936 | if(table != null) | ||
937 | { | ||
938 | if(FindInTable(table, searchString, ignoreCase, startCellOnThisNode, zeroCell, false, out foundCell)) | ||
939 | { | ||
940 | #if SINGLETHREAD | ||
941 | this.EndFind(new object[] { nodes[i].FullPath, foundCell }); | ||
942 | #else | ||
943 | this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { nodes[i].FullPath, foundCell } }); | ||
944 | #endif | ||
945 | return; | ||
946 | } | ||
947 | } | ||
948 | } | ||
949 | if(!this.stopSearch) | ||
950 | { | ||
951 | DataView table = this.GetTableForSearch(searchNode.FullPath); | ||
952 | if(table != null) | ||
953 | { | ||
954 | if(FindInTable(table, searchString, ignoreCase, zeroCell, endCell, false, out foundCell)) | ||
955 | { | ||
956 | #if SINGLETHREAD | ||
957 | this.EndFind(new object[] { searchNode.FullPath, foundCell }); | ||
958 | #else | ||
959 | this.Invoke(new WaitCallback(this.EndFind), new object[] { new object[] { searchNode.FullPath, foundCell } }); | ||
960 | #endif | ||
961 | return; | ||
962 | } | ||
963 | } | ||
964 | } | ||
965 | } | ||
966 | #if SINGLETHREAD | ||
967 | this.EndFind(null); | ||
968 | #else | ||
969 | this.Invoke(new WaitCallback(this.EndFind), new object[] { null }); | ||
970 | #endif | ||
971 | } | ||
972 | |||
973 | private DataView GetTableForSearch(string nodePath) | ||
974 | { | ||
975 | DataView table = (DataView) this.data[nodePath]; | ||
976 | string status = nodePath; | ||
977 | if(table == null) status = status + " (loading)"; | ||
978 | #if SINGLETHREAD | ||
979 | this.FindStatus(nodePath); | ||
980 | #else | ||
981 | this.Invoke(new WaitCallback(this.FindStatus), new object[] { status }); | ||
982 | #endif | ||
983 | if(table == null) | ||
984 | { | ||
985 | this.tablesLoading.Add(nodePath); | ||
986 | this.Invoke(new ThreadStart(this.SetCursor)); | ||
987 | this.LoadTable(nodePath); | ||
988 | table = (DataView) this.data[nodePath]; | ||
989 | } | ||
990 | return table; | ||
991 | } | ||
992 | |||
993 | private void GetFlatTreeNodes(TreeNodeCollection nodes, IList resultsList, bool searchable, string searchRoot) | ||
994 | { | ||
995 | foreach(TreeNode node in nodes) | ||
996 | { | ||
997 | string nodePath = node.FullPath; | ||
998 | IInventoryDataProvider dataProvider = (IInventoryDataProvider) this.dataProviderMap[nodePath]; | ||
999 | if(!searchable || (dataProvider != null && dataProvider.IsNodeSearchable(searchRoot, nodePath))) | ||
1000 | { | ||
1001 | resultsList.Add(node); | ||
1002 | } | ||
1003 | GetFlatTreeNodes(node.Nodes, resultsList, searchable, searchRoot); | ||
1004 | } | ||
1005 | } | ||
1006 | |||
1007 | [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] | ||
1008 | private bool FindInTable(DataView table, string searchString, bool lowerCase, | ||
1009 | DataGridCell startCell, DataGridCell endCell, bool wrap, out DataGridCell foundCell) | ||
1010 | { | ||
1011 | foundCell = new DataGridCell(-1, -1); | ||
1012 | if(table == null) return false; | ||
1013 | if(startCell.RowNumber < 0) startCell.RowNumber = 0; | ||
1014 | if(startCell.ColumnNumber < 0) startCell.ColumnNumber = 0; | ||
1015 | for(int searchRow = startCell.RowNumber; searchRow < table.Count; searchRow++) | ||
1016 | { | ||
1017 | if(this.stopSearch) break; | ||
1018 | if(endCell.RowNumber > startCell.RowNumber && searchRow > endCell.RowNumber) break; | ||
1019 | |||
1020 | DataRowView tableRow = table[searchRow]; | ||
1021 | for(int searchCol = (searchRow == startCell.RowNumber | ||
1022 | ? startCell.ColumnNumber : 0); searchCol < table.Table.Columns.Count; searchCol++) | ||
1023 | { | ||
1024 | if(this.stopSearch) break; | ||
1025 | if(endCell.RowNumber > startCell.RowNumber && searchRow == endCell.RowNumber | ||
1026 | && searchCol >= endCell.ColumnNumber) break; | ||
1027 | |||
1028 | string value = tableRow[searchCol].ToString(); | ||
1029 | if(lowerCase) value = value.ToLowerInvariant(); | ||
1030 | if(value.IndexOf(searchString, StringComparison.Ordinal) >= 0) | ||
1031 | { | ||
1032 | foundCell.RowNumber = searchRow; | ||
1033 | foundCell.ColumnNumber = searchCol; | ||
1034 | return true; | ||
1035 | } | ||
1036 | } | ||
1037 | } | ||
1038 | if(wrap) | ||
1039 | { | ||
1040 | for(int searchRow = 0; searchRow <= endCell.RowNumber; searchRow++) | ||
1041 | { | ||
1042 | if(this.stopSearch) break; | ||
1043 | DataRowView tableRow = table[searchRow]; | ||
1044 | for(int searchCol = 0; searchCol < (searchRow == endCell.RowNumber | ||
1045 | ? endCell.ColumnNumber : table.Table.Columns.Count); searchCol++) | ||
1046 | { | ||
1047 | if(this.stopSearch) break; | ||
1048 | string value = tableRow[searchCol].ToString(); | ||
1049 | if(lowerCase) value = value.ToLowerInvariant(); | ||
1050 | if(value.IndexOf(searchString, StringComparison.Ordinal) >= 0) | ||
1051 | { | ||
1052 | foundCell.RowNumber = searchRow; | ||
1053 | foundCell.ColumnNumber = searchCol; | ||
1054 | return true; | ||
1055 | } | ||
1056 | } | ||
1057 | } | ||
1058 | } | ||
1059 | return false; | ||
1060 | } | ||
1061 | |||
1062 | private void BeginFind() | ||
1063 | { | ||
1064 | lock(syncRoot) | ||
1065 | { | ||
1066 | this.findButton.Enabled = false; | ||
1067 | this.findButton.Text = "Searching..."; | ||
1068 | this.findTextBox.Enabled = false; | ||
1069 | this.searchTreeCheckBox.Visible = false; | ||
1070 | this.findStopButton.Visible = true; | ||
1071 | this.refreshButton.Enabled = false; | ||
1072 | this.searching = true; | ||
1073 | this.stopSearch = false; | ||
1074 | this.SetCursor(); | ||
1075 | } | ||
1076 | } | ||
1077 | |||
1078 | private void FindStatus(object status) | ||
1079 | { | ||
1080 | lock(syncRoot) | ||
1081 | { | ||
1082 | this.dataGrid.CaptionText = "Searching... " + (string) status; | ||
1083 | this.dataGrid.CaptionBackColor = SystemColors.InactiveCaption; | ||
1084 | this.dataGrid.CaptionForeColor = SystemColors.InactiveCaptionText; | ||
1085 | } | ||
1086 | } | ||
1087 | |||
1088 | private void EndFind(object result) | ||
1089 | { | ||
1090 | lock(syncRoot) | ||
1091 | { | ||
1092 | this.searching = false; | ||
1093 | this.refreshButton.Enabled = true; | ||
1094 | this.findStopButton.Visible = false; | ||
1095 | this.searchTreeCheckBox.Visible = true; | ||
1096 | this.findTextBox.Enabled = true; | ||
1097 | this.findButton.Text = "Find"; | ||
1098 | this.findButton.Enabled = true; | ||
1099 | this.dataGrid.CaptionBackColor = SystemColors.ActiveCaption; | ||
1100 | this.dataGrid.CaptionForeColor = SystemColors.ActiveCaptionText; | ||
1101 | this.dataGrid.CaptionText = this.CurrentNodePath; | ||
1102 | if(result != null) | ||
1103 | { | ||
1104 | string nodePath = (string) ((object[]) result)[0]; | ||
1105 | DataGridCell foundCell = (DataGridCell) ((object[]) result)[1]; | ||
1106 | this.GoTo(nodePath, foundCell); | ||
1107 | this.dataGrid.Focus(); | ||
1108 | this.continueSearchPath = nodePath; | ||
1109 | this.continueSearchCell = foundCell; | ||
1110 | if(this.searchTreeCheckBox.Checked) this.searchTreeCheckBox.Text = "Continue"; | ||
1111 | } | ||
1112 | else | ||
1113 | { | ||
1114 | this.continueSearchRoot = null; | ||
1115 | this.continueSearchPath = null; | ||
1116 | this.searchTreeCheckBox.Text = "In Subtree"; | ||
1117 | } | ||
1118 | this.SetCursor(); | ||
1119 | } | ||
1120 | } | ||
1121 | |||
1122 | private void SetCursor() | ||
1123 | { | ||
1124 | if(this.mouseOverGridLink) | ||
1125 | { | ||
1126 | Keys modKeys = Control.ModifierKeys; | ||
1127 | if((modKeys & (Keys.Shift | Keys.Control)) == 0) | ||
1128 | { | ||
1129 | this.Cursor = Cursors.Hand; | ||
1130 | return; | ||
1131 | } | ||
1132 | } | ||
1133 | if(this.tablesLoading == null || this.tablesLoading.Count > 0 || this.searching) | ||
1134 | { | ||
1135 | this.Cursor = Cursors.AppStarting; | ||
1136 | return; | ||
1137 | } | ||
1138 | this.Cursor = Cursors.Arrow; | ||
1139 | } | ||
1140 | |||
1141 | #endregion | ||
1142 | |||
1143 | #region EventHandlers | ||
1144 | |||
1145 | private void Inventory_Load(object sender, System.EventArgs e) | ||
1146 | { | ||
1147 | this.RefreshData(); | ||
1148 | } | ||
1149 | private void refreshButton_Click(object sender, System.EventArgs e) | ||
1150 | { | ||
1151 | this.RefreshData(); | ||
1152 | } | ||
1153 | private void Inventory_MouseDown(object sender, MouseEventArgs e) | ||
1154 | { | ||
1155 | if(e.Button == MouseButtons.XButton1) this.HistoryBack(); | ||
1156 | else if(e.Button == MouseButtons.XButton2) this.HistoryForward(); | ||
1157 | } | ||
1158 | private void Inventory_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1159 | { | ||
1160 | this.SetCursor(); | ||
1161 | if(e.KeyCode == Keys.F3) this.Find(); | ||
1162 | else if(e.KeyCode == Keys.F && (e.Modifiers | Keys.Control) != 0) this.findTextBox.Focus(); | ||
1163 | else if(e.KeyCode == Keys.BrowserBack) this.HistoryBack(); | ||
1164 | else if(e.KeyCode == Keys.BrowserForward) this.HistoryForward(); | ||
1165 | else return; | ||
1166 | e.Handled = true; | ||
1167 | } | ||
1168 | private void treeView_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1169 | { | ||
1170 | this.Inventory_KeyDown(sender, e); | ||
1171 | } | ||
1172 | private void dataGrid_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1173 | { | ||
1174 | this.Inventory_KeyDown(sender, e); | ||
1175 | } | ||
1176 | private void Inventory_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1177 | { | ||
1178 | this.SetCursor(); | ||
1179 | } | ||
1180 | private void treeView_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1181 | { | ||
1182 | this.Inventory_KeyDown(sender, e); | ||
1183 | } | ||
1184 | private void dataGrid_KeyUp(object sender, System.Windows.Forms.KeyEventArgs e) | ||
1185 | { | ||
1186 | this.Inventory_KeyDown(sender, e); | ||
1187 | } | ||
1188 | private void treeView_AfterSelect(object sender, System.Windows.Forms.TreeViewEventArgs e) | ||
1189 | { | ||
1190 | this.GoTo(e.Node.FullPath, anyCell); | ||
1191 | } | ||
1192 | private void backButton_Click(object sender, System.EventArgs e) | ||
1193 | { | ||
1194 | this.HistoryBack(); | ||
1195 | } | ||
1196 | private void forwardButton_Click(object sender, System.EventArgs e) | ||
1197 | { | ||
1198 | this.HistoryForward(); | ||
1199 | } | ||
1200 | private void findTextBox_TextChanged(object sender, System.EventArgs e) | ||
1201 | { | ||
1202 | this.findButton.Enabled = this.findTextBox.Text.Length > 0 && | ||
1203 | this.tablesLoading != null && this.treeView.SelectedNode != null && !searching; | ||
1204 | this.searchTreeCheckBox.Text = "In Subtree"; | ||
1205 | this.continueSearchRoot = null; | ||
1206 | } | ||
1207 | private void findButton_Click(object sender, System.EventArgs e) | ||
1208 | { | ||
1209 | this.Find(); | ||
1210 | } | ||
1211 | private void findTextBox_Enter(object sender, System.EventArgs e) | ||
1212 | { | ||
1213 | findTextBox.SelectAll(); | ||
1214 | } | ||
1215 | private void findStopButton_Click(object sender, System.EventArgs e) | ||
1216 | { | ||
1217 | this.stopSearch = true; | ||
1218 | } | ||
1219 | |||
1220 | private void searchTreeCheckBox_CheckedChanged(object sender, System.EventArgs e) | ||
1221 | { | ||
1222 | if(!searchTreeCheckBox.Checked && searchTreeCheckBox.Text == "Continue") | ||
1223 | { | ||
1224 | this.searchTreeCheckBox.Text = "In Subtree"; | ||
1225 | } | ||
1226 | } | ||
1227 | |||
1228 | #endregion | ||
1229 | |||
1230 | } | ||
1231 | } | ||
diff --git a/src/tools/Dtf/Inventory/Inventory.csproj b/src/tools/Dtf/Inventory/Inventory.csproj new file mode 100644 index 00000000..57bae907 --- /dev/null +++ b/src/tools/Dtf/Inventory/Inventory.csproj | |||
@@ -0,0 +1,42 @@ | |||
1 | |||
2 | <!-- 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. --> | ||
3 | |||
4 | |||
5 | <Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
6 | <PropertyGroup> | ||
7 | <ProjectGuid>{51480F8E-B80F-42DC-91E7-3542C1F12F8C}</ProjectGuid> | ||
8 | <OutputType>WinExe</OutputType> | ||
9 | <RootNamespace>WixToolset.Dtf.Tools.Inventory</RootNamespace> | ||
10 | <AssemblyName>Inventory</AssemblyName> | ||
11 | <TargetFrameworkVersion>v2.0</TargetFrameworkVersion> | ||
12 | <ApplicationIcon>Inventory.ico</ApplicationIcon> | ||
13 | <RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent> | ||
14 | </PropertyGroup> | ||
15 | |||
16 | <ItemGroup> | ||
17 | <Compile Include="components.cs" /> | ||
18 | <Compile Include="Features.cs" /> | ||
19 | <Compile Include="IInventoryDataProvider.cs" /> | ||
20 | <Compile Include="Inventory.cs"> | ||
21 | <SubType>Form</SubType> | ||
22 | </Compile> | ||
23 | <Compile Include="msiutils.cs" /> | ||
24 | <Compile Include="patches.cs" /> | ||
25 | <Compile Include="products.cs" /> | ||
26 | </ItemGroup> | ||
27 | |||
28 | <ItemGroup> | ||
29 | <Content Include="Inventory.ico" /> | ||
30 | </ItemGroup> | ||
31 | |||
32 | <ItemGroup> | ||
33 | <Reference Include="System" /> | ||
34 | <Reference Include="System.Data" /> | ||
35 | <Reference Include="System.Drawing" /> | ||
36 | <Reference Include="System.Windows.Forms" /> | ||
37 | <Reference Include="System.Xml" /> | ||
38 | <ProjectReference Include="..\..\Libraries\WindowsInstaller\WindowsInstaller.csproj" /> | ||
39 | </ItemGroup> | ||
40 | |||
41 | <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), wix.proj))\tools\WixBuild.targets" /> | ||
42 | </Project> | ||
diff --git a/src/tools/Dtf/Inventory/Inventory.ico b/src/tools/Dtf/Inventory/Inventory.ico new file mode 100644 index 00000000..d5757f7a --- /dev/null +++ b/src/tools/Dtf/Inventory/Inventory.ico | |||
Binary files differ | |||
diff --git a/src/tools/Dtf/Inventory/Inventory.resx b/src/tools/Dtf/Inventory/Inventory.resx new file mode 100644 index 00000000..9aeb4d2c --- /dev/null +++ b/src/tools/Dtf/Inventory/Inventory.resx | |||
@@ -0,0 +1,265 @@ | |||
1 | <?xml version="1.0" encoding="utf-8"?> | ||
2 | <root> | ||
3 | <!-- | ||
4 | Microsoft ResX Schema | ||
5 | |||
6 | Version 1.3 | ||
7 | |||
8 | The primary goals of this format is to allow a simple XML format | ||
9 | that is mostly human readable. The generation and parsing of the | ||
10 | various data types are done through the TypeConverter classes | ||
11 | associated with the data types. | ||
12 | |||
13 | Example: | ||
14 | |||
15 | ... ado.net/XML headers & schema ... | ||
16 | <resheader name="resmimetype">text/microsoft-resx</resheader> | ||
17 | <resheader name="version">1.3</resheader> | ||
18 | <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> | ||
19 | <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> | ||
20 | <data name="Name1">this is my long string</data> | ||
21 | <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> | ||
22 | <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> | ||
23 | [base64 mime encoded serialized .NET Framework object] | ||
24 | </data> | ||
25 | <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> | ||
26 | [base64 mime encoded string representing a byte array form of the .NET Framework object] | ||
27 | </data> | ||
28 | |||
29 | There are any number of "resheader" rows that contain simple | ||
30 | name/value pairs. | ||
31 | |||
32 | Each data row contains a name, and value. The row also contains a | ||
33 | type or mimetype. Type corresponds to a .NET class that support | ||
34 | text/value conversion through the TypeConverter architecture. | ||
35 | Classes that don't support this are serialized and stored with the | ||
36 | mimetype set. | ||
37 | |||
38 | The mimetype is used forserialized objects, and tells the | ||
39 | ResXResourceReader how to depersist the object. This is currently not | ||
40 | extensible. For a given mimetype the value must be set accordingly: | ||
41 | |||
42 | Note - application/x-microsoft.net.object.binary.base64 is the format | ||
43 | that the ResXResourceWriter will generate, however the reader can | ||
44 | read any of the formats listed below. | ||
45 | |||
46 | mimetype: application/x-microsoft.net.object.binary.base64 | ||
47 | value : The object must be serialized with | ||
48 | : System.Serialization.Formatters.Binary.BinaryFormatter | ||
49 | : and then encoded with base64 encoding. | ||
50 | |||
51 | mimetype: application/x-microsoft.net.object.soap.base64 | ||
52 | value : The object must be serialized with | ||
53 | : System.Runtime.Serialization.Formatters.Soap.SoapFormatter | ||
54 | : and then encoded with base64 encoding. | ||
55 | |||
56 | mimetype: application/x-microsoft.net.object.bytearray.base64 | ||
57 | value : The object must be serialized into a byte array | ||
58 | : using a System.ComponentModel.TypeConverter | ||
59 | : and then encoded with base64 encoding. | ||
60 | --> | ||
61 | <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> | ||
62 | <xsd:element name="root" msdata:IsDataSet="true"> | ||
63 | <xsd:complexType> | ||
64 | <xsd:choice maxOccurs="unbounded"> | ||
65 | <xsd:element name="data"> | ||
66 | <xsd:complexType> | ||
67 | <xsd:sequence> | ||
68 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||
69 | <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> | ||
70 | </xsd:sequence> | ||
71 | <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" /> | ||
72 | <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> | ||
73 | <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> | ||
74 | </xsd:complexType> | ||
75 | </xsd:element> | ||
76 | <xsd:element name="resheader"> | ||
77 | <xsd:complexType> | ||
78 | <xsd:sequence> | ||
79 | <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> | ||
80 | </xsd:sequence> | ||
81 | <xsd:attribute name="name" type="xsd:string" use="required" /> | ||
82 | </xsd:complexType> | ||
83 | </xsd:element> | ||
84 | </xsd:choice> | ||
85 | </xsd:complexType> | ||
86 | </xsd:element> | ||
87 | </xsd:schema> | ||
88 | <resheader name="resmimetype"> | ||
89 | <value>text/microsoft-resx</value> | ||
90 | </resheader> | ||
91 | <resheader name="version"> | ||
92 | <value>1.3</value> | ||
93 | </resheader> | ||
94 | <resheader name="reader"> | ||
95 | <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||
96 | </resheader> | ||
97 | <resheader name="writer"> | ||
98 | <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||
99 | </resheader> | ||
100 | <data name="dataGrid.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
101 | <value>False</value> | ||
102 | </data> | ||
103 | <data name="dataGrid.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
104 | <value>Private</value> | ||
105 | </data> | ||
106 | <data name="dataGrid.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
107 | <value>Private</value> | ||
108 | </data> | ||
109 | <data name="treeView.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
110 | <value>Private</value> | ||
111 | </data> | ||
112 | <data name="treeView.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
113 | <value>Private</value> | ||
114 | </data> | ||
115 | <data name="treeView.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
116 | <value>False</value> | ||
117 | </data> | ||
118 | <data name="toolPanel.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
119 | <value>False</value> | ||
120 | </data> | ||
121 | <data name="toolPanel.SnapToGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
122 | <value>True</value> | ||
123 | </data> | ||
124 | <data name="toolPanel.DrawGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
125 | <value>True</value> | ||
126 | </data> | ||
127 | <data name="toolPanel.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
128 | <value>Private</value> | ||
129 | </data> | ||
130 | <data name="toolPanel.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
131 | <value>Private</value> | ||
132 | </data> | ||
133 | <data name="toolPanel.GridSize" type="System.Drawing.Size, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||
134 | <value>8, 8</value> | ||
135 | </data> | ||
136 | <data name="findStopButton.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
137 | <value>False</value> | ||
138 | </data> | ||
139 | <data name="findStopButton.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
140 | <value>Private</value> | ||
141 | </data> | ||
142 | <data name="findStopButton.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
143 | <value>Private</value> | ||
144 | </data> | ||
145 | <data name="findButton.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
146 | <value>False</value> | ||
147 | </data> | ||
148 | <data name="findButton.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
149 | <value>Private</value> | ||
150 | </data> | ||
151 | <data name="findButton.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
152 | <value>Private</value> | ||
153 | </data> | ||
154 | <data name="searchTreeCheckBox.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
155 | <value>False</value> | ||
156 | </data> | ||
157 | <data name="searchTreeCheckBox.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
158 | <value>Private</value> | ||
159 | </data> | ||
160 | <data name="searchTreeCheckBox.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
161 | <value>Private</value> | ||
162 | </data> | ||
163 | <data name="findTextBox.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
164 | <value>Private</value> | ||
165 | </data> | ||
166 | <data name="findTextBox.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
167 | <value>False</value> | ||
168 | </data> | ||
169 | <data name="findTextBox.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
170 | <value>Private</value> | ||
171 | </data> | ||
172 | <data name="refreshButton.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
173 | <value>False</value> | ||
174 | </data> | ||
175 | <data name="refreshButton.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
176 | <value>Private</value> | ||
177 | </data> | ||
178 | <data name="refreshButton.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
179 | <value>Private</value> | ||
180 | </data> | ||
181 | <data name="forwardButton.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
182 | <value>False</value> | ||
183 | </data> | ||
184 | <data name="forwardButton.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
185 | <value>Private</value> | ||
186 | </data> | ||
187 | <data name="forwardButton.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
188 | <value>Private</value> | ||
189 | </data> | ||
190 | <data name="backButton.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
191 | <value>False</value> | ||
192 | </data> | ||
193 | <data name="backButton.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
194 | <value>Private</value> | ||
195 | </data> | ||
196 | <data name="backButton.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
197 | <value>Private</value> | ||
198 | </data> | ||
199 | <data name="dataPanel.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
200 | <value>False</value> | ||
201 | </data> | ||
202 | <data name="dataPanel.SnapToGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
203 | <value>True</value> | ||
204 | </data> | ||
205 | <data name="dataPanel.DrawGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
206 | <value>True</value> | ||
207 | </data> | ||
208 | <data name="dataPanel.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
209 | <value>Private</value> | ||
210 | </data> | ||
211 | <data name="dataPanel.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
212 | <value>Private</value> | ||
213 | </data> | ||
214 | <data name="dataPanel.GridSize" type="System.Drawing.Size, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||
215 | <value>8, 8</value> | ||
216 | </data> | ||
217 | <data name="splitter.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
218 | <value>False</value> | ||
219 | </data> | ||
220 | <data name="splitter.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
221 | <value>Private</value> | ||
222 | </data> | ||
223 | <data name="splitter.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
224 | <value>Private</value> | ||
225 | </data> | ||
226 | <data name="gridLinkTip.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
227 | <value>Private</value> | ||
228 | </data> | ||
229 | <data name="gridLinkTip.Location" type="System.Drawing.Point, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||
230 | <value>17, 17</value> | ||
231 | </data> | ||
232 | <data name="gridLinkTip.Modifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
233 | <value>Private</value> | ||
234 | </data> | ||
235 | <data name="$this.Locked" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
236 | <value>False</value> | ||
237 | </data> | ||
238 | <data name="$this.Language" type="System.Globalization.CultureInfo, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
239 | <value>(Default)</value> | ||
240 | </data> | ||
241 | <data name="$this.TrayLargeIcon" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
242 | <value>False</value> | ||
243 | </data> | ||
244 | <data name="$this.Localizable" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
245 | <value>False</value> | ||
246 | </data> | ||
247 | <data name="$this.GridSize" type="System.Drawing.Size, System.Drawing, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"> | ||
248 | <value>8, 8</value> | ||
249 | </data> | ||
250 | <data name="$this.DrawGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
251 | <value>True</value> | ||
252 | </data> | ||
253 | <data name="$this.TrayHeight" type="System.Int32, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
254 | <value>80</value> | ||
255 | </data> | ||
256 | <data name="$this.SnapToGrid" type="System.Boolean, mscorlib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
257 | <value>True</value> | ||
258 | </data> | ||
259 | <data name="$this.Name"> | ||
260 | <value>Inventory</value> | ||
261 | </data> | ||
262 | <data name="$this.DefaultModifiers" type="System.CodeDom.MemberAttributes, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> | ||
263 | <value>Private</value> | ||
264 | </data> | ||
265 | </root> \ No newline at end of file | ||
diff --git a/src/tools/Dtf/Inventory/components.cs b/src/tools/Dtf/Inventory/components.cs new file mode 100644 index 00000000..b516af46 --- /dev/null +++ b/src/tools/Dtf/Inventory/components.cs | |||
@@ -0,0 +1,626 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.IO; | ||
5 | using System.Data; | ||
6 | using System.Text; | ||
7 | using System.Collections; | ||
8 | using System.Collections.Generic; | ||
9 | using System.Globalization; | ||
10 | using System.Windows.Forms; | ||
11 | using Microsoft.Win32; | ||
12 | using WixToolset.Dtf.WindowsInstaller; | ||
13 | using View = WixToolset.Dtf.WindowsInstaller.View; | ||
14 | |||
15 | namespace WixToolset.Dtf.Tools.Inventory | ||
16 | { | ||
17 | /// <summary> | ||
18 | /// Provides inventory data about components of products installed on the system. | ||
19 | /// </summary> | ||
20 | public class ComponentsInventory : IInventoryDataProvider | ||
21 | { | ||
22 | private static object syncRoot = new object(); | ||
23 | |||
24 | public ComponentsInventory() | ||
25 | { | ||
26 | } | ||
27 | |||
28 | public string Description | ||
29 | { | ||
30 | get { return "Components of installed products"; } | ||
31 | } | ||
32 | |||
33 | private Hashtable componentProductsMap; | ||
34 | |||
35 | public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) | ||
36 | { | ||
37 | ArrayList nodes = new ArrayList(); | ||
38 | componentProductsMap = new Hashtable(); | ||
39 | foreach(ProductInstallation product in ProductInstallation.AllProducts) | ||
40 | { | ||
41 | string productName = MsiUtils.GetProductName(product.ProductCode); | ||
42 | statusCallback(nodes.Count, String.Format(@"Products\{0}", productName)); | ||
43 | |||
44 | try | ||
45 | { | ||
46 | IntPtr hWnd = IntPtr.Zero; | ||
47 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
48 | lock(syncRoot) // Only one Installer session can be active at a time | ||
49 | { | ||
50 | using (Session session = Installer.OpenProduct(product.ProductCode)) | ||
51 | { | ||
52 | statusCallback(nodes.Count, String.Format(@"Products\{0}\Features", productName)); | ||
53 | IList<string> features = session.Database.ExecuteStringQuery("SELECT `Feature` FROM `Feature`"); | ||
54 | string[] featuresArray = new string[features.Count]; | ||
55 | features.CopyTo(featuresArray, 0); | ||
56 | Array.Sort(featuresArray, 0, featuresArray.Length, StringComparer.OrdinalIgnoreCase); | ||
57 | foreach (string feature in featuresArray) | ||
58 | { | ||
59 | nodes.Add(String.Format(@"Products\{0}\Features\{1}", productName, feature)); | ||
60 | } | ||
61 | statusCallback(nodes.Count, String.Format(@"Products\{0}\Components", productName)); | ||
62 | nodes.Add(String.Format(@"Products\{0}\Components", productName)); | ||
63 | IList<string> components = session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`"); | ||
64 | for (int i = 0; i < components.Count; i++) | ||
65 | { | ||
66 | string component = components[i]; | ||
67 | if (component.Length > 0) | ||
68 | { | ||
69 | nodes.Add(String.Format(@"Products\{0}\Components\{1}", productName, component)); | ||
70 | ArrayList sharingProducts = (ArrayList) componentProductsMap[component]; | ||
71 | if (sharingProducts == null) | ||
72 | { | ||
73 | sharingProducts = new ArrayList(); | ||
74 | componentProductsMap[component] = sharingProducts; | ||
75 | } | ||
76 | sharingProducts.Add(product.ProductCode); | ||
77 | } | ||
78 | if (i % 100 == 0) statusCallback(nodes.Count, null); | ||
79 | } | ||
80 | nodes.Add(String.Format(@"Products\{0}\Files", productName)); | ||
81 | nodes.Add(String.Format(@"Products\{0}\Registry", productName)); | ||
82 | statusCallback(nodes.Count, String.Empty); | ||
83 | } | ||
84 | } | ||
85 | } | ||
86 | catch(InstallerException) { } | ||
87 | } | ||
88 | statusCallback(nodes.Count, @"Products\...\Components\...\Sharing"); | ||
89 | foreach (DictionaryEntry componentProducts in componentProductsMap) | ||
90 | { | ||
91 | string component = (string) componentProducts.Key; | ||
92 | ArrayList products = (ArrayList) componentProducts.Value; | ||
93 | if(products.Count > 1) | ||
94 | { | ||
95 | foreach(string productCode in products) | ||
96 | { | ||
97 | nodes.Add(String.Format(@"Products\{0}\Components\{1}\Sharing", MsiUtils.GetProductName(productCode), component)); | ||
98 | } | ||
99 | } | ||
100 | } | ||
101 | statusCallback(nodes.Count, String.Empty); | ||
102 | return (string[]) nodes.ToArray(typeof(string)); | ||
103 | } | ||
104 | |||
105 | public bool IsNodeSearchable(string searchRoot, string searchNode) | ||
106 | { | ||
107 | string[] rootPath = searchRoot.Split('\\'); | ||
108 | string[] nodePath = searchNode.Split('\\'); | ||
109 | if(rootPath.Length < 3 && nodePath.Length >= 3 && nodePath[0] == "Products" && nodePath[2] == "Components") | ||
110 | { | ||
111 | // When searching an entire product, don't search the "Components" subtree -- | ||
112 | // it just has duplicate data from the Files and Registry table. And if you | ||
113 | // really want to know about the component, it's only a click away from | ||
114 | // those other tables. | ||
115 | return false; | ||
116 | } | ||
117 | return true; | ||
118 | } | ||
119 | |||
120 | public DataView GetData(string nodePath) | ||
121 | { | ||
122 | string[] path = nodePath.Split('\\'); | ||
123 | |||
124 | if(path.Length == 4 && path[0] == "Products" && path[2] == "Features") | ||
125 | { | ||
126 | return GetFeatureComponentsData(MsiUtils.GetProductCode(path[1]), path[3]); | ||
127 | } | ||
128 | else if(path.Length == 3 && path[0] == "Products" && path[2] == "Components") | ||
129 | { | ||
130 | return GetProductComponentsData(MsiUtils.GetProductCode(path[1])); | ||
131 | } | ||
132 | else if(path.Length == 4 && path[0] == "Products" && path[2] == "Components") | ||
133 | { | ||
134 | return GetComponentData(MsiUtils.GetProductCode(path[1]), path[3]); | ||
135 | } | ||
136 | else if(path.Length == 5 && path[0] == "Products" && path[2] == "Components" && path[4] == "Sharing") | ||
137 | { | ||
138 | return GetComponentProductsData(path[3]); | ||
139 | } | ||
140 | else if(path.Length == 3 && path[0] == "Products" && path[2] == "Files") | ||
141 | { | ||
142 | return GetProductFilesData(MsiUtils.GetProductCode(path[1])); | ||
143 | } | ||
144 | else if(path.Length == 3 && path[0] == "Products" && path[2] == "Registry") | ||
145 | { | ||
146 | return GetProductRegistryData(MsiUtils.GetProductCode(path[1])); | ||
147 | } | ||
148 | return null; | ||
149 | } | ||
150 | |||
151 | public DataView GetComponentData(string productCode, string componentCode) | ||
152 | { | ||
153 | DataTable table = new DataTable("ProductComponentItems"); | ||
154 | table.Locale = CultureInfo.InvariantCulture; | ||
155 | table.Columns.Add("ProductComponentItemsIsKey", typeof(bool)); | ||
156 | table.Columns.Add("ProductComponentItemsKey", typeof(string)); | ||
157 | table.Columns.Add("ProductComponentItemsPath", typeof(string)); | ||
158 | table.Columns.Add("ProductComponentItemsExists", typeof(bool)); | ||
159 | table.Columns.Add("ProductComponentItemsDbVersion", typeof(string)); | ||
160 | table.Columns.Add("ProductComponentItemsInstalledVersion", typeof(string)); | ||
161 | table.Columns.Add("ProductComponentItemsInstalledMatch", typeof(bool)); | ||
162 | try | ||
163 | { | ||
164 | IntPtr hWnd = IntPtr.Zero; | ||
165 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
166 | lock(syncRoot) // Only one Installer session can be active at a time | ||
167 | { | ||
168 | using(Session session = Installer.OpenProduct(productCode)) | ||
169 | { | ||
170 | session.DoAction("CostInitialize"); | ||
171 | session.DoAction("FileCost"); | ||
172 | session.DoAction("CostFinalize"); | ||
173 | |||
174 | foreach(object[] row in this.GetComponentFilesRows(productCode, componentCode, session, false)) | ||
175 | { | ||
176 | table.Rows.Add(row); | ||
177 | } | ||
178 | foreach(object[] row in this.GetComponentRegistryRows(productCode, componentCode, session, false)) | ||
179 | { | ||
180 | table.Rows.Add(row); | ||
181 | } | ||
182 | } | ||
183 | } | ||
184 | return new DataView(table, "", "ProductComponentItemsPath ASC", DataViewRowState.CurrentRows); | ||
185 | } | ||
186 | catch(InstallerException) { } | ||
187 | return null; | ||
188 | } | ||
189 | |||
190 | private object[][] GetComponentFilesRows(string productCode, string componentCode, Session session, bool includeComponent) | ||
191 | { | ||
192 | ArrayList rows = new ArrayList(); | ||
193 | string componentPath = new ComponentInstallation(componentCode, productCode).Path; | ||
194 | |||
195 | string componentKey = (string) session.Database.ExecuteScalar( | ||
196 | "SELECT `Component` FROM `Component` WHERE `ComponentId` = '{0}'", componentCode); | ||
197 | if(componentKey == null) return null; | ||
198 | int attributes = Convert.ToInt32(session.Database.ExecuteScalar( | ||
199 | "SELECT `Attributes` FROM `Component` WHERE `Component` = '{0}'", componentKey)); | ||
200 | bool registryKeyPath = (attributes & (int) ComponentAttributes.RegistryKeyPath) != 0; | ||
201 | if(!registryKeyPath && componentPath.Length > 0) componentPath = Path.GetDirectoryName(componentPath); | ||
202 | string keyPath = (string) session.Database.ExecuteScalar( | ||
203 | "SELECT `KeyPath` FROM `Component` WHERE `Component` = '{0}'", componentKey); | ||
204 | |||
205 | using (View view = session.Database.OpenView("SELECT `File`, `FileName`, `Version`, `Language`, " + | ||
206 | "`Attributes` FROM `File` WHERE `Component_` = '{0}'", componentKey)) | ||
207 | { | ||
208 | view.Execute(); | ||
209 | |||
210 | foreach (Record rec in view) using (rec) | ||
211 | { | ||
212 | string fileKey = (string) rec["File"]; | ||
213 | bool isKey = !registryKeyPath && keyPath == fileKey; | ||
214 | |||
215 | string dbVersion = (string) rec["Version"]; | ||
216 | bool versionedFile = dbVersion.Length != 0; | ||
217 | if(versionedFile) | ||
218 | { | ||
219 | string language = (string) rec["Language"]; | ||
220 | if(language.Length > 0) | ||
221 | { | ||
222 | dbVersion = dbVersion + " (" + language + ")"; | ||
223 | } | ||
224 | } | ||
225 | else if(session.Database.Tables.Contains("MsiFileHash")) | ||
226 | { | ||
227 | IList<int> hash = session.Database.ExecuteIntegerQuery("SELECT `HashPart1`, `HashPart2`, " + | ||
228 | "`HashPart3`, `HashPart4` FROM `MsiFileHash` WHERE `File_` = '{0}'", fileKey); | ||
229 | if(hash != null && hash.Count == 4) | ||
230 | { | ||
231 | dbVersion = this.GetFileHashString(hash); | ||
232 | } | ||
233 | } | ||
234 | |||
235 | string filePath = GetLongFileName((string) rec["FileName"]); | ||
236 | bool exists = false; | ||
237 | bool installedMatch = false; | ||
238 | string installedVersion = ""; | ||
239 | if(!registryKeyPath && componentPath.Length > 0) | ||
240 | { | ||
241 | filePath = Path.Combine(componentPath, filePath); | ||
242 | |||
243 | if(File.Exists(filePath)) | ||
244 | { | ||
245 | exists = true; | ||
246 | if(versionedFile) | ||
247 | { | ||
248 | installedVersion = Installer.GetFileVersion(filePath); | ||
249 | string language = Installer.GetFileLanguage(filePath); | ||
250 | if(language.Length > 0) | ||
251 | { | ||
252 | installedVersion = installedVersion + " (" + language + ")"; | ||
253 | } | ||
254 | } | ||
255 | else | ||
256 | { | ||
257 | int[] hash = new int[4]; | ||
258 | Installer.GetFileHash(filePath, hash); | ||
259 | installedVersion = this.GetFileHashString(hash); | ||
260 | } | ||
261 | installedMatch = installedVersion == dbVersion; | ||
262 | } | ||
263 | } | ||
264 | |||
265 | object[] row; | ||
266 | if(includeComponent) row = new object[] { isKey, fileKey, filePath, exists, dbVersion, installedVersion, installedMatch, componentCode }; | ||
267 | else row = new object[] { isKey, fileKey, filePath, exists, dbVersion, installedVersion, installedMatch }; | ||
268 | rows.Add(row); | ||
269 | } | ||
270 | } | ||
271 | |||
272 | return (object[][]) rows.ToArray(typeof(object[])); | ||
273 | } | ||
274 | |||
275 | private string GetLongFileName(string fileName) | ||
276 | { | ||
277 | string[] fileNames = fileName.Split('|'); | ||
278 | return fileNames.Length == 1? fileNames[0] : fileNames[1]; | ||
279 | } | ||
280 | |||
281 | private string GetFileHashString(IList<int> hash) | ||
282 | { | ||
283 | return String.Format("{0:X8}{1:X8}{2:X8}{3:X8}", (uint) hash[0], (uint) hash[1], (uint) hash[2], (uint) hash[3]); | ||
284 | } | ||
285 | |||
286 | private object[][] GetComponentRegistryRows(string productCode, string componentCode, Session session, bool includeComponent) | ||
287 | { | ||
288 | ArrayList rows = new ArrayList(); | ||
289 | string componentPath = new ComponentInstallation(componentCode, productCode).Path; | ||
290 | |||
291 | string componentKey = (string) session.Database.ExecuteScalar( | ||
292 | "SELECT `Component` FROM `Component` WHERE `ComponentId` = '{0}'", componentCode); | ||
293 | if(componentKey == null) return null; | ||
294 | int attributes = Convert.ToInt32(session.Database.ExecuteScalar( | ||
295 | "SELECT `Attributes` FROM `Component` WHERE `Component` = '{0}'", componentKey)); | ||
296 | bool registryKeyPath = (attributes & (int) ComponentAttributes.RegistryKeyPath) != 0; | ||
297 | if(!registryKeyPath && componentPath.Length > 0) componentPath = Path.GetDirectoryName(componentPath); | ||
298 | string keyPath = (string) session.Database.ExecuteScalar( | ||
299 | "SELECT `KeyPath` FROM `Component` WHERE `Component` = '{0}'", componentKey); | ||
300 | |||
301 | using (View view = session.Database.OpenView("SELECT `Registry`, `Root`, `Key`, `Name`, " + | ||
302 | "`Value` FROM `Registry` WHERE `Component_` = '{0}'", componentKey)) | ||
303 | { | ||
304 | view.Execute(); | ||
305 | |||
306 | foreach (Record rec in view) using (rec) | ||
307 | { | ||
308 | string regName = (string) rec["Name"]; | ||
309 | if(regName == "-") continue; // Don't list deleted keys | ||
310 | |||
311 | string regTableKey = (string) rec["Registry"]; | ||
312 | bool isKey = registryKeyPath && keyPath == regTableKey; | ||
313 | string regPath = this.GetRegistryPath(session, (RegistryRoot) Convert.ToInt32(rec["Root"]), | ||
314 | (string) rec["Key"], (string) rec["Name"]); | ||
315 | |||
316 | string dbValue; | ||
317 | using(Record formatRec = new Record(0)) | ||
318 | { | ||
319 | formatRec[0] = rec["Value"]; | ||
320 | dbValue = session.FormatRecord(formatRec); | ||
321 | } | ||
322 | |||
323 | string installedValue = this.GetRegistryValue(regPath); | ||
324 | bool exists = installedValue != null; | ||
325 | if(!exists) installedValue = ""; | ||
326 | bool match = installedValue == dbValue; | ||
327 | |||
328 | object[] row; | ||
329 | if(includeComponent) row = new object[] { isKey, regTableKey, regPath, exists, dbValue, installedValue, match, componentCode }; | ||
330 | else row = new object[] { isKey, regTableKey, regPath, exists, dbValue, installedValue, match }; | ||
331 | rows.Add(row); | ||
332 | } | ||
333 | } | ||
334 | |||
335 | return (object[][]) rows.ToArray(typeof(object[])); | ||
336 | } | ||
337 | |||
338 | private string GetRegistryPath(Session session, RegistryRoot root, string key, string name) | ||
339 | { | ||
340 | bool allUsers = session.EvaluateCondition("ALLUSERS = 1", true); | ||
341 | string rootName = "????"; | ||
342 | switch(root) | ||
343 | { | ||
344 | case RegistryRoot.LocalMachine : rootName = "HKLM"; break; | ||
345 | case RegistryRoot.CurrentUser : rootName = "HKCU"; break; | ||
346 | case RegistryRoot.Users : rootName = "HKU"; break; | ||
347 | case RegistryRoot.UserOrMachine: rootName = (allUsers ? "HKLM" : "HKCU"); break; | ||
348 | case RegistryRoot.ClassesRoot : rootName = (allUsers ? @"HKLM\Software\Classes" : @"HKCU\Software\Classes"); break; | ||
349 | // TODO: Technically, RegistryRoot.ClassesRoot should be under HKLM on NT4. | ||
350 | } | ||
351 | if(name.Length == 0) name = "(Default)"; | ||
352 | if(name == "+" || name == "*") name = ""; | ||
353 | else name = " : " + name; | ||
354 | using(Record formatRec = new Record(0)) | ||
355 | { | ||
356 | formatRec[0] = String.Format(@"{0}\{1}{2}", rootName, key, name); | ||
357 | return session.FormatRecord(formatRec); | ||
358 | } | ||
359 | } | ||
360 | |||
361 | private string GetRegistryValue(string regPath) | ||
362 | { | ||
363 | string valueName = null; | ||
364 | int iColon = regPath.IndexOf(" : ", StringComparison.Ordinal) + 1; | ||
365 | if(iColon > 0) | ||
366 | { | ||
367 | valueName = regPath.Substring(iColon + 2); | ||
368 | regPath = regPath.Substring(0, iColon - 1); | ||
369 | } | ||
370 | if(valueName == "(Default)") valueName = ""; | ||
371 | |||
372 | RegistryKey root; | ||
373 | if(regPath.StartsWith(@"HKLM\", StringComparison.Ordinal)) | ||
374 | { | ||
375 | root = Registry.LocalMachine; | ||
376 | regPath = regPath.Substring(5); | ||
377 | } | ||
378 | else if(regPath.StartsWith(@"HKCU\", StringComparison.Ordinal)) | ||
379 | { | ||
380 | root = Registry.CurrentUser; | ||
381 | regPath = regPath.Substring(5); | ||
382 | } | ||
383 | else if(regPath.StartsWith(@"HKU\", StringComparison.Ordinal)) | ||
384 | { | ||
385 | root = Registry.Users; | ||
386 | regPath = regPath.Substring(4); | ||
387 | } | ||
388 | else return null; | ||
389 | |||
390 | using(RegistryKey regKey = root.OpenSubKey(regPath)) | ||
391 | { | ||
392 | if(regKey != null) | ||
393 | { | ||
394 | if(valueName == null) | ||
395 | { | ||
396 | // Just checking for the existence of the key. | ||
397 | return ""; | ||
398 | } | ||
399 | object value = regKey.GetValue(valueName); | ||
400 | if(value is string[]) | ||
401 | { | ||
402 | value = String.Join("[~]", (string[]) value); | ||
403 | } | ||
404 | else if(value is int) | ||
405 | { | ||
406 | value = "#" + value.ToString(); | ||
407 | } | ||
408 | else if(value is byte[]) | ||
409 | { | ||
410 | byte[] valueBytes = (byte[]) value; | ||
411 | StringBuilder byteString = new StringBuilder("#x"); | ||
412 | for(int i = 0; i < valueBytes.Length; i++) | ||
413 | { | ||
414 | byteString.Append(valueBytes[i].ToString("x2")); | ||
415 | } | ||
416 | value = byteString.ToString(); | ||
417 | } | ||
418 | return (value != null ? value.ToString() : null); | ||
419 | } | ||
420 | } | ||
421 | return null; | ||
422 | } | ||
423 | |||
424 | public DataView GetProductFilesData(string productCode) | ||
425 | { | ||
426 | DataTable table = new DataTable("ProductFiles"); | ||
427 | table.Locale = CultureInfo.InvariantCulture; | ||
428 | table.Columns.Add("ProductFilesIsKey", typeof(bool)); | ||
429 | table.Columns.Add("ProductFilesKey", typeof(string)); | ||
430 | table.Columns.Add("ProductFilesPath", typeof(string)); | ||
431 | table.Columns.Add("ProductFilesExists", typeof(bool)); | ||
432 | table.Columns.Add("ProductFilesDbVersion", typeof(string)); | ||
433 | table.Columns.Add("ProductFilesInstalledVersion", typeof(string)); | ||
434 | table.Columns.Add("ProductFilesInstalledMatch", typeof(bool)); | ||
435 | table.Columns.Add("ProductFilesComponentID", typeof(string)); | ||
436 | try | ||
437 | { | ||
438 | IntPtr hWnd = IntPtr.Zero; | ||
439 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
440 | lock(syncRoot) // Only one Installer session can be active at a time | ||
441 | { | ||
442 | using(Session session = Installer.OpenProduct(productCode)) | ||
443 | { | ||
444 | session.DoAction("CostInitialize"); | ||
445 | session.DoAction("FileCost"); | ||
446 | session.DoAction("CostFinalize"); | ||
447 | |||
448 | foreach(string componentCode in session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`")) | ||
449 | { | ||
450 | foreach(object[] row in this.GetComponentFilesRows(productCode, componentCode, session, true)) | ||
451 | { | ||
452 | table.Rows.Add(row); | ||
453 | } | ||
454 | } | ||
455 | } | ||
456 | } | ||
457 | return new DataView(table, "", "ProductFilesPath ASC", DataViewRowState.CurrentRows); | ||
458 | } | ||
459 | catch(InstallerException) { } | ||
460 | return null; | ||
461 | } | ||
462 | |||
463 | public DataView GetProductRegistryData(string productCode) | ||
464 | { | ||
465 | DataTable table = new DataTable("ProductRegistry"); | ||
466 | table.Locale = CultureInfo.InvariantCulture; | ||
467 | table.Columns.Add("ProductRegistryIsKey", typeof(bool)); | ||
468 | table.Columns.Add("ProductRegistryKey", typeof(string)); | ||
469 | table.Columns.Add("ProductRegistryPath", typeof(string)); | ||
470 | table.Columns.Add("ProductRegistryExists", typeof(bool)); | ||
471 | table.Columns.Add("ProductRegistryDbVersion", typeof(string)); | ||
472 | table.Columns.Add("ProductRegistryInstalledVersion", typeof(string)); | ||
473 | table.Columns.Add("ProductRegistryInstalledMatch", typeof(bool)); | ||
474 | table.Columns.Add("ProductRegistryComponentID", typeof(string)); | ||
475 | try | ||
476 | { | ||
477 | IntPtr hWnd = IntPtr.Zero; | ||
478 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
479 | lock(syncRoot) // Only one Installer session can be active at a time | ||
480 | { | ||
481 | using(Session session = Installer.OpenProduct(productCode)) | ||
482 | { | ||
483 | session.DoAction("CostInitialize"); | ||
484 | session.DoAction("FileCost"); | ||
485 | session.DoAction("CostFinalize"); | ||
486 | |||
487 | foreach(string componentCode in session.Database.ExecuteStringQuery("SELECT `ComponentId` FROM `Component`")) | ||
488 | { | ||
489 | foreach(object[] row in this.GetComponentRegistryRows(productCode, componentCode, session, true)) | ||
490 | { | ||
491 | table.Rows.Add(row); | ||
492 | } | ||
493 | } | ||
494 | } | ||
495 | } | ||
496 | return new DataView(table, "", "ProductRegistryPath ASC", DataViewRowState.CurrentRows); | ||
497 | } | ||
498 | catch(InstallerException) { } | ||
499 | return null; | ||
500 | } | ||
501 | |||
502 | public DataView GetComponentProductsData(string componentCode) | ||
503 | { | ||
504 | DataTable table = new DataTable("ComponentProducts"); | ||
505 | table.Locale = CultureInfo.InvariantCulture; | ||
506 | table.Columns.Add("ComponentProductsProductName", typeof(string)); | ||
507 | table.Columns.Add("ComponentProductsProductCode", typeof(string)); | ||
508 | table.Columns.Add("ComponentProductsComponentPath", typeof(string)); | ||
509 | |||
510 | if(this.componentProductsMap != null) | ||
511 | { | ||
512 | ArrayList componentProducts = (ArrayList) this.componentProductsMap[componentCode]; | ||
513 | foreach(string productCode in componentProducts) | ||
514 | { | ||
515 | string productName = MsiUtils.GetProductName(productCode); | ||
516 | string componentPath = new ComponentInstallation(componentCode, productCode).Path; | ||
517 | table.Rows.Add(new object[] { productName, productCode, componentPath }); | ||
518 | } | ||
519 | return new DataView(table, "", "ComponentProductsProductName ASC", DataViewRowState.CurrentRows); | ||
520 | } | ||
521 | return null; | ||
522 | } | ||
523 | |||
524 | public DataView GetProductComponentsData(string productCode) | ||
525 | { | ||
526 | DataTable table = new DataTable("ProductComponents"); | ||
527 | table.Locale = CultureInfo.InvariantCulture; | ||
528 | table.Columns.Add("ProductComponentsComponentName", typeof(string)); | ||
529 | table.Columns.Add("ProductComponentsComponentID", typeof(string)); | ||
530 | table.Columns.Add("ProductComponentsInstallState", typeof(string)); | ||
531 | |||
532 | try | ||
533 | { | ||
534 | IntPtr hWnd = IntPtr.Zero; | ||
535 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
536 | lock(syncRoot) // Only one Installer session can be active at a time | ||
537 | { | ||
538 | using(Session session = Installer.OpenProduct(productCode)) | ||
539 | { | ||
540 | session.DoAction("CostInitialize"); | ||
541 | session.DoAction("FileCost"); | ||
542 | session.DoAction("CostFinalize"); | ||
543 | |||
544 | IList<string> componentsAndIds = session.Database.ExecuteStringQuery( | ||
545 | "SELECT `Component`, `ComponentId` FROM `Component`"); | ||
546 | |||
547 | for (int i = 0; i < componentsAndIds.Count; i += 2) | ||
548 | { | ||
549 | if(componentsAndIds[i+1] == "Temporary Id") continue; | ||
550 | InstallState compState = session.Components[componentsAndIds[i]].CurrentState; | ||
551 | table.Rows.Add(new object[] { componentsAndIds[i], componentsAndIds[i+1], | ||
552 | (compState == InstallState.Advertised ? "Advertised" : compState.ToString())}); | ||
553 | } | ||
554 | } | ||
555 | } | ||
556 | return new DataView(table, "", "ProductComponentsComponentName ASC", DataViewRowState.CurrentRows); | ||
557 | } | ||
558 | catch(InstallerException) { } | ||
559 | return null; | ||
560 | } | ||
561 | |||
562 | public DataView GetFeatureComponentsData(string productCode, string feature) | ||
563 | { | ||
564 | DataTable table = new DataTable("ProductFeatureComponents"); | ||
565 | table.Locale = CultureInfo.InvariantCulture; | ||
566 | table.Columns.Add("ProductFeatureComponentsComponentName", typeof(string)); | ||
567 | table.Columns.Add("ProductFeatureComponentsComponentID", typeof(string)); | ||
568 | |||
569 | try | ||
570 | { | ||
571 | IntPtr hWnd = IntPtr.Zero; | ||
572 | Installer.SetInternalUI(InstallUIOptions.Silent, ref hWnd); | ||
573 | lock(syncRoot) // Only one Installer session can be active at a time | ||
574 | { | ||
575 | using(Session session = Installer.OpenProduct(productCode)) | ||
576 | { | ||
577 | IList<string> componentsAndIds = session.Database.ExecuteStringQuery( | ||
578 | "SELECT `FeatureComponents`.`Component_`, " + | ||
579 | "`Component`.`ComponentId` FROM `FeatureComponents`, `Component` " + | ||
580 | "WHERE `FeatureComponents`.`Component_` = `Component`.`Component` " + | ||
581 | "AND `FeatureComponents`.`Feature_` = '{0}'", feature); | ||
582 | for (int i = 0; i < componentsAndIds.Count; i += 2) | ||
583 | { | ||
584 | table.Rows.Add(new object[] { componentsAndIds[i], componentsAndIds[i+1] }); | ||
585 | } | ||
586 | } | ||
587 | } | ||
588 | return new DataView(table, "", "ProductFeatureComponentsComponentName ASC", DataViewRowState.CurrentRows); | ||
589 | } | ||
590 | catch(InstallerException) { } | ||
591 | return null; | ||
592 | } | ||
593 | |||
594 | public string GetLink(string nodePath, DataRow row) | ||
595 | { | ||
596 | string[] path = nodePath.Split('\\'); | ||
597 | |||
598 | if(path.Length == 3 && path[0] == "Products" && path[2] == "Components") | ||
599 | { | ||
600 | string component = (string) row["ProductComponentsComponentID"]; | ||
601 | return String.Format(@"Products\{0}\Components\{1}", path[1], component); | ||
602 | } | ||
603 | else if(path.Length == 4 && path[0] == "Products" && path[2] == "Features") | ||
604 | { | ||
605 | string component = (string) row["ProductFeatureComponentsComponentID"]; | ||
606 | return String.Format(@"Products\{0}\Components\{1}", path[1], component); | ||
607 | } | ||
608 | else if(path.Length == 3 && path[0] == "Products" && path[2] == "Files") | ||
609 | { | ||
610 | string component = (string) row["ProductFilesComponentID"]; | ||
611 | return String.Format(@"Products\{0}\Components\{1}", path[1], component); | ||
612 | } | ||
613 | else if(path.Length == 3 && path[0] == "Products" && path[2] == "Registry") | ||
614 | { | ||
615 | string component = (string) row["ProductRegistryComponentID"]; | ||
616 | return String.Format(@"Products\{0}\Components\{1}", path[1], component); | ||
617 | } | ||
618 | else if(path.Length == 5 && path[0] == "Products" && path[2] == "Components" && path[4] == "Sharing") | ||
619 | { | ||
620 | string product = (string) row["ComponentProductsProductCode"]; | ||
621 | return String.Format(@"Products\{0}\Components\{1}", MsiUtils.GetProductName(product), path[3]); | ||
622 | } | ||
623 | return null; | ||
624 | } | ||
625 | } | ||
626 | } | ||
diff --git a/src/tools/Dtf/Inventory/msiutils.cs b/src/tools/Dtf/Inventory/msiutils.cs new file mode 100644 index 00000000..189d28a9 --- /dev/null +++ b/src/tools/Dtf/Inventory/msiutils.cs | |||
@@ -0,0 +1,46 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.Collections; | ||
5 | using WixToolset.Dtf.WindowsInstaller; | ||
6 | |||
7 | |||
8 | namespace WixToolset.Dtf.Tools.Inventory | ||
9 | { | ||
10 | public class MsiUtils | ||
11 | { | ||
12 | private static Hashtable productCodesToNames = new Hashtable(); | ||
13 | private static Hashtable productNamesToCodes = new Hashtable(); | ||
14 | |||
15 | public static string GetProductName(string productCode) | ||
16 | { | ||
17 | string productName = (string) productCodesToNames[productCode]; | ||
18 | if(productName == null) | ||
19 | { | ||
20 | productName = new ProductInstallation(productCode).ProductName; | ||
21 | productName = productName.Replace('\\', ' '); | ||
22 | if(productNamesToCodes.Contains(productName)) | ||
23 | { | ||
24 | string modifiedProductName = null; | ||
25 | for(int i = 2; i < Int32.MaxValue; i++) | ||
26 | { | ||
27 | modifiedProductName = productName + " [" + i + "]"; | ||
28 | if(!productNamesToCodes.Contains(modifiedProductName)) break; | ||
29 | } | ||
30 | productName = modifiedProductName; | ||
31 | } | ||
32 | productCodesToNames[productCode] = productName; | ||
33 | productNamesToCodes[productName] = productCode; | ||
34 | } | ||
35 | return productName; | ||
36 | } | ||
37 | |||
38 | // Assumes GetProductName() has already been called for this product. | ||
39 | public static string GetProductCode(string productName) | ||
40 | { | ||
41 | return (string) productNamesToCodes[productName]; | ||
42 | } | ||
43 | |||
44 | private MsiUtils() { } | ||
45 | } | ||
46 | } | ||
diff --git a/src/tools/Dtf/Inventory/patches.cs b/src/tools/Dtf/Inventory/patches.cs new file mode 100644 index 00000000..ca96a97d --- /dev/null +++ b/src/tools/Dtf/Inventory/patches.cs | |||
@@ -0,0 +1,227 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.IO; | ||
5 | using System.Security; | ||
6 | using System.Data; | ||
7 | using System.Collections; | ||
8 | using System.Globalization; | ||
9 | using System.Windows.Forms; | ||
10 | using WixToolset.Dtf.WindowsInstaller; | ||
11 | |||
12 | namespace WixToolset.Dtf.Tools.Inventory | ||
13 | { | ||
14 | /// <summary> | ||
15 | /// Provides inventory data about patches installed on the system. | ||
16 | /// </summary> | ||
17 | public class PatchesInventory : IInventoryDataProvider | ||
18 | { | ||
19 | public PatchesInventory() | ||
20 | { | ||
21 | } | ||
22 | |||
23 | public string Description | ||
24 | { | ||
25 | get { return "Installed patches"; } | ||
26 | } | ||
27 | |||
28 | public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) | ||
29 | { | ||
30 | ArrayList nodes = new ArrayList(); | ||
31 | statusCallback(nodes.Count, @"Products\...\Patches"); | ||
32 | foreach (ProductInstallation product in ProductInstallation.AllProducts) | ||
33 | { | ||
34 | string productName = MsiUtils.GetProductName(product.ProductCode); | ||
35 | |||
36 | bool addedRoot = false; | ||
37 | foreach (PatchInstallation productPatch in PatchInstallation.GetPatches(null, product.ProductCode, null, UserContexts.All, PatchStates.Applied)) | ||
38 | { | ||
39 | if (!addedRoot) nodes.Add(String.Format(@"Products\{0}\Patches", productName)); | ||
40 | nodes.Add(String.Format(@"Products\{0}\Patches\{1}", productName, productPatch.PatchCode)); | ||
41 | } | ||
42 | } | ||
43 | |||
44 | statusCallback(nodes.Count, "Patches"); | ||
45 | |||
46 | string[] allPatches = GetAllPatchesList(); | ||
47 | if(allPatches.Length > 0) | ||
48 | { | ||
49 | nodes.Add("Patches"); | ||
50 | foreach(string patchCode in allPatches) | ||
51 | { | ||
52 | nodes.Add(String.Format(@"Patches\{0}", patchCode)); | ||
53 | nodes.Add(String.Format(@"Patches\{0}\Patched Products", patchCode)); | ||
54 | } | ||
55 | statusCallback(nodes.Count, String.Empty); | ||
56 | } | ||
57 | return (string[]) nodes.ToArray(typeof(string)); | ||
58 | } | ||
59 | |||
60 | public bool IsNodeSearchable(string searchRoot, string searchNode) | ||
61 | { | ||
62 | return true; | ||
63 | } | ||
64 | |||
65 | public DataView GetData(string nodePath) | ||
66 | { | ||
67 | string[] path = nodePath.Split('\\'); | ||
68 | |||
69 | if(path.Length == 3 && path[0] == "Products" && path[2] == "Patches") | ||
70 | { | ||
71 | return this.GetProductPatchData(path[1]); | ||
72 | } | ||
73 | else if(path.Length == 4 && path[0] == "Products" && path[2] == "Patches") | ||
74 | { | ||
75 | return this.GetPatchData(path[3]); | ||
76 | } | ||
77 | else if(path.Length == 1 && path[0] == "Patches") | ||
78 | { | ||
79 | return this.GetAllPatchesData(); | ||
80 | } | ||
81 | else if(path.Length == 2 && path[0] == "Patches") | ||
82 | { | ||
83 | return this.GetPatchData(path[1]); | ||
84 | } | ||
85 | else if(path.Length == 3 && path[0] == "Patches" && path[2] == "Patched Products") | ||
86 | { | ||
87 | return this.GetPatchTargetData(path[1]); | ||
88 | } | ||
89 | return null; | ||
90 | } | ||
91 | |||
92 | private string[] GetAllPatchesList() | ||
93 | { | ||
94 | ArrayList patchList = new ArrayList(); | ||
95 | foreach(PatchInstallation patch in PatchInstallation.AllPatches) | ||
96 | { | ||
97 | if(!patchList.Contains(patch.PatchCode)) | ||
98 | { | ||
99 | patchList.Add(patch.PatchCode); | ||
100 | } | ||
101 | } | ||
102 | string[] patchArray = (string[]) patchList.ToArray(typeof(string)); | ||
103 | Array.Sort(patchArray, 0, patchArray.Length, StringComparer.Ordinal); | ||
104 | return patchArray; | ||
105 | } | ||
106 | |||
107 | private DataView GetAllPatchesData() | ||
108 | { | ||
109 | DataTable table = new DataTable("Patches"); | ||
110 | table.Locale = CultureInfo.InvariantCulture; | ||
111 | table.Columns.Add("PatchesPatchCode", typeof(string)); | ||
112 | |||
113 | foreach(string patchCode in GetAllPatchesList()) | ||
114 | { | ||
115 | table.Rows.Add(new object[] { patchCode }); | ||
116 | } | ||
117 | return new DataView(table, "", "PatchesPatchCode ASC", DataViewRowState.CurrentRows); | ||
118 | } | ||
119 | |||
120 | private DataView GetProductPatchData(string productCode) | ||
121 | { | ||
122 | DataTable table = new DataTable("ProductPatches"); | ||
123 | table.Locale = CultureInfo.InvariantCulture; | ||
124 | table.Columns.Add("ProductPatchesPatchCode", typeof(string)); | ||
125 | |||
126 | foreach(PatchInstallation patch in PatchInstallation.GetPatches(null, productCode, null, UserContexts.All, PatchStates.Applied)) | ||
127 | { | ||
128 | table.Rows.Add(new object[] { patch.PatchCode }); | ||
129 | } | ||
130 | return new DataView(table, "", "ProductPatchesPatchCode ASC", DataViewRowState.CurrentRows); | ||
131 | } | ||
132 | |||
133 | private DataView GetPatchData(string patchCode) | ||
134 | { | ||
135 | DataTable table = new DataTable("PatchProperties"); | ||
136 | table.Locale = CultureInfo.InvariantCulture; | ||
137 | table.Columns.Add("PatchPropertiesProperty", typeof(string)); | ||
138 | table.Columns.Add("PatchPropertiesValue", typeof(string)); | ||
139 | |||
140 | table.Rows.Add(new object[] { "PatchCode", patchCode }); | ||
141 | |||
142 | PatchInstallation patch = new PatchInstallation(patchCode, null); | ||
143 | |||
144 | string localPackage = null; | ||
145 | foreach(string property in new string[] | ||
146 | { | ||
147 | "InstallDate", | ||
148 | "LocalPackage", | ||
149 | "State", | ||
150 | "Transforms", | ||
151 | "Uninstallable", | ||
152 | }) | ||
153 | { | ||
154 | try | ||
155 | { | ||
156 | string value = patch[property]; | ||
157 | table.Rows.Add(new object[] { property, (value != null ? value : "") }); | ||
158 | if(property == "LocalPackage") localPackage = value; | ||
159 | } | ||
160 | catch(InstallerException iex) | ||
161 | { | ||
162 | table.Rows.Add(new object[] { property, iex.Message }); | ||
163 | } | ||
164 | catch(ArgumentException) { } | ||
165 | } | ||
166 | |||
167 | if(localPackage != null) | ||
168 | { | ||
169 | try | ||
170 | { | ||
171 | using(SummaryInfo patchSummaryInfo = new SummaryInfo(localPackage, false)) | ||
172 | { | ||
173 | table.Rows.Add(new object[] { "Title", patchSummaryInfo.Title }); | ||
174 | table.Rows.Add(new object[] { "Subject", patchSummaryInfo.Subject }); | ||
175 | table.Rows.Add(new object[] { "Author", patchSummaryInfo.Author }); | ||
176 | table.Rows.Add(new object[] { "Comments", patchSummaryInfo.Comments }); | ||
177 | table.Rows.Add(new object[] { "TargetProductCodes", patchSummaryInfo.Template }); | ||
178 | string obsoletedPatchCodes = patchSummaryInfo.RevisionNumber.Substring(patchSummaryInfo.RevisionNumber.IndexOf('}') + 1); | ||
179 | table.Rows.Add(new object[] { "ObsoletedPatchCodes", obsoletedPatchCodes }); | ||
180 | table.Rows.Add(new object[] { "TransformNames", patchSummaryInfo.LastSavedBy }); | ||
181 | } | ||
182 | } | ||
183 | catch(InstallerException) { } | ||
184 | catch(IOException) { } | ||
185 | catch(SecurityException) { } | ||
186 | } | ||
187 | return new DataView(table, "", "PatchPropertiesProperty ASC", DataViewRowState.CurrentRows); | ||
188 | } | ||
189 | |||
190 | private DataView GetPatchTargetData(string patchCode) | ||
191 | { | ||
192 | DataTable table = new DataTable("PatchTargets"); | ||
193 | table.Locale = CultureInfo.InvariantCulture; | ||
194 | table.Columns.Add("PatchTargetsProductName", typeof(string)); | ||
195 | table.Columns.Add("PatchTargetsProductCode", typeof(string)); | ||
196 | |||
197 | foreach (PatchInstallation patch in PatchInstallation.GetPatches(patchCode, null, null, UserContexts.All, PatchStates.Applied)) | ||
198 | { | ||
199 | if(patch.PatchCode == patchCode) | ||
200 | { | ||
201 | string productName = MsiUtils.GetProductName(patch.ProductCode); | ||
202 | table.Rows.Add(new object[] { productName, patch.ProductCode }); | ||
203 | } | ||
204 | } | ||
205 | return new DataView(table, "", "PatchTargetsProductName ASC", DataViewRowState.CurrentRows); | ||
206 | } | ||
207 | |||
208 | public string GetLink(string nodePath, DataRow row) | ||
209 | { | ||
210 | string[] path = nodePath.Split('\\'); | ||
211 | |||
212 | if(path.Length == 3 && path[0] == "Products" && path[2] == "Patches") | ||
213 | { | ||
214 | return String.Format(@"Patches\{0}", row["ProductPatchesPatchCode"]); | ||
215 | } | ||
216 | else if(path.Length == 1 && path[0] == "Patches") | ||
217 | { | ||
218 | return String.Format(@"Patches\{0}", row["PatchesPatchCode"]); | ||
219 | } | ||
220 | else if(path.Length == 3 && path[0] == "Patches" && path[2] == "Patched Products") | ||
221 | { | ||
222 | return String.Format(@"Products\{0}", MsiUtils.GetProductCode((string) row["PatchTargetsProductCode"])); | ||
223 | } | ||
224 | return null; | ||
225 | } | ||
226 | } | ||
227 | } | ||
diff --git a/src/tools/Dtf/Inventory/products.cs b/src/tools/Dtf/Inventory/products.cs new file mode 100644 index 00000000..635e5439 --- /dev/null +++ b/src/tools/Dtf/Inventory/products.cs | |||
@@ -0,0 +1,145 @@ | |||
1 | // 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. | ||
2 | |||
3 | using System; | ||
4 | using System.Data; | ||
5 | using System.Globalization; | ||
6 | using System.Collections; | ||
7 | using System.Windows.Forms; | ||
8 | using WixToolset.Dtf.WindowsInstaller; | ||
9 | |||
10 | namespace WixToolset.Dtf.Tools.Inventory | ||
11 | { | ||
12 | /// <summary> | ||
13 | /// Provides inventory data about products installed or advertised on the system. | ||
14 | /// </summary> | ||
15 | public class ProductsInventory : IInventoryDataProvider | ||
16 | { | ||
17 | public ProductsInventory() | ||
18 | { | ||
19 | } | ||
20 | |||
21 | public string Description | ||
22 | { | ||
23 | get { return "Installed products"; } | ||
24 | } | ||
25 | |||
26 | public string[] GetNodes(InventoryDataLoadStatusCallback statusCallback) | ||
27 | { | ||
28 | statusCallback(0, "Products"); | ||
29 | ArrayList nodes = new ArrayList(); | ||
30 | nodes.Add("Products"); | ||
31 | foreach(ProductInstallation product in ProductInstallation.AllProducts) | ||
32 | { | ||
33 | nodes.Add("Products\\" + MsiUtils.GetProductName(product.ProductCode)); | ||
34 | } | ||
35 | statusCallback(nodes.Count, String.Empty); | ||
36 | return (string[]) nodes.ToArray(typeof(string)); | ||
37 | } | ||
38 | |||
39 | public bool IsNodeSearchable(string searchRoot, string searchNode) | ||
40 | { | ||
41 | return true; | ||
42 | } | ||
43 | |||
44 | public DataView GetData(string nodePath) | ||
45 | { | ||
46 | string[] path = nodePath.Split('\\'); | ||
47 | |||
48 | if(path.Length == 1 && path[0] == "Products") | ||
49 | { | ||
50 | return this.GetAllProductsData(); | ||
51 | } | ||
52 | else if(path.Length == 2 && path[0] == "Products") | ||
53 | { | ||
54 | return this.GetProductData(MsiUtils.GetProductCode(path[1])); | ||
55 | } | ||
56 | return null; | ||
57 | } | ||
58 | |||
59 | private DataView GetAllProductsData() | ||
60 | { | ||
61 | DataTable table = new DataTable("Products"); | ||
62 | table.Locale = CultureInfo.InvariantCulture; | ||
63 | table.Columns.Add("ProductsProductName", typeof(string)); | ||
64 | table.Columns.Add("ProductsProductCode", typeof(string)); | ||
65 | |||
66 | foreach (ProductInstallation product in ProductInstallation.AllProducts) | ||
67 | { | ||
68 | string productName = MsiUtils.GetProductName(product.ProductCode); | ||
69 | table.Rows.Add(new object[] { productName, product.ProductCode }); | ||
70 | } | ||
71 | return new DataView(table, "", "ProductsProductName ASC", DataViewRowState.CurrentRows); | ||
72 | } | ||
73 | |||
74 | private DataView GetProductData(string productCode) | ||
75 | { | ||
76 | DataTable table = new DataTable("ProductProperties"); | ||
77 | table.Locale = CultureInfo.InvariantCulture; | ||
78 | table.Columns.Add("ProductPropertiesProperty", typeof(string)); | ||
79 | table.Columns.Add("ProductPropertiesValue", typeof(string)); | ||
80 | |||
81 | // Add a fake "ProductCode" install property, just for display convenience. | ||
82 | table.Rows.Add(new object[] { "ProductCode", productCode }); | ||
83 | |||
84 | ProductInstallation product = new ProductInstallation(productCode); | ||
85 | |||
86 | foreach(string property in new string[] | ||
87 | { | ||
88 | "AssignmentType", | ||
89 | "DiskPrompt", | ||
90 | "HelpLink", | ||
91 | "HelpTelephone", | ||
92 | "InstalledProductName", | ||
93 | "InstallDate", | ||
94 | "InstallLocation", | ||
95 | "InstallSource", | ||
96 | "Language", | ||
97 | "LastUsedSource", | ||
98 | "LastUsedType", | ||
99 | "LocalPackage", | ||
100 | "MediaPackagePath", | ||
101 | "PackageCode", | ||
102 | "PackageName", | ||
103 | "ProductIcon", | ||
104 | "ProductID", | ||
105 | "ProductName", | ||
106 | "Publisher", | ||
107 | "RegCompany", | ||
108 | "RegOwner", | ||
109 | "State", | ||
110 | "transforms", | ||
111 | "Uninstallable", | ||
112 | "UrlInfoAbout", | ||
113 | "UrlUpdateInfo", | ||
114 | "Version", | ||
115 | "VersionMinor", | ||
116 | "VersionMajor", | ||
117 | "VersionString" | ||
118 | }) | ||
119 | { | ||
120 | try | ||
121 | { | ||
122 | string value = product[property]; | ||
123 | table.Rows.Add(new object[] { property, (value != null ? value : "") }); | ||
124 | } | ||
125 | catch(InstallerException iex) | ||
126 | { | ||
127 | table.Rows.Add(new object[] { property, iex.Message }); | ||
128 | } | ||
129 | catch(ArgumentException) { } | ||
130 | } | ||
131 | return new DataView(table, "", "ProductPropertiesProperty ASC", DataViewRowState.CurrentRows); | ||
132 | } | ||
133 | |||
134 | public string GetLink(string nodePath, DataRow row) | ||
135 | { | ||
136 | string[] path = nodePath.Split('\\'); | ||
137 | |||
138 | if(path.Length == 1 && path[0] == "Products") | ||
139 | { | ||
140 | return String.Format(@"Products\{0}", MsiUtils.GetProductName((string) row["ProductsProductCode"])); | ||
141 | } | ||
142 | return null; | ||
143 | } | ||
144 | } | ||
145 | } | ||
diff --git a/src/tools/Dtf/Inventory/xp.manifest b/src/tools/Dtf/Inventory/xp.manifest new file mode 100644 index 00000000..34d61fea --- /dev/null +++ b/src/tools/Dtf/Inventory/xp.manifest | |||
@@ -0,0 +1,15 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> | ||
2 | <!-- 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. --> | ||
3 | |||
4 | |||
5 | <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> | ||
6 | <dependency> | ||
7 | <dependentAssembly> | ||
8 | <assemblyIdentity type="win32" | ||
9 | name="Microsoft.Windows.Common-Controls" | ||
10 | version="6.0.0.0" language="*" | ||
11 | processorArchitecture="X86" | ||
12 | publicKeyToken="6595b64144ccf1df" /> | ||
13 | </dependentAssembly> | ||
14 | </dependency> | ||
15 | </assembly> | ||