aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/Validator.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/Validator.cs')
-rw-r--r--src/WixToolset.Core/Validator.cs401
1 files changed, 401 insertions, 0 deletions
diff --git a/src/WixToolset.Core/Validator.cs b/src/WixToolset.Core/Validator.cs
new file mode 100644
index 00000000..6420b9b7
--- /dev/null
+++ b/src/WixToolset.Core/Validator.cs
@@ -0,0 +1,401 @@
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
3namespace WixToolset
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Specialized;
8 using System.ComponentModel;
9 using System.Diagnostics.CodeAnalysis;
10 using System.Globalization;
11 using System.IO;
12 using System.Threading;
13 using WixToolset.Data;
14 using WixToolset.Extensibility;
15 using WixToolset.Msi;
16 using WixToolset.Core.Native;
17
18 /// <summary>
19 /// Runs internal consistency evaluators (ICEs) from cub files against a database.
20 /// </summary>
21 public sealed class Validator : IMessageHandler
22 {
23 private string actionName;
24 private StringCollection cubeFiles;
25 private ValidatorExtension extension;
26 private string[] ices;
27 private Output output;
28 private string[] suppressedICEs;
29 private InstallUIHandler validationUIHandler;
30 private bool validationSessionComplete;
31
32 /// <summary>
33 /// Instantiate a new Validator.
34 /// </summary>
35 public Validator()
36 {
37 this.cubeFiles = new StringCollection();
38 this.extension = new ValidatorExtension();
39 this.validationUIHandler = new InstallUIHandler(this.ValidationUIHandler);
40 }
41
42 /// <summary>
43 /// Gets or sets a <see cref="ValidatorExtension"/> that directs messages from the validator.
44 /// </summary>
45 /// <value>A <see cref="ValidatorExtension"/> that directs messages from the validator.</value>
46 public ValidatorExtension Extension
47 {
48 get { return this.extension; }
49 set { this.extension = value; }
50 }
51
52 /// <summary>
53 /// Gets or sets the list of ICEs to run.
54 /// </summary>
55 /// <value>The list of ICEs.</value>
56 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
57 public string[] ICEs
58 {
59 get { return this.ices; }
60 set { this.ices = value; }
61 }
62
63 /// <summary>
64 /// Gets or sets the output used for finding source line information.
65 /// </summary>
66 /// <value>The output used for finding source line information.</value>
67 public Output Output
68 {
69 // cache Output object until validation for changes in extension
70 get { return this.output; }
71 set { this.output = value; }
72 }
73
74 /// <summary>
75 /// Gets or sets the suppressed ICEs.
76 /// </summary>
77 /// <value>The suppressed ICEs.</value>
78 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
79 public string[] SuppressedICEs
80 {
81 get { return this.suppressedICEs; }
82 set { this.suppressedICEs = value; }
83 }
84
85 /// <summary>
86 /// Sets the temporary path for the Binder.
87 /// </summary>
88 public string TempFilesLocation { private get; set; }
89
90 /// <summary>
91 /// Add a cube file to the validation run.
92 /// </summary>
93 /// <param name="cubeFile">A cube file.</param>
94 public void AddCubeFile(string cubeFile)
95 {
96 this.cubeFiles.Add(cubeFile);
97 }
98
99 /// <summary>
100 /// Validate a database.
101 /// </summary>
102 /// <param name="databaseFile">The database to validate.</param>
103 /// <returns>true if validation succeeded; false otherwise.</returns>
104 public void Validate(string databaseFile)
105 {
106 Dictionary<string, string> indexedICEs = new Dictionary<string, string>();
107 Dictionary<string, string> indexedSuppressedICEs = new Dictionary<string, string>();
108 int previousUILevel = (int)InstallUILevels.Basic;
109 IntPtr previousHwnd = IntPtr.Zero;
110 InstallUIHandler previousUIHandler = null;
111
112 if (null == databaseFile)
113 {
114 throw new ArgumentNullException("databaseFile");
115 }
116
117 // initialize the validator extension
118 this.extension.DatabaseFile = databaseFile;
119 this.extension.Output = this.output;
120 this.extension.InitializeValidator();
121
122 // Ensure the temporary files can be created.
123 Directory.CreateDirectory(this.TempFilesLocation);
124
125 // index the ICEs
126 if (null != this.ices)
127 {
128 foreach (string ice in this.ices)
129 {
130 indexedICEs[ice] = null;
131 }
132 }
133
134 // index the suppressed ICEs
135 if (null != this.suppressedICEs)
136 {
137 foreach (string suppressedICE in this.suppressedICEs)
138 {
139 indexedSuppressedICEs[suppressedICE] = null;
140 }
141 }
142
143 // copy the database to a temporary location so it can be manipulated
144 string tempDatabaseFile = Path.Combine(this.TempFilesLocation, Path.GetFileName(databaseFile));
145 File.Copy(databaseFile, tempDatabaseFile);
146
147 // remove the read-only property from the temporary database
148 FileAttributes attributes = File.GetAttributes(tempDatabaseFile);
149 File.SetAttributes(tempDatabaseFile, attributes & ~FileAttributes.ReadOnly);
150
151 Mutex mutex = new Mutex(false, "WixValidator");
152 try
153 {
154 if (!mutex.WaitOne(0, false))
155 {
156 this.OnMessage(WixVerboses.ValidationSerialized());
157 mutex.WaitOne();
158 }
159
160 using (Database database = new Database(tempDatabaseFile, OpenDatabase.Direct))
161 {
162 bool propertyTableExists = database.TableExists("Property");
163 string productCode = null;
164
165 // remove the product code from the database before opening a session to prevent opening an installed product
166 if (propertyTableExists)
167 {
168 using (View view = database.OpenExecuteView("SELECT `Value` FROM `Property` WHERE Property = 'ProductCode'"))
169 {
170 using (Record record = view.Fetch())
171 {
172 if (null != record)
173 {
174 productCode = record.GetString(1);
175
176 using (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
177 {
178 }
179 }
180 }
181 }
182 }
183
184 // merge in the cube databases
185 foreach (string cubeFile in this.cubeFiles)
186 {
187 try
188 {
189 using (Database cubeDatabase = new Database(cubeFile, OpenDatabase.ReadOnly))
190 {
191 try
192 {
193 database.Merge(cubeDatabase, "MergeConflicts");
194 }
195 catch
196 {
197 // ignore merge errors since they are expected in the _Validation table
198 }
199 }
200 }
201 catch (Win32Exception e)
202 {
203 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
204 {
205 throw new WixException(WixErrors.CubeFileNotFound(cubeFile));
206 }
207
208 throw;
209 }
210 }
211
212 // commit the database before proceeding to ensure the streams don't get confused
213 database.Commit();
214
215 // the property table may have been added to the database
216 // from a cub database without the proper validation rows
217 if (!propertyTableExists)
218 {
219 using (View view = database.OpenExecuteView("DROP table `Property`"))
220 {
221 }
222 }
223
224 // get all the action names for ICEs which have not been suppressed
225 List<string> actions = new List<string>();
226 using (View view = database.OpenExecuteView("SELECT `Action` FROM `_ICESequence` ORDER BY `Sequence`"))
227 {
228 while (true)
229 {
230 using (Record record = view.Fetch())
231 {
232 if (null == record)
233 {
234 break;
235 }
236
237 string action = record.GetString(1);
238
239 if (!indexedSuppressedICEs.ContainsKey(action))
240 {
241 actions.Add(action);
242 }
243 }
244 }
245 }
246
247 if (0 != indexedICEs.Count)
248 {
249 // Walk backwards and remove those that arent in the list
250 for (int i = actions.Count - 1; 0 <= i; i--)
251 {
252 if (!indexedICEs.ContainsKey(actions[i]))
253 {
254 actions.RemoveAt(i);
255 }
256 }
257 }
258
259 // disable the internal UI handler and set an external UI handler
260 previousUILevel = Installer.SetInternalUI((int)InstallUILevels.None, ref previousHwnd);
261 previousUIHandler = Installer.SetExternalUI(this.validationUIHandler, (int)InstallLogModes.Error | (int)InstallLogModes.Warning | (int)InstallLogModes.User, IntPtr.Zero);
262
263 // create a session for running the ICEs
264 this.validationSessionComplete = false;
265 using (Session session = new Session(database))
266 {
267 // add the product code back into the database
268 if (null != productCode)
269 {
270 // some CUBs erroneously have a ProductCode property, so delete it if we just picked one up
271 using (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
272 {
273 }
274
275 using (View view = database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "INSERT INTO `Property` (`Property`, `Value`) VALUES ('ProductCode', '{0}')", productCode)))
276 {
277 }
278 }
279
280 foreach (string action in actions)
281 {
282 this.actionName = action;
283 try
284 {
285 session.DoAction(action);
286 }
287 catch (Win32Exception e)
288 {
289 if (!Messaging.Instance.EncounteredError)
290 {
291 throw e;
292 }
293 // TODO: Review why this was clearing the error state when an exception had happened but an error was already encountered. That's weird.
294 //else
295 //{
296 // this.encounteredError = false;
297 //}
298 }
299 this.actionName = null;
300 }
301
302 // Mark the validation session complete so we ignore any messages that MSI may fire
303 // during session clean-up.
304 this.validationSessionComplete = true;
305 }
306 }
307 }
308 catch (Win32Exception e)
309 {
310 // avoid displaying errors twice since one may have already occurred in the UI handler
311 if (!Messaging.Instance.EncounteredError)
312 {
313 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
314 {
315 // databaseFile is not passed since during light
316 // this would be the temporary copy and there would be
317 // no final output since the error occured; during smoke
318 // they should know the path passed into smoke
319 this.OnMessage(WixErrors.ValidationFailedToOpenDatabase());
320 }
321 else if (0x64D == e.NativeErrorCode)
322 {
323 this.OnMessage(WixErrors.ValidationFailedDueToLowMsiEngine());
324 }
325 else if (0x654 == e.NativeErrorCode)
326 {
327 this.OnMessage(WixErrors.ValidationFailedDueToInvalidPackage());
328 }
329 else if (0x658 == e.NativeErrorCode)
330 {
331 this.OnMessage(WixErrors.ValidationFailedDueToMultilanguageMergeModule());
332 }
333 else if (0x659 == e.NativeErrorCode)
334 {
335 this.OnMessage(WixWarnings.ValidationFailedDueToSystemPolicy());
336 }
337 else
338 {
339 string msgTemp = e.Message;
340
341 if (null != this.actionName)
342 {
343 msgTemp = String.Concat("Action - '", this.actionName, "' ", e.Message);
344 }
345
346 this.OnMessage(WixErrors.Win32Exception(e.NativeErrorCode, msgTemp));
347 }
348 }
349 }
350 finally
351 {
352 Installer.SetExternalUI(previousUIHandler, 0, IntPtr.Zero);
353 Installer.SetInternalUI(previousUILevel, ref previousHwnd);
354
355 this.validationSessionComplete = false; // no validation session at this point, so reset the completion flag.
356
357 mutex.ReleaseMutex();
358 this.cubeFiles.Clear();
359 this.extension.FinalizeValidator();
360 }
361 }
362
363 /// <summary>
364 /// Sends a message to the message delegate if there is one.
365 /// </summary>
366 /// <param name="mea">Message event arguments.</param>
367 public void OnMessage(MessageEventArgs e)
368 {
369 Messaging.Instance.OnMessage(e);
370 this.extension.OnMessage(e);
371 }
372
373 /// <summary>
374 /// The validation external UI handler.
375 /// </summary>
376 /// <param name="context">Pointer to an application context.
377 /// This parameter can be used for error checking.</param>
378 /// <param name="messageType">Specifies a combination of one message box style,
379 /// one message box icon type, one default button, and one installation message type.</param>
380 /// <param name="message">Specifies the message text.</param>
381 /// <returns>-1 for an error, 0 if no action was taken, 1 if OK, 3 to abort.</returns>
382 private int ValidationUIHandler(IntPtr context, uint messageType, string message)
383 {
384 try
385 {
386 // If we're getting messges during the validation session, send them to
387 // the extension. Otherwise, ignore the messages.
388 if (!this.validationSessionComplete)
389 {
390 this.extension.Log(message, this.actionName);
391 }
392 }
393 catch (WixException ex)
394 {
395 this.OnMessage(ex.Error);
396 }
397
398 return 1;
399 }
400 }
401}