| 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 | namespace WixToolset.Dtf.WindowsInstaller |
| 4 | { |
| 5 | using System; |
| 6 | using System.IO; |
| 7 | using System.Text; |
| 8 | using System.Security; |
| 9 | using System.Reflection; |
| 10 | using System.Collections; |
| 11 | using System.Configuration; |
| 12 | using System.Runtime.InteropServices; |
| 13 | using System.Diagnostics.CodeAnalysis; |
| 14 | |
| 15 | /// <summary> |
| 16 | /// Managed-code portion of the custom action proxy. |
| 17 | /// </summary> |
| 18 | internal static class CustomActionProxy |
| 19 | { |
| 20 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 21 | public static int InvokeCustomAction32(int sessionHandle, string entryPoint, |
| 22 | int remotingDelegatePtr) |
| 23 | { |
| 24 | return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr)); |
| 25 | } |
| 26 | |
| 27 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 28 | public static int InvokeCustomAction64(int sessionHandle, string entryPoint, |
| 29 | long remotingDelegatePtr) |
| 30 | { |
| 31 | return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr)); |
| 32 | } |
| 33 | |
| 34 | /// <summary> |
| 35 | /// Invokes a managed custom action method. |
| 36 | /// </summary> |
| 37 | /// <param name="sessionHandle">Integer handle to the installer session.</param> |
| 38 | /// <param name="entryPoint">Name of the custom action entrypoint. This must |
| 39 | /// either map to an entrypoint definition in the <c>customActions</c> |
| 40 | /// config section, or be an explicit entrypoint of the form: |
| 41 | /// "AssemblyName!Namespace.Class.Method"</param> |
| 42 | /// <param name="remotingDelegatePtr">Pointer to a delegate used to |
| 43 | /// make remote API calls, if this custom action is running out-of-proc.</param> |
| 44 | /// <returns>The value returned by the custom action method, |
| 45 | /// or ERROR_INSTALL_FAILURE if the custom action could not be invoked.</returns> |
| 46 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] |
| 47 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 48 | public static int InvokeCustomAction(int sessionHandle, string entryPoint, |
| 49 | IntPtr remotingDelegatePtr) |
| 50 | { |
| 51 | Session session = null; |
| 52 | string assemblyName, className, methodName; |
| 53 | MethodInfo method; |
| 54 | |
| 55 | try |
| 56 | { |
| 57 | MsiRemoteInvoke remotingDelegate = (MsiRemoteInvoke) |
| 58 | Marshal.GetDelegateForFunctionPointer( |
| 59 | remotingDelegatePtr, typeof(MsiRemoteInvoke)); |
| 60 | RemotableNativeMethods.RemotingDelegate = remotingDelegate; |
| 61 | |
| 62 | sessionHandle = RemotableNativeMethods.MakeRemoteHandle(sessionHandle); |
| 63 | session = new Session((IntPtr) sessionHandle, false); |
| 64 | if (String.IsNullOrEmpty(entryPoint)) |
| 65 | { |
| 66 | throw new ArgumentNullException("entryPoint"); |
| 67 | } |
| 68 | |
| 69 | if (!CustomActionProxy.FindEntryPoint( |
| 70 | session, |
| 71 | entryPoint, |
| 72 | out assemblyName, |
| 73 | out className, |
| 74 | out methodName)) |
| 75 | { |
| 76 | return (int) ActionResult.Failure; |
| 77 | } |
| 78 | session.Log("Calling custom action {0}!{1}.{2}", assemblyName, className, methodName); |
| 79 | |
| 80 | method = CustomActionProxy.GetCustomActionMethod( |
| 81 | session, |
| 82 | assemblyName, |
| 83 | className, |
| 84 | methodName); |
| 85 | if (method == null) |
| 86 | { |
| 87 | return (int) ActionResult.Failure; |
| 88 | } |
| 89 | } |
| 90 | catch (Exception ex) |
| 91 | { |
| 92 | if (session != null) |
| 93 | { |
| 94 | try |
| 95 | { |
| 96 | session.Log("Exception while loading custom action:"); |
| 97 | session.Log(ex.ToString()); |
| 98 | } |
| 99 | catch (Exception) { } |
| 100 | } |
| 101 | return (int) ActionResult.Failure; |
| 102 | } |
| 103 | |
| 104 | string originalDirectory = null; |
| 105 | |
| 106 | try |
| 107 | { |
| 108 | // Remember the original directory so we can restore it later. |
| 109 | originalDirectory = Environment.CurrentDirectory; |
| 110 | |
| 111 | // Set the current directory to the location of the extracted files. |
| 112 | Environment.CurrentDirectory = |
| 113 | AppDomain.CurrentDomain.BaseDirectory; |
| 114 | |
| 115 | object[] args = new object[] { session }; |
| 116 | if (DebugBreakEnabled(new string[] { entryPoint, methodName })) |
| 117 | { |
| 118 | string message = String.Format( |
| 119 | "To debug your custom action, attach to process ID {0} (0x{0:x}) and click OK; otherwise, click Cancel to fail the custom action.", |
| 120 | System.Diagnostics.Process.GetCurrentProcess().Id |
| 121 | ); |
| 122 | |
| 123 | MessageResult button = NativeMethods.MessageBox( |
| 124 | IntPtr.Zero, |
| 125 | message, |
| 126 | "Custom Action Breakpoint", |
| 127 | (int)MessageButtons.OKCancel | (int)MessageIcon.Asterisk | (int)(MessageBoxStyles.TopMost | MessageBoxStyles.ServiceNotification) |
| 128 | ); |
| 129 | |
| 130 | if (MessageResult.Cancel == button) |
| 131 | { |
| 132 | return (int)ActionResult.UserExit; |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | ActionResult result = (ActionResult) method.Invoke(null, args); |
| 137 | session.Close(); |
| 138 | return (int) result; |
| 139 | } |
| 140 | catch (InstallCanceledException) |
| 141 | { |
| 142 | return (int) ActionResult.UserExit; |
| 143 | } |
| 144 | catch (Exception ex) |
| 145 | { |
| 146 | session.Log("Exception thrown by custom action:"); |
| 147 | session.Log(ex.ToString()); |
| 148 | return (int) ActionResult.Failure; |
| 149 | } |
| 150 | finally |
| 151 | { |
| 152 | try |
| 153 | { |
| 154 | if (!String.IsNullOrEmpty(originalDirectory)) |
| 155 | { |
| 156 | Environment.CurrentDirectory = originalDirectory; |
| 157 | } |
| 158 | } |
| 159 | catch (Exception ex) |
| 160 | { |
| 161 | session.Log("Failed to restore current directory after running custom action: {0}", ex.Message); |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | /// <summary> |
| 167 | /// Checks the "MMsiBreak" environment variable for any matching custom action names. |
| 168 | /// </summary> |
| 169 | /// <param name="names">List of names to search for in the environment |
| 170 | /// variable string.</param> |
| 171 | /// <returns>True if a match was found, else false.</returns> |
| 172 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 173 | internal static bool DebugBreakEnabled(string[] names) |
| 174 | { |
| 175 | string mmsibreak = Environment.GetEnvironmentVariable("MMsiBreak"); |
| 176 | if (mmsibreak != null) |
| 177 | { |
| 178 | foreach (string breakName in mmsibreak.Split(',', ';')) |
| 179 | { |
| 180 | foreach (string name in names) |
| 181 | { |
| 182 | if (breakName == name) |
| 183 | { |
| 184 | return true; |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | } |
| 189 | return false; |
| 190 | } |
| 191 | |
| 192 | /// <summary> |
| 193 | /// Locates and parses an entrypoint mapping in CustomAction.config. |
| 194 | /// </summary> |
| 195 | /// <param name="session">Installer session handle, just used for logging.</param> |
| 196 | /// <param name="entryPoint">Custom action entrypoint name: the key value |
| 197 | /// in an item in the <c>customActions</c> section of the config file.</param> |
| 198 | /// <param name="assemblyName">Returned display name of the assembly from |
| 199 | /// the entrypoint mapping.</param> |
| 200 | /// <param name="className">Returned class name of the entrypoint mapping.</param> |
| 201 | /// <param name="methodName">Returned method name of the entrypoint mapping.</param> |
| 202 | /// <returns>True if the entrypoint was found, false if not or if some error |
| 203 | /// occurred.</returns> |
| 204 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 205 | private static bool FindEntryPoint( |
| 206 | Session session, |
| 207 | string entryPoint, |
| 208 | out string assemblyName, |
| 209 | out string className, |
| 210 | out string methodName) |
| 211 | { |
| 212 | assemblyName = null; |
| 213 | className = null; |
| 214 | methodName = null; |
| 215 | |
| 216 | string fullEntryPoint; |
| 217 | if (entryPoint.IndexOf('!') > 0) |
| 218 | { |
| 219 | fullEntryPoint = entryPoint; |
| 220 | } |
| 221 | else |
| 222 | { |
| 223 | #if NETFRAMEWORK |
| 224 | IDictionary config; |
| 225 | try |
| 226 | { |
| 227 | config = (IDictionary) ConfigurationManager.GetSection("customActions"); |
| 228 | } |
| 229 | catch (ConfigurationException cex) |
| 230 | { |
| 231 | session.Log("Error: missing or invalid customActions config section."); |
| 232 | session.Log(cex.ToString()); |
| 233 | return false; |
| 234 | } |
| 235 | fullEntryPoint = (string) config[entryPoint]; |
| 236 | if (fullEntryPoint == null) |
| 237 | { |
| 238 | session.Log( |
| 239 | "Error: custom action entry point '{0}' not found " + |
| 240 | "in customActions config section.", |
| 241 | entryPoint); |
| 242 | return false; |
| 243 | } |
| 244 | #else |
| 245 | throw new NotImplementedException(); |
| 246 | #endif |
| 247 | } |
| 248 | |
| 249 | int assemblySplit = fullEntryPoint.IndexOf('!'); |
| 250 | int methodSplit = fullEntryPoint.LastIndexOf('.'); |
| 251 | if (assemblySplit < 0 || methodSplit < 0 || methodSplit < assemblySplit) |
| 252 | { |
| 253 | session.Log("Error: invalid custom action entry point:" + entryPoint); |
| 254 | return false; |
| 255 | } |
| 256 | |
| 257 | assemblyName = fullEntryPoint.Substring(0, assemblySplit); |
| 258 | className = fullEntryPoint.Substring(assemblySplit + 1, methodSplit - assemblySplit - 1); |
| 259 | methodName = fullEntryPoint.Substring(methodSplit + 1); |
| 260 | return true; |
| 261 | } |
| 262 | |
| 263 | /// <summary> |
| 264 | /// Uses reflection to load the assembly and class and find the method. |
| 265 | /// </summary> |
| 266 | /// <param name="session">Installer session handle, just used for logging.</param> |
| 267 | /// <param name="assemblyName">Display name of the assembly containing the |
| 268 | /// custom action method.</param> |
| 269 | /// <param name="className">Fully-qualified name of the class containing the |
| 270 | /// custom action method.</param> |
| 271 | /// <param name="methodName">Name of the custom action method.</param> |
| 272 | /// <returns>The method, or null if not found.</returns> |
| 273 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 274 | private static MethodInfo GetCustomActionMethod( |
| 275 | Session session, |
| 276 | string assemblyName, |
| 277 | string className, |
| 278 | string methodName) |
| 279 | { |
| 280 | Assembly customActionAssembly; |
| 281 | Type customActionClass = null; |
| 282 | Exception caughtEx = null; |
| 283 | try |
| 284 | { |
| 285 | customActionAssembly = AppDomain.CurrentDomain.Load(assemblyName); |
| 286 | customActionClass = customActionAssembly.GetType(className, true, true); |
| 287 | } |
| 288 | catch (IOException ex) { caughtEx = ex; } |
| 289 | catch (BadImageFormatException ex) { caughtEx = ex; } |
| 290 | catch (TypeLoadException ex) { caughtEx = ex; } |
| 291 | catch (ReflectionTypeLoadException ex) { caughtEx = ex; } |
| 292 | catch (SecurityException ex) { caughtEx = ex; } |
| 293 | if (caughtEx != null) |
| 294 | { |
| 295 | session.Log("Error: could not load custom action class " + className + " from assembly: " + assemblyName); |
| 296 | session.Log(caughtEx.ToString()); |
| 297 | return null; |
| 298 | } |
| 299 | |
| 300 | MethodInfo[] methods = customActionClass.GetMethods( |
| 301 | BindingFlags.Public | BindingFlags.Static); |
| 302 | foreach (MethodInfo method in methods) |
| 303 | { |
| 304 | if (method.Name == methodName && |
| 305 | CustomActionProxy.MethodHasCustomActionSignature(method)) |
| 306 | { |
| 307 | return method; |
| 308 | } |
| 309 | } |
| 310 | session.Log("Error: custom action method \"" + methodName + |
| 311 | "\" is missing or has the wrong signature."); |
| 312 | return null; |
| 313 | } |
| 314 | |
| 315 | /// <summary> |
| 316 | /// Checks if a method has the right return and paramater types |
| 317 | /// for a custom action, and that it is marked by a CustomActionAttribute. |
| 318 | /// </summary> |
| 319 | /// <param name="method">Method to be checked.</param> |
| 320 | /// <returns>True if the method is a valid custom action, else false.</returns> |
| 321 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] |
| 322 | private static bool MethodHasCustomActionSignature(MethodInfo method) |
| 323 | { |
| 324 | if (method.ReturnType == typeof(ActionResult) && |
| 325 | method.GetParameters().Length == 1 && |
| 326 | method.GetParameters()[0].ParameterType == typeof(Session)) |
| 327 | { |
| 328 | object[] methodAttribs = method.GetCustomAttributes(false); |
| 329 | foreach (object attrib in methodAttribs) |
| 330 | { |
| 331 | if (attrib is CustomActionAttribute) |
| 332 | { |
| 333 | return true; |
| 334 | } |
| 335 | } |
| 336 | } |
| 337 | return false; |
| 338 | } |
| 339 | } |
| 340 | } |