e79aa3c0ed
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
589 lines
24 KiB
C#
589 lines
24 KiB
C#
//-----------------------------------------------------------------------------
|
|
// 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<object, State> states;
|
|
Dictionary<int, Stack<Activity>> runningThreads;
|
|
InstrumentationTracker instrumentationTracker;
|
|
List<string> 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<object, State>();
|
|
this.runningThreads = new Dictionary<int, Stack<Activity>>();
|
|
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<string, byte[]> checksumCache = null;
|
|
try
|
|
{
|
|
byte[] checksum;
|
|
Dictionary<object, SourceLocation> sourceLocations = SourceLocationProvider.GetSourceLocations(activity, out sourcePath, out isTemporaryFile, out checksum);
|
|
if (checksum != null)
|
|
{
|
|
checksumCache = new Dictionary<string, byte[]>();
|
|
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<Activity> 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<string>();
|
|
}
|
|
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<KeyValuePair<Activity, Activity>> pairsRemaining = new Queue<KeyValuePair<Activity, Activity>>();
|
|
|
|
pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(rootActivity1, rootActivity2));
|
|
HashSet<Activity> visited = new HashSet<Activity>();
|
|
KeyValuePair<Activity, Activity> 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<Activity> enumerator1 = WorkflowInspectionServices.GetActivities(activity1).GetEnumerator();
|
|
IEnumerator<Activity> 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<Activity, Activity>(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<object, SourceLocation> sourceLocations, string typeNamePrefix, Dictionary<string, byte[]> checksumCache)
|
|
{
|
|
Queue<KeyValuePair<Activity, string>> pairsRemaining = new Queue<KeyValuePair<Activity, string>>();
|
|
|
|
string name;
|
|
Activity activity = rootActivity;
|
|
KeyValuePair<Activity, string> pair = new KeyValuePair<Activity, string>(activity, string.Empty);
|
|
pairsRemaining.Enqueue(pair);
|
|
HashSet<string> existingNames = new HashSet<string>();
|
|
HashSet<Activity> visited = new HashSet<Activity>();
|
|
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<Activity, string>(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<string, object> 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<string, object> GenerateLocals(ActivityInstance instance)
|
|
{
|
|
Dictionary<string, object> locals = new Dictionary<string, object>();
|
|
locals.Add("debugInfo", new DebugInfo(instance));
|
|
return locals;
|
|
}
|
|
|
|
void Push(int threadId, Activity activity)
|
|
{
|
|
((Stack<Activity>)this.runningThreads[threadId]).Push(activity);
|
|
}
|
|
|
|
void Pop(int threadId)
|
|
{
|
|
Stack<Activity> 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<int, Stack<Activity>> entry in this.runningThreads)
|
|
{
|
|
Stack<Activity> threadStack = entry.Value;
|
|
if (threadStack.Peek() == activity)
|
|
{
|
|
threadId = entry.Key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (threadId < 0 && !strict)
|
|
{
|
|
foreach (KeyValuePair<int, Stack<Activity>> entry in this.runningThreads)
|
|
{
|
|
Stack<Activity> 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<ActivityInstance> ancestors = null;
|
|
|
|
if (!this.DebugStartedAtRoot)
|
|
{
|
|
ancestors = new Stack<ActivityInstance>();
|
|
|
|
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<Activity> newStack = new Stack<Activity>();
|
|
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<ActivityInstance> 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;
|
|
}
|
|
}
|
|
}
|