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