1 //-----------------------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation.  All rights reserved.
3 //-----------------------------------------------------------------------------
4 namespace System.Activities.Debugger
5 {
6     using System;
7     using System.Activities.Expressions;
8     using System.Activities.Hosting;
9     using System.Activities.Runtime;
10     using System.Activities.Statements;
11     using System.Activities.XamlIntegration;
12     using System.Collections.Generic;
13     using System.Diagnostics;
14     using System.Diagnostics.CodeAnalysis;
15     using System.Globalization;
16     using System.IO;
17     using System.Linq;
18     using System.Linq.Expressions;
19     using System.Runtime;
20 
21     [DebuggerNonUserCode]
22     class DebugManager
23     {
24         static StateManager.DynamicModuleManager dynamicModuleManager;
25         WorkflowInstance host;
26         StateManager stateManager;
27         Dictionary<object, State> states;
28         Dictionary<int, Stack<Activity>> runningThreads;
29         InstrumentationTracker instrumentationTracker;
30         List<string> temporaryFiles;
31 
DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup, WorkflowInstance host, bool debugStartedAtRoot)32         public DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup,
33             WorkflowInstance host, bool debugStartedAtRoot) :
34             this(root, moduleNamePrefix, typeNamePrefix, auxiliaryThreadName, breakOnStartup, host, debugStartedAtRoot, false)
35         {
36         }
37 
DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup, WorkflowInstance host, bool debugStartedAtRoot, bool resetDynamicModule)38         internal DebugManager(Activity root, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup,
39             WorkflowInstance host, bool debugStartedAtRoot, bool resetDynamicModule)
40         {
41             if (resetDynamicModule)
42             {
43                 dynamicModuleManager = null;
44             }
45 
46             if (dynamicModuleManager == null)
47             {
48                 dynamicModuleManager = new StateManager.DynamicModuleManager(moduleNamePrefix);
49             }
50 
51             this.stateManager = new StateManager(
52                 new StateManager.Properties
53                     {
54                         ModuleNamePrefix = moduleNamePrefix,
55                         TypeNamePrefix = typeNamePrefix,
56                         AuxiliaryThreadName = auxiliaryThreadName,
57                         BreakOnStartup = breakOnStartup
58                     },
59                     debugStartedAtRoot, dynamicModuleManager);
60 
61             this.states = new Dictionary<object, State>();
62             this.runningThreads = new Dictionary<int, Stack<Activity>>();
63             this.instrumentationTracker = new InstrumentationTracker(root);
64             this.host = host;
65         }
66 
67         // Whether we're priming the background thread (in Attach To Process case).
68         public bool IsPriming
69         {
70             set { this.stateManager.IsPriming = value; }
71         }
72 
73         // Whether debugging is done from the start of the root workflow,
74         // contrast to attaching into the middle of a running workflow.
75         bool DebugStartedAtRoot
76         {
77             get
78             {
79                 return this.stateManager.DebugStartedAtRoot;
80             }
81         }
82 
Instrument(Activity activity)83         internal void Instrument(Activity activity)
84         {
85             bool isTemporaryFile = false;
86             string sourcePath = null;
87             bool instrumentationFailed = false;
88             Dictionary<string, byte[]> checksumCache = null;
89             try
90             {
91                 byte[] checksum;
92                 Dictionary<object, SourceLocation> sourceLocations = SourceLocationProvider.GetSourceLocations(activity, out sourcePath, out isTemporaryFile, out checksum);
93                 if (checksum != null)
94                 {
95                     checksumCache = new Dictionary<string, byte[]>();
96                     checksumCache.Add(sourcePath.ToUpperInvariant(), checksum);
97                 }
98                 Instrument(activity, sourceLocations, Path.GetFileNameWithoutExtension(sourcePath), checksumCache);
99             }
100             catch (Exception ex)
101             {
102                 instrumentationFailed = true;
103                 Trace.WriteLine(SR.DebugInstrumentationFailed(ex.Message));
104 
105                 if (Fx.IsFatal(ex))
106                 {
107                     throw;
108                 }
109             }
110 
111             List<Activity> sameSourceActivities = this.instrumentationTracker.GetSameSourceSubRoots(activity);
112             this.instrumentationTracker.MarkInstrumented(activity);
113 
114             foreach (Activity sameSourceActivity in sameSourceActivities)
115             {
116                 if (!instrumentationFailed)
117                 {
118                     MapInstrumentationStates(activity, sameSourceActivity);
119                 }
120                 // Mark it as instrumentated, even though it fails so it won't be
121                 // retried.
122                 this.instrumentationTracker.MarkInstrumented(sameSourceActivity);
123             }
124 
125             if (isTemporaryFile)
126             {
127                 if (this.temporaryFiles == null)
128                 {
129                     this.temporaryFiles = new List<string>();
130                 }
131                 Fx.Assert(!string.IsNullOrEmpty(sourcePath), "SourcePath cannot be null for temporary file");
132                 this.temporaryFiles.Add(sourcePath);
133             }
134         }
135 
136         // Workflow rooted at rootActivity1 and rootActivity2 have same source file, but they
137         // are two different instantiation.
138         // rootActivity1 has been instrumented and its instrumentation states can be
139         // re-used by rootActivity2.
140         //
141         // MapInstrumentationStates will walk both Workflow trees in parallel and map every
142         // state for activities in rootActivity1 to corresponding activities in rootActivity2.
MapInstrumentationStates(Activity rootActivity1, Activity rootActivity2)143         void MapInstrumentationStates(Activity rootActivity1, Activity rootActivity2)
144         {
145             Queue<KeyValuePair<Activity, Activity>> pairsRemaining = new Queue<KeyValuePair<Activity, Activity>>();
146 
147             pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(rootActivity1, rootActivity2));
148             HashSet<Activity> visited = new HashSet<Activity>();
149             KeyValuePair<Activity, Activity> currentPair;
150             State state;
151 
152             while (pairsRemaining.Count > 0)
153             {
154                 currentPair = pairsRemaining.Dequeue();
155                 Activity activity1 = currentPair.Key;
156                 Activity activity2 = currentPair.Value;
157 
158                 if (this.states.TryGetValue(activity1, out state))
159                 {
160                     if (this.states.ContainsKey(activity2))
161                     {
162                         Trace.WriteLine("Workflow", SR.DuplicateInstrumentation(activity2.DisplayName));
163                     }
164                     else
165                     {
166                         // Map activity2 to the same state.
167                         this.states.Add(activity2, state);
168                     }
169                 }
170                 //Some activities may not have corresponding Xaml node, e.g. ActivityFaultedOutput.
171 
172                 visited.Add(activity1);
173 
174                 // This to avoid comparing any value expression with DesignTimeValueExpression (in designer case).
175                 IEnumerator<Activity> enumerator1 = WorkflowInspectionServices.GetActivities(activity1).GetEnumerator();
176                 IEnumerator<Activity> enumerator2 = WorkflowInspectionServices.GetActivities(activity2).GetEnumerator();
177 
178                 bool hasNextItem1 = enumerator1.MoveNext();
179                 bool hasNextItem2 = enumerator2.MoveNext();
180 
181                 while (hasNextItem1 && hasNextItem2)
182                 {
183                     if (!visited.Contains(enumerator1.Current))  // avoid adding the same activity (e.g. some default implementation).
184                     {
185                         if (enumerator1.Current.GetType() != enumerator2.Current.GetType())
186                         {
187                             // Give debugger log instead of just asserting; to help user find out mismatch problem.
188                             Trace.WriteLine(
189                                 "Unmatched type: " + enumerator1.Current.GetType().FullName +
190                                 " vs " + enumerator2.Current.GetType().FullName + "\n");
191                         }
192                         pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(enumerator1.Current, enumerator2.Current));
193                     }
194 
195                     hasNextItem1 = enumerator1.MoveNext();
196                     hasNextItem2 = enumerator2.MoveNext();
197                 }
198 
199                 // If enumerators do not finish at the same time, then they have unmatched number of activities.
200                 // Give debugger log instead of just asserting; to help user find out mismatch problem.
201                 if (hasNextItem1 || hasNextItem2)
202                 {
203                     Trace.WriteLine("Workflow", "Unmatched number of children\n");
204                 }
205             }
206         }
207 
208 
209         // Main instrumentation.
210         // Currently the typeNamePrefix is used to notify the Designer of which file to show.
211         // This will no longer necessary when the callstack API can give us the source line
212         // information.
Instrument(Activity rootActivity, Dictionary<object, SourceLocation> sourceLocations, string typeNamePrefix, Dictionary<string, byte[]> checksumCache)213         public void Instrument(Activity rootActivity, Dictionary<object, SourceLocation> sourceLocations, string typeNamePrefix, Dictionary<string, byte[]> checksumCache)
214         {
215             Queue<KeyValuePair<Activity, string>> pairsRemaining = new Queue<KeyValuePair<Activity, string>>();
216 
217             string name;
218             Activity activity = rootActivity;
219             KeyValuePair<Activity, string> pair = new KeyValuePair<Activity, string>(activity, string.Empty);
220             pairsRemaining.Enqueue(pair);
221             HashSet<string> existingNames = new HashSet<string>();
222             HashSet<Activity> visited = new HashSet<Activity>();
223             SourceLocation sourceLocation;
224 
225             while (pairsRemaining.Count > 0)
226             {
227                 pair = pairsRemaining.Dequeue();
228                 activity = pair.Key;
229                 string parentName = pair.Value;
230                 string displayName = activity.DisplayName;
231 
232                 // If no DisplayName, then use the type name.
233                 if (string.IsNullOrEmpty(displayName))
234                 {
235                     displayName = activity.GetType().Name;
236                 }
237 
238                 if (parentName == string.Empty)
239                 {   // the root
240                     name = displayName;
241                 }
242                 else
243                 {
244                     name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", parentName, displayName);
245                 }
246 
247                 int i = 0;
248                 while (existingNames.Contains(name))
249                 {
250                     ++i;
251                     name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}{2}", parentName, displayName, i.ToString(CultureInfo.InvariantCulture));
252                 }
253 
254                 existingNames.Add(name);
255 
256                 visited.Add(activity);
257 
258                 if (sourceLocations.TryGetValue(activity, out sourceLocation))
259                 {
260                     object[] objects = activity.GetType().GetCustomAttributes(typeof(DebuggerStepThroughAttribute), false);
261                     if ((objects == null || objects.Length == 0))
262                     {
263                         Instrument(activity, sourceLocation, name);
264                     }
265                 }
266 
267                 foreach (Activity childActivity in WorkflowInspectionServices.GetActivities(activity))
268                 {
269                     if (!visited.Contains(childActivity))
270                     {
271                         pairsRemaining.Enqueue(new KeyValuePair<Activity, string>(childActivity, name));
272                     }
273                 }
274             }
275             this.stateManager.Bake(typeNamePrefix, checksumCache);
276         }
277 
278         // Exiting the DebugManager.
279         // Delete all temporary files
Exit()280         public void Exit()
281         {
282             if (this.temporaryFiles != null)
283             {
284                 foreach (string temporaryFile in this.temporaryFiles)
285                 {
286                     // Clean up published source.
287                     try
288                     {
289                         File.Delete(temporaryFile);
290                     }
291                     catch (IOException)
292                     {
293                         // ---- IOException silently.
294                     }
295                     this.temporaryFiles = null;
296                 }
297             }
298             this.stateManager.ExitThreads(); // State manager is still keep for the session in SessionStateManager
299             this.stateManager = null;
300         }
301 
Instrument(Activity activity, SourceLocation sourceLocation, string name)302         void Instrument(Activity activity, SourceLocation sourceLocation, string name)
303         {
304             Fx.Assert(activity != null, "activity can't be null");
305             Fx.Assert(sourceLocation != null, "sourceLocation can't be null");
306             if (this.states.ContainsKey(activity))
307             {
308                 Trace.WriteLine(SR.DuplicateInstrumentation(activity.DisplayName));
309             }
310             else
311             {
312                 State activityState = this.stateManager.DefineStateWithDebugInfo(sourceLocation, name);
313                 this.states.Add(activity, activityState);
314             }
315         }
316 
317         // Test whether activity has been instrumented.
318         // If not, try to instrument it.
319         // It will return true if instrumentation is already done or
320         // instrumentation is succesful.  False otherwise.
EnsureInstrumented(Activity activity)321         bool EnsureInstrumented(Activity activity)
322         {
323             // This is the most common case, we will find the instrumentation.
324             if (this.states.ContainsKey(activity))
325             {
326                 return true;
327             }
328 
329             // No states correspond to this yet.
330             if (this.instrumentationTracker.IsUninstrumentedSubRoot(activity))
331             {
332                 Instrument(activity);
333                 return this.states.ContainsKey(activity);
334             }
335             else
336             {
337                 return false;
338             }
339         }
340 
341         // Primitive EnterState
EnterState(int threadId, Activity activity, Dictionary<string, object> locals)342         void EnterState(int threadId, Activity activity, Dictionary<string, object> locals)
343         {
344             Fx.Assert(activity != null, "activity cannot be null");
345             this.Push(threadId, activity);
346 
347             State activityState;
348             if (this.states.TryGetValue(activity, out activityState))
349             {
350                 this.stateManager.EnterState(threadId, activityState, locals);
351             }
352             else
353             {
354                 Fx.Assert(false, "Uninstrumented activity is disallowed: " + activity.DisplayName);
355             }
356         }
OnEnterState(ActivityInstance instance)357         public void OnEnterState(ActivityInstance instance)
358         {
359             Fx.Assert(instance != null, "ActivityInstance cannot be null");
360             Activity activity = instance.Activity;
361 
362             if (this.EnsureInstrumented(activity))
363             {
364                 this.EnterState(GetOrCreateThreadId(activity, instance), activity, GenerateLocals(instance));
365             }
366         }
367 
368         [SuppressMessage(FxCop.Category.Usage, FxCop.Rule.ReviewUnusedParameters)]
OnEnterState(Activity expression, ActivityInstance instance, LocationEnvironment environment)369         public void OnEnterState(Activity expression, ActivityInstance instance, LocationEnvironment environment)
370         {
371             if (this.EnsureInstrumented(expression))
372             {
373                 this.EnterState(GetOrCreateThreadId(expression, instance), expression, GenerateLocals(instance));
374             }
375         }
376 
LeaveState(Activity activity)377         void LeaveState(Activity activity)
378         {
379             Fx.Assert(activity != null, "Activity cannot be null");
380             int threadId = GetExecutingThreadId(activity, true);
381 
382             // If debugging was not started from the root, then threadId should not be < 0.
383             Fx.Assert(!this.DebugStartedAtRoot || threadId >= 0, "Leaving from an unknown state");
384 
385             if (threadId >= 0)
386             {
387                 State activityState;
388                 if (this.states.TryGetValue(activity, out activityState))
389                 {
390                     this.stateManager.LeaveState(threadId, activityState);
391                 }
392                 else
393                 {
394                     Fx.Assert(false, "Uninstrumented activity is disallowed: " + activity.DisplayName);
395                 }
396                 this.Pop(threadId);
397             }
398         }
399 
OnLeaveState(ActivityInstance activityInstance)400         public void OnLeaveState(ActivityInstance activityInstance)
401         {
402             Fx.Assert(activityInstance != null, "ActivityInstance cannot be null");
403             if (this.EnsureInstrumented(activityInstance.Activity))
404             {
405                 this.LeaveState(activityInstance.Activity);
406             }
407         }
408 
GenerateLocals(ActivityInstance instance)409         static Dictionary<string, object> GenerateLocals(ActivityInstance instance)
410         {
411             Dictionary<string, object> locals = new Dictionary<string, object>();
412             locals.Add("debugInfo", new DebugInfo(instance));
413             return locals;
414         }
415 
Push(int threadId, Activity activity)416         void Push(int threadId, Activity activity)
417         {
418             ((Stack<Activity>)this.runningThreads[threadId]).Push(activity);
419         }
420 
Pop(int threadId)421         void Pop(int threadId)
422         {
423             Stack<Activity> stack = this.runningThreads[threadId];
424             stack.Pop();
425             if (stack.Count == 0)
426             {
427                 this.stateManager.Exit(threadId);
428                 this.runningThreads.Remove(threadId);
429             }
430         }
431 
432         // Given an activity, return the thread id where it is currently
433         // executed (on the top of the callstack).
434         // Boolean "strict" parameter determine whether the activity itself should
435         // be on top of the stack.
436         // Strict checking is needed in the case of "Leave"-ing a state.
437         // Non-strict checking is needed for "Enter"-ing a state, since the direct parent of
438         // the activity may not yet be executed (e.g. the activity is an argument of another activity,
439         // the activity is "enter"-ed even though the direct parent is not yet "enter"-ed.
GetExecutingThreadId(Activity activity, bool strict)440         int GetExecutingThreadId(Activity activity, bool strict)
441         {
442             int threadId = -1;
443 
444             foreach (KeyValuePair<int, Stack<Activity>> entry in this.runningThreads)
445             {
446                 Stack<Activity> threadStack = entry.Value;
447                 if (threadStack.Peek() == activity)
448                 {
449                     threadId = entry.Key;
450                     break;
451                 }
452             }
453 
454             if (threadId < 0 && !strict)
455             {
456                 foreach (KeyValuePair<int, Stack<Activity>> entry in this.runningThreads)
457                 {
458                     Stack<Activity> threadStack = entry.Value;
459                     Activity topActivity = threadStack.Peek();
460                     if (!IsParallelActivity(topActivity) && IsAncestorOf(threadStack.Peek(), activity))
461                     {
462                         threadId = entry.Key;
463                         break;
464                     }
465                 }
466             }
467             return threadId;
468         }
469 
IsAncestorOf(Activity ancestorActivity, Activity activity)470         static bool IsAncestorOf(Activity ancestorActivity, Activity activity)
471         {
472             Fx.Assert(activity != null, "IsAncestorOf: Cannot pass null as activity");
473             Fx.Assert(ancestorActivity != null, "IsAncestorOf: Cannot pass null as ancestorActivity");
474             activity = activity.Parent;
475             while (activity != null && activity != ancestorActivity && !IsParallelActivity(activity))
476             {
477                 activity = activity.Parent;
478             }
479             return (activity == ancestorActivity);
480         }
481 
IsParallelActivity(Activity activity)482         static bool IsParallelActivity(Activity activity)
483         {
484             Fx.Assert(activity != null, "IsParallel: Cannot pass null as activity");
485             return activity is Parallel ||
486                     (activity.GetType().IsGenericType && activity.GetType().GetGenericTypeDefinition() == typeof(ParallelForEach<>));
487         }
488 
489         // Get threads currently executing the parent of the given activity,
490         // if none then create a new one and prep the call stack to current state.
GetOrCreateThreadId(Activity activity, ActivityInstance instance)491         int GetOrCreateThreadId(Activity activity, ActivityInstance instance)
492         {
493             int threadId = -1;
494             if (activity.Parent != null && !IsParallelActivity(activity.Parent))
495             {
496                 threadId = GetExecutingThreadId(activity.Parent, false);
497             }
498             if (threadId < 0)
499             {
500                 threadId = CreateLogicalThread(activity, instance, false);
501             }
502             return threadId;
503         }
504 
505         // Create logical thread and bring its call stack to reflect call from
506         // the root up to (but not including) the instance.
507         // If the activity is an expression though, then the call stack will also include the instance
508         // (since it is the parent of the expression).
CreateLogicalThread(Activity activity, ActivityInstance instance, bool primeCurrentInstance)509         int CreateLogicalThread(Activity activity, ActivityInstance instance, bool primeCurrentInstance)
510         {
511             Stack<ActivityInstance> ancestors = null;
512 
513             if (!this.DebugStartedAtRoot)
514             {
515                 ancestors = new Stack<ActivityInstance>();
516 
517                 if (activity != instance.Activity || primeCurrentInstance)
518                 {   // This mean that activity is an expression and
519                     // instance is the parent of this expression.
520 
521                     Fx.Assert(primeCurrentInstance || (activity is ActivityWithResult), "Expect an ActivityWithResult");
522                     Fx.Assert(primeCurrentInstance || (activity.Parent == instance.Activity), "Argument Expression is not given correct parent instance");
523                     if (primeCurrentInstance || !IsParallelActivity(instance.Activity))
524                     {
525                         ancestors.Push(instance);
526                     }
527                 }
528 
529                 ActivityInstance instanceParent = instance.Parent;
530                 while (instanceParent != null && !IsParallelActivity(instanceParent.Activity))
531                 {
532                     ancestors.Push(instanceParent);
533                     instanceParent = instanceParent.Parent;
534                 }
535 
536                 if (instanceParent != null && IsParallelActivity(instanceParent.Activity))
537                 {
538                     // Ensure thread is created for the parent (a Parallel activity).
539                     int parentThreadId = GetExecutingThreadId(instanceParent.Activity, false);
540                     if (parentThreadId < 0)
541                     {
542                         parentThreadId = CreateLogicalThread(instanceParent.Activity, instanceParent, true);
543                         Fx.Assert(parentThreadId > 0, "Parallel main thread can't be created");
544                     }
545                 }
546             }
547 
548             string threadName = "DebuggerThread:";
549             if (activity.Parent != null)
550             {
551                 threadName += activity.Parent.DisplayName;
552             }
553             else // Special case for the root of WorklowService that does not have a parent.
554             {
555                 threadName += activity.DisplayName;
556             }
557 
558             int newThreadId = this.stateManager.CreateLogicalThread(threadName);
559             Stack<Activity> newStack = new Stack<Activity>();
560             this.runningThreads.Add(newThreadId, newStack);
561 
562             if (!this.DebugStartedAtRoot && ancestors != null)
563             { // Need to create callstack to current activity.
564                 PrimeCallStack(newThreadId, ancestors);
565             }
566 
567             return newThreadId;
568         }
569 
570         // Prime the call stack to contains all the ancestors of this instance.
571         // Note: the call stack will not include the current instance.
PrimeCallStack(int threadId, Stack<ActivityInstance> ancestors)572         void PrimeCallStack(int threadId, Stack<ActivityInstance> ancestors)
573         {
574             Fx.Assert(!this.DebugStartedAtRoot, "Priming should not be called if the debugging is attached from the start of the workflow");
575             bool currentIsPrimingValue = this.stateManager.IsPriming;
576             this.stateManager.IsPriming = true;
577             while (ancestors.Count > 0)
578             {
579                 ActivityInstance currentInstance = ancestors.Pop();
580                 if (EnsureInstrumented(currentInstance.Activity))
581                 {
582                     this.EnterState(threadId, currentInstance.Activity, GenerateLocals(currentInstance));
583                 }
584             }
585             this.stateManager.IsPriming = currentIsPrimingValue;
586         }
587     }
588 }
589