1 //---------------------------------------------------------------- 2 // Copyright (c) Microsoft Corporation. All rights reserved. 3 //---------------------------------------------------------------- 4 5 namespace System.Activities.Debugger 6 { 7 using System; 8 using System.Activities.Hosting; 9 using System.Activities.XamlIntegration; 10 using System.Diagnostics; 11 using System.Collections.Generic; 12 using System.Diagnostics.CodeAnalysis; 13 using System.Runtime; 14 using System.Reflection; 15 using System.Security; 16 using System.Security.Permissions; 17 using System.Xaml; 18 using System.Xml; 19 using System.IO; 20 using System.Activities.Validation; 21 using System.Collections.ObjectModel; 22 using System.Runtime.Serialization; 23 using System.Activities.Debugger.Symbol; 24 using System.Globalization; 25 26 // Provide SourceLocation information for activities in given root activity. 27 // This is integration point with Workflow project system (TBD). 28 // The current plan is to get SourceLocation from (in this order): 29 // 1. pdb (when available) 30 // 2a. parse xaml files available in the same project (or metadata store) or 31 // 2b. ask user to point to the correct xaml source. 32 // 3. Publish (serialize to tmp file) and deserialize it to collect SourceLocation (for loose xaml). 33 // Current code cover only step 3. 34 35 [DebuggerNonUserCode] 36 public static class SourceLocationProvider 37 { 38 [Fx.Tag.Throws(typeof(Exception), "Calls Serialize/Deserialize to temporary file")] 39 [SuppressMessage(FxCop.Category.Design, FxCop.Rule.DoNotCatchGeneralExceptionTypes, 40 Justification = "We catch all exceptions to avoid leaking security sensitive information.")] 41 [SuppressMessage(FxCop.Category.Security, "CA2103:ReviewImperativeSecurity", 42 Justification = "This is security reviewed.")] 43 [SuppressMessage(FxCop.Category.Security, FxCop.Rule.SecureAsserts, 44 Justification = "The Assert is only enforce while reading the file and the contents is not leaked.")] 45 [SuppressMessage("Reliability", "Reliability108:IsFatalRule", 46 Justification = "We catch all exceptions to avoid leaking security sensitive information.")] 47 [Fx.Tag.SecurityNote(Critical = "Asserting FileIOPermission(Read) for the specified file name that is contained the attached property on the XAML.", 48 Safe = "We are not exposing the contents of the file.")] 49 [SecuritySafeCritical] GetSourceLocations(Activity rootActivity, out string sourcePath, out bool isTemporaryFile, out byte[] checksum)50 static internal Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, out string sourcePath, out bool isTemporaryFile, out byte[] checksum) 51 { 52 isTemporaryFile = false; 53 checksum = null; 54 string symbolString = DebugSymbol.GetSymbol(rootActivity) as String; 55 if (string.IsNullOrEmpty(symbolString) && rootActivity.Children != null && rootActivity.Children.Count > 0) 56 { // In case of actual root is wrapped either in x:Class activity or CorrelationScope 57 Activity body = rootActivity.Children[0]; 58 string bodySymbolString = DebugSymbol.GetSymbol(body) as String; 59 if (!string.IsNullOrEmpty(bodySymbolString)) 60 { 61 rootActivity = body; 62 symbolString = bodySymbolString; 63 } 64 } 65 66 if (!string.IsNullOrEmpty(symbolString)) 67 { 68 try 69 { 70 WorkflowSymbol wfSymbol = WorkflowSymbol.Decode(symbolString); 71 if (wfSymbol != null) 72 { 73 sourcePath = wfSymbol.FileName; 74 checksum = wfSymbol.GetChecksum(); 75 // rootActivity is the activity with the attached symbol string. 76 // rootActivity.RootActivity is the workflow root activity. 77 // if they are not the same, then it must be compiled XAML, because loose XAML (i.e. XAMLX) always have the symbol attached at the root. 78 if (rootActivity.RootActivity != rootActivity) 79 { 80 Fx.Assert(rootActivity.Parent != null, "Compiled XAML implementation always have a parent."); 81 rootActivity = rootActivity.Parent; 82 } 83 return GetSourceLocations(rootActivity, wfSymbol, translateInternalActivityToOrigin: false); 84 } 85 } 86 catch (SerializationException) 87 { 88 // Ignore invalid symbol. 89 } 90 } 91 92 sourcePath = XamlDebuggerXmlReader.GetFileName(rootActivity) as string; 93 Dictionary<object, SourceLocation> mapping; 94 Assembly localAssembly; 95 bool permissionRevertNeeded = false; 96 97 // This may not be the local assembly since it may not be the real root for x:Class 98 localAssembly = rootActivity.GetType().Assembly; 99 100 if (rootActivity.Parent != null) 101 { 102 localAssembly = rootActivity.Parent.GetType().Assembly; 103 } 104 105 if (rootActivity.Children != null && rootActivity.Children.Count > 0) 106 { // In case of actual root is wrapped either in x:Class activity or CorrelationScope 107 Activity body = rootActivity.Children[0]; 108 string bodySourcePath = XamlDebuggerXmlReader.GetFileName(body) as string; 109 if (!string.IsNullOrEmpty(bodySourcePath)) 110 { 111 rootActivity = body; 112 sourcePath = bodySourcePath; 113 } 114 } 115 116 try 117 { 118 Fx.Assert(!string.IsNullOrEmpty(sourcePath), "If sourcePath is null, it should have been short-circuited before reaching here."); 119 120 SourceLocation tempSourceLocation; 121 Activity tempRootActivity; 122 123 checksum = SymbolHelper.CalculateChecksum(sourcePath); 124 125 if (TryGetSourceLocation(rootActivity, sourcePath, checksum, out tempSourceLocation)) // already has source location. 126 { 127 tempRootActivity = rootActivity; 128 } 129 else 130 { 131 byte[] buffer; 132 // Need to store the file in memory temporary so don't have to re-read the file twice 133 // for XamlDebugXmlReader's BracketLocator. 134 // If there is a debugger attached, Assert FileIOPermission for Read access to the specific file. 135 if (System.Diagnostics.Debugger.IsAttached) 136 { 137 permissionRevertNeeded = true; 138 FileIOPermission permission = new FileIOPermission(FileIOPermissionAccess.Read, sourcePath); 139 permission.Assert(); 140 } 141 142 try 143 { 144 FileInfo fi = new FileInfo(sourcePath); 145 buffer = new byte[fi.Length]; 146 147 using (FileStream fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read)) 148 { 149 fs.Read(buffer, 0, buffer.Length); 150 } 151 } 152 finally 153 { 154 // If we Asserted FileIOPermission, revert it. 155 if (permissionRevertNeeded) 156 { 157 CodeAccessPermission.RevertAssert(); 158 permissionRevertNeeded = false; 159 } 160 } 161 162 object deserializedObject = Deserialize(buffer, localAssembly); 163 IDebuggableWorkflowTree debuggableWorkflowTree = deserializedObject as IDebuggableWorkflowTree; 164 if (debuggableWorkflowTree != null) 165 { // Declarative Service and x:Class case 166 tempRootActivity = debuggableWorkflowTree.GetWorkflowRoot(); 167 } 168 else 169 { // Loose XAML case. 170 tempRootActivity = deserializedObject as Activity; 171 } 172 173 Fx.Assert(tempRootActivity != null, "Unexpected workflow xaml file"); 174 } 175 176 mapping = new Dictionary<object, SourceLocation>(); 177 if (tempRootActivity != null) 178 { 179 CollectMapping(rootActivity, tempRootActivity, mapping, sourcePath, checksum); 180 } 181 } 182 catch (Exception) 183 { 184 // Only eat the exception if we were running in partial trust. 185 if (!PartialTrustHelpers.AppDomainFullyTrusted) 186 { 187 // Eat the exception and return an empty dictionary. 188 return new Dictionary<object, SourceLocation>(); 189 } 190 else 191 { 192 throw; 193 } 194 } 195 196 return mapping; 197 } 198 GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol)199 public static Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol) 200 { 201 return GetSourceLocations(rootActivity, symbol, translateInternalActivityToOrigin: true); 202 } 203 204 // For most of the time, we need source location for object that appear on XAML. 205 // During debugging, however, we must not transform the internal activity to their origin to make sure it stop when the internal activity is about the execute 206 // Therefore, in debugger scenario, translateInternalActivityToOrigin will be set to false. GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol, bool translateInternalActivityToOrigin)207 internal static Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol, bool translateInternalActivityToOrigin) 208 { 209 Activity workflowRoot = rootActivity.RootActivity ?? rootActivity; 210 if (!workflowRoot.IsMetadataFullyCached) 211 { 212 IList<ValidationError> validationErrors = null; 213 ActivityUtilities.CacheRootMetadata(workflowRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); 214 } 215 216 Dictionary<object, SourceLocation> newMapping = new Dictionary<object, SourceLocation>(); 217 218 // Make sure the qid we are using to TryGetElementFromRoot 219 // are shifted appropriately such that the first digit that QID is 220 // the same as the last digit of the rootActivity.QualifiedId. 221 222 int[] rootIdArray = rootActivity.QualifiedId.AsIDArray(); 223 int idOffset = rootIdArray[rootIdArray.Length - 1] - 1; 224 225 foreach (ActivitySymbol actSym in symbol.Symbols) 226 { 227 QualifiedId qid = new QualifiedId(actSym.QualifiedId); 228 if (idOffset != 0) 229 { 230 int[] idArray = qid.AsIDArray(); 231 idArray[0] += idOffset; 232 qid = new QualifiedId(idArray); 233 } 234 Activity activity; 235 if (QualifiedId.TryGetElementFromRoot(rootActivity, qid, out activity)) 236 { 237 object origin = activity; 238 if (translateInternalActivityToOrigin && activity.Origin != null) 239 { 240 origin = activity.Origin; 241 } 242 243 newMapping.Add(origin, 244 new SourceLocation(symbol.FileName, symbol.GetChecksum(), actSym.StartLine, actSym.StartColumn, actSym.EndLine, actSym.EndColumn)); 245 } 246 } 247 return newMapping; 248 } 249 250 [Fx.Tag.SecurityNote(Miscellaneous = "RequiresReview - We are deserializing XAML from a file. The file may have been read under and Assert for FileIOPermission. The data hould be validated and not cached.")] Deserialize(byte[] buffer, Assembly localAssembly)251 internal static object Deserialize(byte[] buffer, Assembly localAssembly) 252 { 253 using (MemoryStream memoryStream = new MemoryStream(buffer)) 254 { 255 using (TextReader streamReader = new StreamReader(memoryStream)) 256 { 257 using (XamlDebuggerXmlReader xamlDebuggerReader = new XamlDebuggerXmlReader(streamReader, new XamlSchemaContext(), localAssembly)) 258 { 259 xamlDebuggerReader.SourceLocationFound += XamlDebuggerXmlReader.SetSourceLocation; 260 261 using (XamlReader activityBuilderReader = ActivityXamlServices.CreateBuilderReader(xamlDebuggerReader)) 262 { 263 return XamlServices.Load(activityBuilderReader); 264 } 265 } 266 } 267 } 268 } 269 CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path)270 public static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path) 271 { 272 CollectMapping(rootActivity1, rootActivity2, mapping, path, null, requirePrepareForRuntime: true); 273 } 274 275 // Collect mapping for activity1 and its descendants to their corresponding source location. 276 // activity2 is the shadow of activity1 but with SourceLocation information. 277 [Fx.Tag.SecurityNote(Miscellaneous = "RequiresReview - We are dealing with activity and SourceLocation information that came from the user, possibly under an Assert for FileIOPermission. The data hould be validated and not cached.")] CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum, bool requirePrepareForRuntime)278 static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum, bool requirePrepareForRuntime) 279 { 280 // For x:Class, the rootActivity here may not be the real root, but it's the first child of the x:Class activity. 281 Activity realRoot1 = (rootActivity1.RootActivity != null) ? rootActivity1.RootActivity : rootActivity1; 282 if ((requirePrepareForRuntime && !realRoot1.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot1.IsMetadataFullyCached)) 283 { 284 IList<ValidationError> validationErrors = null; 285 ActivityUtilities.CacheRootMetadata(realRoot1, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); 286 } 287 288 // Similarly for rootActivity2. 289 Activity realRoot2 = (rootActivity2.RootActivity != null) ? rootActivity2.RootActivity : rootActivity2; 290 if (rootActivity1 != rootActivity2 && (requirePrepareForRuntime && !realRoot2.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot2.IsMetadataFullyCached)) 291 { 292 IList<ValidationError> validationErrors = null; 293 ActivityUtilities.CacheRootMetadata(realRoot2, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); 294 } 295 296 Queue<KeyValuePair<Activity, Activity>> pairsRemaining = new Queue<KeyValuePair<Activity, Activity>>(); 297 298 pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(rootActivity1, rootActivity2)); 299 KeyValuePair<Activity, Activity> currentPair; 300 HashSet<Activity> visited = new HashSet<Activity>(); 301 302 while (pairsRemaining.Count > 0) 303 { 304 currentPair = pairsRemaining.Dequeue(); 305 Activity activity1 = currentPair.Key; 306 Activity activity2 = currentPair.Value; 307 308 visited.Add(activity1); 309 310 SourceLocation sourceLocation; 311 if (TryGetSourceLocation(activity2, path, checksum, out sourceLocation)) 312 { 313 mapping.Add(activity1, sourceLocation); 314 } 315 else if (!((activity2 is IExpressionContainer) || (activity2 is IValueSerializableExpression))) // Expression is known not to have source location. 316 { 317 //Some activities may not have corresponding Xaml node, e.g. ActivityFaultedOutput. 318 Trace.WriteLine("WorkflowDebugger: Does not have corresponding Xaml node for: " + activity2.DisplayName + "\n"); 319 } 320 321 // This to avoid comparing any value expression with DesignTimeValueExpression (in designer case). 322 if (!((activity1 is IExpressionContainer) || (activity2 is IExpressionContainer) || 323 (activity1 is IValueSerializableExpression) || (activity2 is IValueSerializableExpression))) 324 { 325 IEnumerator<Activity> enumerator1 = WorkflowInspectionServices.GetActivities(activity1).GetEnumerator(); 326 IEnumerator<Activity> enumerator2 = WorkflowInspectionServices.GetActivities(activity2).GetEnumerator(); 327 bool hasNextItem1 = enumerator1.MoveNext(); 328 bool hasNextItem2 = enumerator2.MoveNext(); 329 while (hasNextItem1 && hasNextItem2) 330 { 331 if (!visited.Contains(enumerator1.Current)) // avoid adding the same activity (e.g. some default implementation). 332 { 333 if (enumerator1.Current.GetType() != enumerator2.Current.GetType()) 334 { 335 // Give debugger log instead of just asserting; to help user find out mismatch problem. 336 Trace.WriteLine( 337 "Unmatched type: " + enumerator1.Current.GetType().FullName + 338 " vs " + enumerator2.Current.GetType().FullName + "\n"); 339 } 340 pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(enumerator1.Current, enumerator2.Current)); 341 } 342 hasNextItem1 = enumerator1.MoveNext(); 343 hasNextItem2 = enumerator2.MoveNext(); 344 } 345 346 // If enumerators do not finish at the same time, then they have unmatched number of activities. 347 // Give debugger log instead of just asserting; to help user find out mismatch problem. 348 if (hasNextItem1 || hasNextItem2) 349 { 350 Trace.WriteLine("Unmatched number of children\n"); 351 } 352 } 353 } 354 } 355 CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum)356 static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum) 357 { 358 CollectMapping(rootActivity1, rootActivity2, mapping, path, checksum, requirePrepareForRuntime: true); 359 } 360 // Get SourceLocation for object deserialized with XamlDebuggerXmlReader in deserializer stack. TryGetSourceLocation(object obj, string path, byte[] checksum, out SourceLocation sourceLocation)361 static bool TryGetSourceLocation(object obj, string path, byte[] checksum, out SourceLocation sourceLocation) 362 { 363 sourceLocation = null; 364 int startLine, startColumn, endLine, endColumn; 365 366 if (AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.StartLineName, out startLine) && 367 AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.StartColumnName, out startColumn) && 368 AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.EndLineName, out endLine) && 369 AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.EndColumnName, out endColumn) && 370 SourceLocation.IsValidRange(startLine, startColumn, endLine, endColumn)) 371 { 372 sourceLocation = new SourceLocation(path, checksum, startLine, startColumn, endLine, endColumn); 373 return true; 374 } 375 return false; 376 } 377 GetSymbols(Activity rootActivity, Dictionary<object, SourceLocation> sourceLocations)378 public static ICollection<ActivitySymbol> GetSymbols(Activity rootActivity, Dictionary<object, SourceLocation> sourceLocations) 379 { 380 List<ActivitySymbol> symbols = new List<ActivitySymbol>(); 381 Activity realRoot = (rootActivity.RootActivity != null) ? rootActivity.RootActivity : rootActivity; 382 if (!realRoot.IsMetadataFullyCached) 383 { 384 IList<ValidationError> validationErrors = null; 385 ActivityUtilities.CacheRootMetadata(realRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); 386 } 387 Queue<Activity> activitiesRemaining = new Queue<Activity>(); 388 activitiesRemaining.Enqueue(realRoot); 389 HashSet<Activity> visited = new HashSet<Activity>(); 390 while (activitiesRemaining.Count > 0) 391 { 392 Activity currentActivity = activitiesRemaining.Dequeue(); 393 SourceLocation sourceLocation; 394 object origin = currentActivity.Origin == null ? currentActivity : currentActivity.Origin; 395 if (!visited.Contains(currentActivity) && sourceLocations.TryGetValue(origin, out sourceLocation)) 396 { 397 symbols.Add(new ActivitySymbol 398 { 399 QualifiedId = currentActivity.QualifiedId.AsByteArray(), 400 StartLine = sourceLocation.StartLine, 401 StartColumn = sourceLocation.StartColumn, 402 EndLine = sourceLocation.EndLine, 403 EndColumn = sourceLocation.EndColumn 404 }); 405 } 406 visited.Add(currentActivity); 407 foreach (Activity childActivity in WorkflowInspectionServices.GetActivities(currentActivity)) 408 { 409 activitiesRemaining.Enqueue(childActivity); 410 } 411 } 412 return symbols; 413 } 414 } 415 } 416