//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- namespace System.Activities.Debugger { using System; using System.Activities.Expressions; using System.Activities.Hosting; using System.Activities.Runtime; using System.Activities.Statements; using System.Activities.XamlIntegration; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Runtime; [DebuggerNonUserCode] class DebugManager { static StateManager.DynamicModuleManager dynamicModuleManager; WorkflowInstance host; StateManager stateManager; Dictionary states; Dictionary> runningThreads; InstrumentationTracker instrumentationTracker; List temporaryFiles; public DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup, WorkflowInstance host, bool debugStartedAtRoot) : this(root, moduleNamePrefix, typeNamePrefix, auxiliaryThreadName, breakOnStartup, host, debugStartedAtRoot, false) { } internal DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup, WorkflowInstance host, bool debugStartedAtRoot, bool resetDynamicModule) { if (resetDynamicModule) { dynamicModuleManager = null; } if (dynamicModuleManager == null) { dynamicModuleManager = new StateManager.DynamicModuleManager(moduleNamePrefix); } this.stateManager = new StateManager( new StateManager.Properties { ModuleNamePrefix = moduleNamePrefix, TypeNamePrefix = typeNamePrefix, AuxiliaryThreadName = auxiliaryThreadName, BreakOnStartup = breakOnStartup }, debugStartedAtRoot, dynamicModuleManager); this.states = new Dictionary(); this.runningThreads = new Dictionary>(); this.instrumentationTracker = new InstrumentationTracker(root); this.host = host; } // Whether we're priming the background thread (in Attach To Process case). public bool IsPriming { set { this.stateManager.IsPriming = value; } } // Whether debugging is done from the start of the root workflow, // contrast to attaching into the middle of a running workflow. bool DebugStartedAtRoot { get { return this.stateManager.DebugStartedAtRoot; } } internal void Instrument(Activity activity) { bool isTemporaryFile = false; string sourcePath = null; bool instrumentationFailed = false; Dictionary checksumCache = null; try { byte[] checksum; Dictionary sourceLocations = SourceLocationProvider.GetSourceLocations(activity, out sourcePath, out isTemporaryFile, out checksum); if (checksum != null) { checksumCache = new Dictionary(); checksumCache.Add(sourcePath.ToUpperInvariant(), checksum); } Instrument(activity, sourceLocations, Path.GetFileNameWithoutExtension(sourcePath), checksumCache); } catch (Exception ex) { instrumentationFailed = true; Trace.WriteLine(SR.DebugInstrumentationFailed(ex.Message)); if (Fx.IsFatal(ex)) { throw; } } List sameSourceActivities = this.instrumentationTracker.GetSameSourceSubRoots(activity); this.instrumentationTracker.MarkInstrumented(activity); foreach (Activity sameSourceActivity in sameSourceActivities) { if (!instrumentationFailed) { MapInstrumentationStates(activity, sameSourceActivity); } // Mark it as instrumentated, even though it fails so it won't be // retried. this.instrumentationTracker.MarkInstrumented(sameSourceActivity); } if (isTemporaryFile) { if (this.temporaryFiles == null) { this.temporaryFiles = new List(); } Fx.Assert(!string.IsNullOrEmpty(sourcePath), "SourcePath cannot be null for temporary file"); this.temporaryFiles.Add(sourcePath); } } // Workflow rooted at rootActivity1 and rootActivity2 have same source file, but they // are two different instantiation. // rootActivity1 has been instrumented and its instrumentation states can be // re-used by rootActivity2. // // MapInstrumentationStates will walk both Workflow trees in parallel and map every // state for activities in rootActivity1 to corresponding activities in rootActivity2. void MapInstrumentationStates(Activity rootActivity1, Activity rootActivity2) { Queue> pairsRemaining = new Queue>(); pairsRemaining.Enqueue(new KeyValuePair(rootActivity1, rootActivity2)); HashSet visited = new HashSet(); KeyValuePair currentPair; State state; while (pairsRemaining.Count > 0) { currentPair = pairsRemaining.Dequeue(); Activity activity1 = currentPair.Key; Activity activity2 = currentPair.Value; if (this.states.TryGetValue(activity1, out state)) { if (this.states.ContainsKey(activity2)) { Trace.WriteLine("Workflow", SR.DuplicateInstrumentation(activity2.DisplayName)); } else { // Map activity2 to the same state. this.states.Add(activity2, state); } } //Some activities may not have corresponding Xaml node, e.g. ActivityFaultedOutput. visited.Add(activity1); // This to avoid comparing any value expression with DesignTimeValueExpression (in designer case). 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("Workflow", "Unmatched number of children\n"); } } } // Main instrumentation. // Currently the typeNamePrefix is used to notify the Designer of which file to show. // This will no longer necessary when the callstack API can give us the source line // information. public void Instrument(Activity rootActivity, Dictionary sourceLocations, string typeNamePrefix, Dictionary checksumCache) { Queue> pairsRemaining = new Queue>(); string name; Activity activity = rootActivity; KeyValuePair pair = new KeyValuePair(activity, string.Empty); pairsRemaining.Enqueue(pair); HashSet existingNames = new HashSet(); HashSet visited = new HashSet(); SourceLocation sourceLocation; while (pairsRemaining.Count > 0) { pair = pairsRemaining.Dequeue(); activity = pair.Key; string parentName = pair.Value; string displayName = activity.DisplayName; // If no DisplayName, then use the type name. if (string.IsNullOrEmpty(displayName)) { displayName = activity.GetType().Name; } if (parentName == string.Empty) { // the root name = displayName; } else { name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", parentName, displayName); } int i = 0; while (existingNames.Contains(name)) { ++i; name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}{2}", parentName, displayName, i.ToString(CultureInfo.InvariantCulture)); } existingNames.Add(name); visited.Add(activity); if (sourceLocations.TryGetValue(activity, out sourceLocation)) { object[] objects = activity.GetType().GetCustomAttributes(typeof(DebuggerStepThroughAttribute), false); if ((objects == null || objects.Length == 0)) { Instrument(activity, sourceLocation, name); } } foreach (Activity childActivity in WorkflowInspectionServices.GetActivities(activity)) { if (!visited.Contains(childActivity)) { pairsRemaining.Enqueue(new KeyValuePair(childActivity, name)); } } } this.stateManager.Bake(typeNamePrefix, checksumCache); } // Exiting the DebugManager. // Delete all temporary files public void Exit() { if (this.temporaryFiles != null) { foreach (string temporaryFile in this.temporaryFiles) { // Clean up published source. try { File.Delete(temporaryFile); } catch (IOException) { // ---- IOException silently. } this.temporaryFiles = null; } } this.stateManager.ExitThreads(); // State manager is still keep for the session in SessionStateManager this.stateManager = null; } void Instrument(Activity activity, SourceLocation sourceLocation, string name) { Fx.Assert(activity != null, "activity can't be null"); Fx.Assert(sourceLocation != null, "sourceLocation can't be null"); if (this.states.ContainsKey(activity)) { Trace.WriteLine(SR.DuplicateInstrumentation(activity.DisplayName)); } else { State activityState = this.stateManager.DefineStateWithDebugInfo(sourceLocation, name); this.states.Add(activity, activityState); } } // Test whether activity has been instrumented. // If not, try to instrument it. // It will return true if instrumentation is already done or // instrumentation is succesful. False otherwise. bool EnsureInstrumented(Activity activity) { // This is the most common case, we will find the instrumentation. if (this.states.ContainsKey(activity)) { return true; } // No states correspond to this yet. if (this.instrumentationTracker.IsUninstrumentedSubRoot(activity)) { Instrument(activity); return this.states.ContainsKey(activity); } else { return false; } } // Primitive EnterState void EnterState(int threadId, Activity activity, Dictionary locals) { Fx.Assert(activity != null, "activity cannot be null"); this.Push(threadId, activity); State activityState; if (this.states.TryGetValue(activity, out activityState)) { this.stateManager.EnterState(threadId, activityState, locals); } else { Fx.Assert(false, "Uninstrumented activity is disallowed: " + activity.DisplayName); } } public void OnEnterState(ActivityInstance instance) { Fx.Assert(instance != null, "ActivityInstance cannot be null"); Activity activity = instance.Activity; if (this.EnsureInstrumented(activity)) { this.EnterState(GetOrCreateThreadId(activity, instance), activity, GenerateLocals(instance)); } } [SuppressMessage(FxCop.Category.Usage, FxCop.Rule.ReviewUnusedParameters)] public void OnEnterState(Activity expression, ActivityInstance instance, LocationEnvironment environment) { if (this.EnsureInstrumented(expression)) { this.EnterState(GetOrCreateThreadId(expression, instance), expression, GenerateLocals(instance)); } } void LeaveState(Activity activity) { Fx.Assert(activity != null, "Activity cannot be null"); int threadId = GetExecutingThreadId(activity, true); // If debugging was not started from the root, then threadId should not be < 0. Fx.Assert(!this.DebugStartedAtRoot || threadId >= 0, "Leaving from an unknown state"); if (threadId >= 0) { State activityState; if (this.states.TryGetValue(activity, out activityState)) { this.stateManager.LeaveState(threadId, activityState); } else { Fx.Assert(false, "Uninstrumented activity is disallowed: " + activity.DisplayName); } this.Pop(threadId); } } public void OnLeaveState(ActivityInstance activityInstance) { Fx.Assert(activityInstance != null, "ActivityInstance cannot be null"); if (this.EnsureInstrumented(activityInstance.Activity)) { this.LeaveState(activityInstance.Activity); } } static Dictionary GenerateLocals(ActivityInstance instance) { Dictionary locals = new Dictionary(); locals.Add("debugInfo", new DebugInfo(instance)); return locals; } void Push(int threadId, Activity activity) { ((Stack)this.runningThreads[threadId]).Push(activity); } void Pop(int threadId) { Stack stack = this.runningThreads[threadId]; stack.Pop(); if (stack.Count == 0) { this.stateManager.Exit(threadId); this.runningThreads.Remove(threadId); } } // Given an activity, return the thread id where it is currently // executed (on the top of the callstack). // Boolean "strict" parameter determine whether the activity itself should // be on top of the stack. // Strict checking is needed in the case of "Leave"-ing a state. // Non-strict checking is needed for "Enter"-ing a state, since the direct parent of // the activity may not yet be executed (e.g. the activity is an argument of another activity, // the activity is "enter"-ed even though the direct parent is not yet "enter"-ed. int GetExecutingThreadId(Activity activity, bool strict) { int threadId = -1; foreach (KeyValuePair> entry in this.runningThreads) { Stack threadStack = entry.Value; if (threadStack.Peek() == activity) { threadId = entry.Key; break; } } if (threadId < 0 && !strict) { foreach (KeyValuePair> entry in this.runningThreads) { Stack threadStack = entry.Value; Activity topActivity = threadStack.Peek(); if (!IsParallelActivity(topActivity) && IsAncestorOf(threadStack.Peek(), activity)) { threadId = entry.Key; break; } } } return threadId; } static bool IsAncestorOf(Activity ancestorActivity, Activity activity) { Fx.Assert(activity != null, "IsAncestorOf: Cannot pass null as activity"); Fx.Assert(ancestorActivity != null, "IsAncestorOf: Cannot pass null as ancestorActivity"); activity = activity.Parent; while (activity != null && activity != ancestorActivity && !IsParallelActivity(activity)) { activity = activity.Parent; } return (activity == ancestorActivity); } static bool IsParallelActivity(Activity activity) { Fx.Assert(activity != null, "IsParallel: Cannot pass null as activity"); return activity is Parallel || (activity.GetType().IsGenericType && activity.GetType().GetGenericTypeDefinition() == typeof(ParallelForEach<>)); } // Get threads currently executing the parent of the given activity, // if none then create a new one and prep the call stack to current state. int GetOrCreateThreadId(Activity activity, ActivityInstance instance) { int threadId = -1; if (activity.Parent != null && !IsParallelActivity(activity.Parent)) { threadId = GetExecutingThreadId(activity.Parent, false); } if (threadId < 0) { threadId = CreateLogicalThread(activity, instance, false); } return threadId; } // Create logical thread and bring its call stack to reflect call from // the root up to (but not including) the instance. // If the activity is an expression though, then the call stack will also include the instance // (since it is the parent of the expression). int CreateLogicalThread(Activity activity, ActivityInstance instance, bool primeCurrentInstance) { Stack ancestors = null; if (!this.DebugStartedAtRoot) { ancestors = new Stack(); if (activity != instance.Activity || primeCurrentInstance) { // This mean that activity is an expression and // instance is the parent of this expression. Fx.Assert(primeCurrentInstance || (activity is ActivityWithResult), "Expect an ActivityWithResult"); Fx.Assert(primeCurrentInstance || (activity.Parent == instance.Activity), "Argument Expression is not given correct parent instance"); if (primeCurrentInstance || !IsParallelActivity(instance.Activity)) { ancestors.Push(instance); } } ActivityInstance instanceParent = instance.Parent; while (instanceParent != null && !IsParallelActivity(instanceParent.Activity)) { ancestors.Push(instanceParent); instanceParent = instanceParent.Parent; } if (instanceParent != null && IsParallelActivity(instanceParent.Activity)) { // Ensure thread is created for the parent (a Parallel activity). int parentThreadId = GetExecutingThreadId(instanceParent.Activity, false); if (parentThreadId < 0) { parentThreadId = CreateLogicalThread(instanceParent.Activity, instanceParent, true); Fx.Assert(parentThreadId > 0, "Parallel main thread can't be created"); } } } string threadName = "DebuggerThread:"; if (activity.Parent != null) { threadName += activity.Parent.DisplayName; } else // Special case for the root of WorklowService that does not have a parent. { threadName += activity.DisplayName; } int newThreadId = this.stateManager.CreateLogicalThread(threadName); Stack newStack = new Stack(); this.runningThreads.Add(newThreadId, newStack); if (!this.DebugStartedAtRoot && ancestors != null) { // Need to create callstack to current activity. PrimeCallStack(newThreadId, ancestors); } return newThreadId; } // Prime the call stack to contains all the ancestors of this instance. // Note: the call stack will not include the current instance. void PrimeCallStack(int threadId, Stack ancestors) { Fx.Assert(!this.DebugStartedAtRoot, "Priming should not be called if the debugging is attached from the start of the workflow"); bool currentIsPrimingValue = this.stateManager.IsPriming; this.stateManager.IsPriming = true; while (ancestors.Count > 0) { ActivityInstance currentInstance = ancestors.Pop(); if (EnsureInstrumented(currentInstance.Activity)) { this.EnterState(threadId, currentInstance.Activity, GenerateLocals(currentInstance)); } } this.stateManager.IsPriming = currentIsPrimingValue; } } }