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