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