//---------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //---------------------------------------------------------------- namespace System.Activities.Debugger { using System; using System.Activities.Hosting; using System.Activities.XamlIntegration; using System.Diagnostics; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime; using System.Reflection; using System.Security; using System.Security.Permissions; using System.Xaml; using System.Xml; using System.IO; using System.Activities.Validation; using System.Collections.ObjectModel; using System.Runtime.Serialization; using System.Activities.Debugger.Symbol; using System.Globalization; // Provide SourceLocation information for activities in given root activity. // This is integration point with Workflow project system (TBD). // The current plan is to get SourceLocation from (in this order): // 1. pdb (when available) // 2a. parse xaml files available in the same project (or metadata store) or // 2b. ask user to point to the correct xaml source. // 3. Publish (serialize to tmp file) and deserialize it to collect SourceLocation (for loose xaml). // Current code cover only step 3. [DebuggerNonUserCode] public static class SourceLocationProvider { [Fx.Tag.Throws(typeof(Exception), "Calls Serialize/Deserialize to temporary file")] [SuppressMessage(FxCop.Category.Design, FxCop.Rule.DoNotCatchGeneralExceptionTypes, Justification = "We catch all exceptions to avoid leaking security sensitive information.")] [SuppressMessage(FxCop.Category.Security, "CA2103:ReviewImperativeSecurity", Justification = "This is security reviewed.")] [SuppressMessage(FxCop.Category.Security, FxCop.Rule.SecureAsserts, Justification = "The Assert is only enforce while reading the file and the contents is not leaked.")] [SuppressMessage("Reliability", "Reliability108:IsFatalRule", Justification = "We catch all exceptions to avoid leaking security sensitive information.")] [Fx.Tag.SecurityNote(Critical = "Asserting FileIOPermission(Read) for the specified file name that is contained the attached property on the XAML.", Safe = "We are not exposing the contents of the file.")] [SecuritySafeCritical] static internal Dictionary GetSourceLocations(Activity rootActivity, out string sourcePath, out bool isTemporaryFile, out byte[] checksum) { isTemporaryFile = false; checksum = null; string symbolString = DebugSymbol.GetSymbol(rootActivity) as String; if (string.IsNullOrEmpty(symbolString) && rootActivity.Children != null && rootActivity.Children.Count > 0) { // In case of actual root is wrapped either in x:Class activity or CorrelationScope Activity body = rootActivity.Children[0]; string bodySymbolString = DebugSymbol.GetSymbol(body) as String; if (!string.IsNullOrEmpty(bodySymbolString)) { rootActivity = body; symbolString = bodySymbolString; } } if (!string.IsNullOrEmpty(symbolString)) { try { WorkflowSymbol wfSymbol = WorkflowSymbol.Decode(symbolString); if (wfSymbol != null) { sourcePath = wfSymbol.FileName; checksum = wfSymbol.GetChecksum(); // rootActivity is the activity with the attached symbol string. // rootActivity.RootActivity is the workflow root activity. // 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. if (rootActivity.RootActivity != rootActivity) { Fx.Assert(rootActivity.Parent != null, "Compiled XAML implementation always have a parent."); rootActivity = rootActivity.Parent; } return GetSourceLocations(rootActivity, wfSymbol, translateInternalActivityToOrigin: false); } } catch (SerializationException) { // Ignore invalid symbol. } } sourcePath = XamlDebuggerXmlReader.GetFileName(rootActivity) as string; Dictionary mapping; Assembly localAssembly; bool permissionRevertNeeded = false; // This may not be the local assembly since it may not be the real root for x:Class localAssembly = rootActivity.GetType().Assembly; if (rootActivity.Parent != null) { localAssembly = rootActivity.Parent.GetType().Assembly; } if (rootActivity.Children != null && rootActivity.Children.Count > 0) { // In case of actual root is wrapped either in x:Class activity or CorrelationScope Activity body = rootActivity.Children[0]; string bodySourcePath = XamlDebuggerXmlReader.GetFileName(body) as string; if (!string.IsNullOrEmpty(bodySourcePath)) { rootActivity = body; sourcePath = bodySourcePath; } } try { Fx.Assert(!string.IsNullOrEmpty(sourcePath), "If sourcePath is null, it should have been short-circuited before reaching here."); SourceLocation tempSourceLocation; Activity tempRootActivity; checksum = SymbolHelper.CalculateChecksum(sourcePath); if (TryGetSourceLocation(rootActivity, sourcePath, checksum, out tempSourceLocation)) // already has source location. { tempRootActivity = rootActivity; } else { byte[] buffer; // Need to store the file in memory temporary so don't have to re-read the file twice // for XamlDebugXmlReader's BracketLocator. // If there is a debugger attached, Assert FileIOPermission for Read access to the specific file. if (System.Diagnostics.Debugger.IsAttached) { permissionRevertNeeded = true; FileIOPermission permission = new FileIOPermission(FileIOPermissionAccess.Read, sourcePath); permission.Assert(); } try { FileInfo fi = new FileInfo(sourcePath); buffer = new byte[fi.Length]; using (FileStream fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read)) { fs.Read(buffer, 0, buffer.Length); } } finally { // If we Asserted FileIOPermission, revert it. if (permissionRevertNeeded) { CodeAccessPermission.RevertAssert(); permissionRevertNeeded = false; } } object deserializedObject = Deserialize(buffer, localAssembly); IDebuggableWorkflowTree debuggableWorkflowTree = deserializedObject as IDebuggableWorkflowTree; if (debuggableWorkflowTree != null) { // Declarative Service and x:Class case tempRootActivity = debuggableWorkflowTree.GetWorkflowRoot(); } else { // Loose XAML case. tempRootActivity = deserializedObject as Activity; } Fx.Assert(tempRootActivity != null, "Unexpected workflow xaml file"); } mapping = new Dictionary(); if (tempRootActivity != null) { CollectMapping(rootActivity, tempRootActivity, mapping, sourcePath, checksum); } } catch (Exception) { // Only eat the exception if we were running in partial trust. if (!PartialTrustHelpers.AppDomainFullyTrusted) { // Eat the exception and return an empty dictionary. return new Dictionary(); } else { throw; } } return mapping; } public static Dictionary GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol) { return GetSourceLocations(rootActivity, symbol, translateInternalActivityToOrigin: true); } // For most of the time, we need source location for object that appear on XAML. // 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 // Therefore, in debugger scenario, translateInternalActivityToOrigin will be set to false. internal static Dictionary GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol, bool translateInternalActivityToOrigin) { Activity workflowRoot = rootActivity.RootActivity ?? rootActivity; if (!workflowRoot.IsMetadataFullyCached) { IList validationErrors = null; ActivityUtilities.CacheRootMetadata(workflowRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); } Dictionary newMapping = new Dictionary(); // Make sure the qid we are using to TryGetElementFromRoot // are shifted appropriately such that the first digit that QID is // the same as the last digit of the rootActivity.QualifiedId. int[] rootIdArray = rootActivity.QualifiedId.AsIDArray(); int idOffset = rootIdArray[rootIdArray.Length - 1] - 1; foreach (ActivitySymbol actSym in symbol.Symbols) { QualifiedId qid = new QualifiedId(actSym.QualifiedId); if (idOffset != 0) { int[] idArray = qid.AsIDArray(); idArray[0] += idOffset; qid = new QualifiedId(idArray); } Activity activity; if (QualifiedId.TryGetElementFromRoot(rootActivity, qid, out activity)) { object origin = activity; if (translateInternalActivityToOrigin && activity.Origin != null) { origin = activity.Origin; } newMapping.Add(origin, new SourceLocation(symbol.FileName, symbol.GetChecksum(), actSym.StartLine, actSym.StartColumn, actSym.EndLine, actSym.EndColumn)); } } return newMapping; } [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.")] internal static object Deserialize(byte[] buffer, Assembly localAssembly) { using (MemoryStream memoryStream = new MemoryStream(buffer)) { using (TextReader streamReader = new StreamReader(memoryStream)) { using (XamlDebuggerXmlReader xamlDebuggerReader = new XamlDebuggerXmlReader(streamReader, new XamlSchemaContext(), localAssembly)) { xamlDebuggerReader.SourceLocationFound += XamlDebuggerXmlReader.SetSourceLocation; using (XamlReader activityBuilderReader = ActivityXamlServices.CreateBuilderReader(xamlDebuggerReader)) { return XamlServices.Load(activityBuilderReader); } } } } } public static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary mapping, string path) { CollectMapping(rootActivity1, rootActivity2, mapping, path, null, requirePrepareForRuntime: true); } // Collect mapping for activity1 and its descendants to their corresponding source location. // activity2 is the shadow of activity1 but with SourceLocation information. [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.")] static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary mapping, string path, byte[] checksum, bool requirePrepareForRuntime) { // For x:Class, the rootActivity here may not be the real root, but it's the first child of the x:Class activity. Activity realRoot1 = (rootActivity1.RootActivity != null) ? rootActivity1.RootActivity : rootActivity1; if ((requirePrepareForRuntime && !realRoot1.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot1.IsMetadataFullyCached)) { IList validationErrors = null; ActivityUtilities.CacheRootMetadata(realRoot1, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); } // Similarly for rootActivity2. Activity realRoot2 = (rootActivity2.RootActivity != null) ? rootActivity2.RootActivity : rootActivity2; if (rootActivity1 != rootActivity2 && (requirePrepareForRuntime && !realRoot2.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot2.IsMetadataFullyCached)) { IList validationErrors = null; ActivityUtilities.CacheRootMetadata(realRoot2, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); } Queue> pairsRemaining = new Queue>(); pairsRemaining.Enqueue(new KeyValuePair(rootActivity1, rootActivity2)); KeyValuePair currentPair; HashSet visited = new HashSet(); while (pairsRemaining.Count > 0) { currentPair = pairsRemaining.Dequeue(); Activity activity1 = currentPair.Key; Activity activity2 = currentPair.Value; visited.Add(activity1); SourceLocation sourceLocation; if (TryGetSourceLocation(activity2, path, checksum, out sourceLocation)) { mapping.Add(activity1, sourceLocation); } else if (!((activity2 is IExpressionContainer) || (activity2 is IValueSerializableExpression))) // Expression is known not to have source location. { //Some activities may not have corresponding Xaml node, e.g. ActivityFaultedOutput. Trace.WriteLine("WorkflowDebugger: Does not have corresponding Xaml node for: " + activity2.DisplayName + "\n"); } // This to avoid comparing any value expression with DesignTimeValueExpression (in designer case). if (!((activity1 is IExpressionContainer) || (activity2 is IExpressionContainer) || (activity1 is IValueSerializableExpression) || (activity2 is IValueSerializableExpression))) { IEnumerator enumerator1 = WorkflowInspectionServices.GetActivities(activity1).GetEnumerator(); IEnumerator enumerator2 = WorkflowInspectionServices.GetActivities(activity2).GetEnumerator(); bool hasNextItem1 = enumerator1.MoveNext(); bool hasNextItem2 = enumerator2.MoveNext(); while (hasNextItem1 && hasNextItem2) { if (!visited.Contains(enumerator1.Current)) // avoid adding the same activity (e.g. some default implementation). { if (enumerator1.Current.GetType() != enumerator2.Current.GetType()) { // Give debugger log instead of just asserting; to help user find out mismatch problem. Trace.WriteLine( "Unmatched type: " + enumerator1.Current.GetType().FullName + " vs " + enumerator2.Current.GetType().FullName + "\n"); } pairsRemaining.Enqueue(new KeyValuePair(enumerator1.Current, enumerator2.Current)); } hasNextItem1 = enumerator1.MoveNext(); hasNextItem2 = enumerator2.MoveNext(); } // If enumerators do not finish at the same time, then they have unmatched number of activities. // Give debugger log instead of just asserting; to help user find out mismatch problem. if (hasNextItem1 || hasNextItem2) { Trace.WriteLine("Unmatched number of children\n"); } } } } static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary mapping, string path, byte[] checksum) { CollectMapping(rootActivity1, rootActivity2, mapping, path, checksum, requirePrepareForRuntime: true); } // Get SourceLocation for object deserialized with XamlDebuggerXmlReader in deserializer stack. static bool TryGetSourceLocation(object obj, string path, byte[] checksum, out SourceLocation sourceLocation) { sourceLocation = null; int startLine, startColumn, endLine, endColumn; if (AttachablePropertyServices.TryGetProperty(obj, XamlDebuggerXmlReader.StartLineName, out startLine) && AttachablePropertyServices.TryGetProperty(obj, XamlDebuggerXmlReader.StartColumnName, out startColumn) && AttachablePropertyServices.TryGetProperty(obj, XamlDebuggerXmlReader.EndLineName, out endLine) && AttachablePropertyServices.TryGetProperty(obj, XamlDebuggerXmlReader.EndColumnName, out endColumn) && SourceLocation.IsValidRange(startLine, startColumn, endLine, endColumn)) { sourceLocation = new SourceLocation(path, checksum, startLine, startColumn, endLine, endColumn); return true; } return false; } public static ICollection GetSymbols(Activity rootActivity, Dictionary sourceLocations) { List symbols = new List(); Activity realRoot = (rootActivity.RootActivity != null) ? rootActivity.RootActivity : rootActivity; if (!realRoot.IsMetadataFullyCached) { IList validationErrors = null; ActivityUtilities.CacheRootMetadata(realRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors); } Queue activitiesRemaining = new Queue(); activitiesRemaining.Enqueue(realRoot); HashSet visited = new HashSet(); while (activitiesRemaining.Count > 0) { Activity currentActivity = activitiesRemaining.Dequeue(); SourceLocation sourceLocation; object origin = currentActivity.Origin == null ? currentActivity : currentActivity.Origin; if (!visited.Contains(currentActivity) && sourceLocations.TryGetValue(origin, out sourceLocation)) { symbols.Add(new ActivitySymbol { QualifiedId = currentActivity.QualifiedId.AsByteArray(), StartLine = sourceLocation.StartLine, StartColumn = sourceLocation.StartColumn, EndLine = sourceLocation.EndLine, EndColumn = sourceLocation.EndColumn }); } visited.Add(currentActivity); foreach (Activity childActivity in WorkflowInspectionServices.GetActivities(currentActivity)) { activitiesRemaining.Enqueue(childActivity); } } return symbols; } } }