//----------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- namespace System.Activities.Debugger { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.SymbolStore; using System.Globalization; using System.Reflection; using System.Reflection.Emit; using System.Runtime; using System.Security; using System.Security.Permissions; using System.Activities.Debugger.Symbol; // Manager for supporting debugging a state machine. // The general usage is to call: // - DefineState() for each state // - Bake() once you've defined all the states you need to enter. // - EnterState() / LeaveState() for each state. // You can Define new states and bake them, such as if the script loads a new file. // Baking is expensive, so it's best to define as many states in each batch. [DebuggerNonUserCode] // This class need not serialized. [Fx.Tag.XamlVisible(false)] public sealed class StateManager : IDisposable { static readonly Guid WorkflowLanguageGuid = new Guid("1F1149BB-9732-4EB8-9ED4-FA738768919C"); static readonly LocalsItemDescription[] debugInfoDescriptions = new LocalsItemDescription[] { new LocalsItemDescription("debugInfo", typeof(DebugInfo)) }; static Type threadWorkerControllerType = typeof(ThreadWorkerController); static MethodInfo islandWorkerMethodInfo = threadWorkerControllerType.GetMethod("IslandWorker", BindingFlags.Static | BindingFlags.Public); internal const string MethodWithPrimingPrefix = "_"; List threads; DynamicModuleManager dynamicModuleManager; // Don't expose this, because that would expose the setters. Changing the properties // after baking types has undefined semantics and would be confusing to the user. Properties properties; bool debugStartedAtRoot; // Simple default constructor. internal StateManager() : this(new Properties(), true, null) { } // Constructor. // Properties must be set at creation time. internal StateManager(Properties properties, bool debugStartedAtRoot, DynamicModuleManager dynamicModuleManager) { this.properties = properties; this.debugStartedAtRoot = debugStartedAtRoot; this.threads = new List(); if (dynamicModuleManager == null) { dynamicModuleManager = new DynamicModuleManager(this.properties.ModuleNamePrefix); } this.dynamicModuleManager = dynamicModuleManager; } internal Properties ManagerProperties { get { return this.properties; } } internal bool IsPriming { get; set; } // Whether debugging is started at the root workflow (contrast to attaching in the middle // of a running workflow. internal bool DebugStartedAtRoot { get { return this.debugStartedAtRoot; } } // Declare a new state associated with the given source location. // States should have disjoint source locations. // location is Source location associated with this state. // This returns a state object, which can be passed to EnterState. internal State DefineState(SourceLocation location) { return DefineState(location, string.Empty, null, 0); } internal State DefineState(SourceLocation location, string name) { return DefineState(location, name, null, 0); } internal State DefineState(SourceLocation location, string name, LocalsItemDescription[] earlyLocals, int numberOfEarlyLocals) { return this.dynamicModuleManager.DefineState(location, name, earlyLocals, numberOfEarlyLocals); } internal State DefineStateWithDebugInfo(SourceLocation location, string name) { return DefineState(location, name, debugInfoDescriptions, debugInfoDescriptions.Length); } // Bake all states using the default type namespace. // States must be baked before calling EnterState(). internal void Bake() { Bake(this.properties.TypeNamePrefix, null); } // Bake all newly defined states. States must be baked before calling EnterState(). // typeName is the type name that the islands are contained in. This // may show up on the callstack. If this is not unique, it will be appended with a unique // identifier. internal void Bake(string typeName, Dictionary checksumCache) { this.dynamicModuleManager.Bake(typeName, this.properties.TypeNamePrefix, checksumCache); } internal int CreateLogicalThread(string threadName) { int threadId = -1; // Reuse thread if exists // Start from 1 since main thread never disposed earlier. for (int i = 1; i < this.threads.Count; ++i) { if (this.threads[i] == null) { this.threads[i] = new LogicalThread(i, threadName, this); threadId = i; break; } } // If can't reuse old thread. if (threadId < 0) { threadId = this.threads.Count; this.threads.Add(new LogicalThread(threadId, threadName, this)); } return threadId; } // Push the state onto the virtual callstack, with no locals. // State is the state to push onto stack. //internal void EnterState(int threadIndex, State state) //{ // this.EnterState(threadIndex, state, null); //} // Enter a state and push it onto the 'virtual callstack'. // If the user set a a breakpoint at the source location associated with // this state, this call will hit that breakpoint. // Call LeaveState when the interpretter is finished with this state. // // State is state to enter. // "locals" is local variables (both early-bound and late-bound) associated with this state. // Early-bound locals match by name with the set passed into DefineState. // Late-bound will be displayed read-only to the user in the watch window. // // EnterState can be called reentrantly. If code calls Enter(A); Enter(B); Enter(C); // Then on the call to Enter(C), the virtual callstack will be A-->B-->C. // Each call to Enter() will rebuild the virtual callstack. // internal void EnterState(int threadIndex, State state, IDictionary locals) { this.EnterState(threadIndex, new VirtualStackFrame(state, locals)); } // Enter a state and push it onto the 'virtual callstack'. // Stackframe describing state to enter, along with the // locals in that state. internal void EnterState(int threadIndex, VirtualStackFrame stackFrame) { Fx.Assert(threadIndex < this.threads.Count, "Index out of range for thread"); Fx.Assert(this.threads[threadIndex] != null, "LogicalThread is null"); this.threads[threadIndex].EnterState(stackFrame); } // Pop the state most recently pushed by EnterState. internal void LeaveState(int threadIndex, State state) { Fx.Assert(threadIndex < this.threads.Count, "Index out of range for thread"); Fx.Assert(this.threads[threadIndex] != null, "LogicalThread is null"); this.threads[threadIndex].LeaveState(state); } // Common helper to invoke an Stack frame. // This handles marshaling the args. // islandArguments - arbitrary argument passed ot the islands. // [DebuggerHidden] internal void InvokeWorker(object islandArguments, VirtualStackFrame stackFrame) { State state = stackFrame.State; if (!state.DebuggingEnabled) { // We need to short circuit and call IslandWorker because that is what the generated code // would have done, if we had generated it. This causes the thread to finish up. ThreadWorkerController.IslandWorker((ThreadWorkerController)islandArguments); return; } MethodInfo methodInfo = this.dynamicModuleManager.GetIsland(state, this.IsPriming); IDictionary allLocals = stackFrame.Locals; // Package up the raw arguments array. const int numberOfBaseArguments = 2; int numberOfEarlyLocals = state.NumberOfEarlyLocals; object[] arguments = new object[numberOfBaseArguments + numberOfEarlyLocals]; // +1 for IslandArguments and +1 for IsPriming arguments[0] = this.IsPriming; arguments[1] = islandArguments; if (numberOfEarlyLocals > 0) { int i = numberOfBaseArguments; foreach (LocalsItemDescription localsItemDescription in state.EarlyLocals) { string name = localsItemDescription.Name; object value; if (allLocals.TryGetValue(name, out value)) { // We could assert that val.GetType() is assignable to localsItemDescription.Type. // MethodInfo invoke will check this anyways; but we could check // it and give a better error. } else { // Local not supplied in the array! Use a default. value = Activator.CreateInstance(localsItemDescription.Type); } arguments[i] = value; i++; } } methodInfo.Invoke(null, arguments); } // Release any unmanaged resources. // This may not necessarily unload islands or dynamic modules that were created until the calling appdomain has exited. public void Dispose() { ExitThreads(); } internal void ExitThreads() { foreach (LogicalThread logicalThread in this.threads) { if (logicalThread != null) { logicalThread.Exit(); } } this.threads.Clear(); } // Release any unmanaged resources. // This may not necessarily unload islands or dynamic modules that were created until the calling appdomain has exited. public void Exit(int threadIndex) { Fx.Assert(threadIndex >= 0, "Invalid thread index"); Fx.Assert(this.threads[threadIndex] != null, "Cannot dispose null LogicalThread"); LogicalThread thread = this.threads[threadIndex]; thread.Exit(); // Null the entry on the List for future reuse. this.threads[threadIndex] = null; } // Property bag for Manager. These provide customization hooks. // All properties have valid default values. [DebuggerNonUserCode] internal class Properties { public Properties() : this("Locals", "Script", "States", "WorkflowDebuggerThread", true) { } public Properties(string defaultLocalsName, string moduleNamePrefix, string typeNamePrefix, string auxiliaryThreadName, bool breakOnStartup) { this.DefaultLocalsName = defaultLocalsName; this.ModuleNamePrefix = moduleNamePrefix; this.TypeNamePrefix = typeNamePrefix; this.AuxiliaryThreadName = auxiliaryThreadName; this.BreakOnStartup = breakOnStartup; } public string DefaultLocalsName { get; set; } // The name of the dynamic module (not including extension) that the states are emitted to. // This may show up on the callstack. // This is a prefix because there may be multiple modules for the islands. public string ModuleNamePrefix { get; set; } // Typename that states are created in. // This is a prefix because there may be multiple Types for the islands // (such as if islands are created lazily). public string TypeNamePrefix { get; set; } // If UseAuxiliaryThread is true, sets the friendly name of that thread as visible // in the debugger's window. public string AuxiliaryThreadName { get; set; } // If true, the VM issues a Debugger.Break() before entering the first state. // This can be useful for an F11 experience on startup to stop at the first state. // If this is false, then the interpreter will run until it hits a breakpoint or some // other stopping event. public bool BreakOnStartup { get; set; } } [DebuggerNonUserCode] class LogicalThread { int threadId; Stack callStack; ThreadWorkerController controller; public LogicalThread(int threadId, string threadName, StateManager stateManager) { this.threadId = threadId; this.callStack = new Stack(); this.controller = new ThreadWorkerController(); this.controller.Initialize(threadName + "." + threadId.ToString(CultureInfo.InvariantCulture), stateManager); } // Unwind call stack cleanly. void UnwindCallStack() { while (this.callStack.Count > 0) { // LeaveState will do the popping. this.LeaveState(this.callStack.Peek().State); } } public void Exit() { this.UnwindCallStack(); this.controller.Exit(); } // Enter a state and push it onto the 'virtual callstack'. // Stackframe describing state to enter, along with the // locals in that state. public void EnterState(VirtualStackFrame stackFrame) { if (stackFrame != null && stackFrame.State != null) { this.callStack.Push(stackFrame); this.controller.EnterState(stackFrame); } else { // signify "Uninstrumented call" this.callStack.Push(null); } } // Pop the state most recently pushed by EnterState. [SuppressMessage(FxCop.Category.Usage, FxCop.Rule.ReviewUnusedParameters, Justification = "Revisit for bug 36860")] public void LeaveState(State state) { if (this.callStack.Count > 0) { VirtualStackFrame stackFrame = this.callStack.Pop(); Fx.Assert( (state == null && stackFrame == null) || (stackFrame != null && stackFrame.State == state), "Unmatched LeaveState: " + ((state == null) ? "null" : state.Name) + " with top stack frame: " + ((stackFrame == null || stackFrame.State == null) ? "null" : stackFrame.State.Name)); if (stackFrame != null) // Matches with an uninstrumented Activity. { this.controller.LeaveState(); } } else { Fx.Assert("Unmatched LeaveState: " + ((state != null) ? state.Name : "null")); } } } internal class DynamicModuleManager { // List of all state that have been created with DefineState. List states; Dictionary stateMap = new Dictionary(); // Index into states array of the last set of states baked. // So Bake() will build islands for each state // { states[x], where indexLastBaked <= x < states.Length; } int indexLastBaked; // Mapping from State --> MethodInfo for that state. // This gets populated as states get baked [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic assembly.")] [SecurityCritical] Dictionary islands; [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic assembly.")] [SecurityCritical] Dictionary islandsWithPriming; [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic assembly.")] [SecurityCritical] ModuleBuilder dynamicModule; [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic assembly.")] [SecurityCritical] Dictionary sourceDocuments; [Fx.Tag.SecurityNote(Critical = "Accesses Critical members and calling Critical method InitDynamicModule.", Safe = "We are only creating empty dictionaries, not populating them. And we are validating the provided moduleNamePrefix in partial trust.")] [SecuritySafeCritical] public DynamicModuleManager(string moduleNamePrefix) { this.states = new List(); this.islands = new Dictionary(); this.islandsWithPriming = new Dictionary(); this.sourceDocuments = new Dictionary(); if (!PartialTrustHelpers.AppDomainFullyTrusted) { moduleNamePrefix = State.ValidateIdentifierString(moduleNamePrefix); } InitDynamicModule(moduleNamePrefix); } public State DefineState(SourceLocation location, string name, LocalsItemDescription[] earlyLocals, int numberOfEarlyLocals) { State state; lock (this) { if (!this.stateMap.TryGetValue(location, out state)) { lock (this) { state = new State(location, name, earlyLocals, numberOfEarlyLocals); this.stateMap.Add(location, state); this.states.Add(state); } } } return state; } // Bake all newly defined states. States must be baked before calling EnterState(). // typeName is the type name that the islands are contained in. This // may show up on the callstack. If this is not unique, it will be appended with a unique // identifier. [Fx.Tag.SecurityNote(Critical = "Accesses Critical members and invoking Critical methods.", Safe = "We validating the input strings - typeName and typeNamePrefix - and the checksum values in the checksumCache.")] [SecuritySafeCritical] public void Bake(string typeName, string typeNamePrefix, Dictionary checksumCache) { // In partial trust, validate the typeName and typeNamePrefix. if (!PartialTrustHelpers.AppDomainFullyTrusted) { typeName = State.ValidateIdentifierString(typeName); typeNamePrefix = State.ValidateIdentifierString(typeNamePrefix); if (checksumCache != null) { bool nullifyChecksumCache = false; foreach (KeyValuePair kvpair in checksumCache) { // We use an MD5 hash for the checksum, so the byte array should be 16 elements long. if (!SymbolHelper.ValidateChecksum(kvpair.Value)) { nullifyChecksumCache = true; Trace.WriteLine(SR.DebugSymbolChecksumValueInvalid); break; } } // If we found an invalid checksum, just don't use the cache. if (nullifyChecksumCache) { checksumCache = null; } } } lock (this) { if (this.indexLastBaked < this.states.Count) // there are newly created states. { // Ensure typename is unique. Append a number if needed. int suffix = 1; while (this.dynamicModule.GetType(typeName) != null) { typeName = typeNamePrefix + "_" + suffix.ToString(CultureInfo.InvariantCulture); ++suffix; } TypeBuilder typeBuilder = this.dynamicModule.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class); for (int i = indexLastBaked; i < this.states.Count; i++) { // Only create the island if debugging is enabled for the state. if (this.states[i].DebuggingEnabled) { MethodBuilder methodBuilder = this.CreateIsland(typeBuilder, this.states[i], false, checksumCache); Fx.Assert(methodBuilder != null, "CreateIsland call should have succeeded"); // Always generate method with priming, for the following scenario: // 1. Start debugging a workflow inside VS, workflow debug session 1 starts (debugStartedAtRoot = true, instrumentation is done) // 2. Workflow persisted, workflow debug session 1 ends // 3. Workflow continued, workflow debug session 2 starts (debugStartedAtRoot = false, instrumentation is skipped because the static dynamicModuleManager is being reused and the instrumentation is done) // 4. PrimeCallStack is called to rebuild the call stack // 5. NullReferenceException will be thrown if MethodInfo with prime is not available MethodBuilder methodBuilderWithPriming = this.CreateIsland(typeBuilder, this.states[i], true, checksumCache); Fx.Assert(methodBuilderWithPriming != null, "CreateIsland call should have succeeded"); // Save information needed to call Type.GetMethod() later. this.states[i].CacheMethodInfo(typeBuilder, methodBuilder.Name); } } // Actual baking. typeBuilder.CreateType(); // Calling Type.GetMethod() is slow (10,000 calls can take ~1 minute). // So defer that to later. this.indexLastBaked = this.states.Count; } } } [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic module.")] [SecurityCritical] internal MethodBuilder CreateMethodBuilder(TypeBuilder typeBuilder, Type typeIslandArguments, State state, bool withPriming) { // create the method string methodName = (state.Name != null ? state.Name : ("Line_" + state.Location.StartLine)); if (withPriming) { methodName = MethodWithPrimingPrefix + methodName; } // Parameters to the islands: // 1. Args // 2. IDict of late-bound locals. // 3 ... N. list of early bound locals. const int numberOfBaseArguments = 2; IEnumerable earlyLocals = state.EarlyLocals; int numberOfEarlyLocals = state.NumberOfEarlyLocals; Type[] parameterTypes = new Type[numberOfBaseArguments + numberOfEarlyLocals]; parameterTypes[0] = typeof(bool); parameterTypes[1] = typeIslandArguments; if (numberOfEarlyLocals > 0) { int i = numberOfBaseArguments; foreach (LocalsItemDescription localsItemDescription in earlyLocals) { parameterTypes[i] = localsItemDescription.Type; i++; } } Type returnType = typeof(void); MethodBuilder methodbuilder = typeBuilder.DefineMethod( methodName, MethodAttributes.Static | MethodAttributes.Public, returnType, parameterTypes); // Need to define parameter here, otherwise EE cannot get the correct IDebugContainerField // for debugInfo. methodbuilder.DefineParameter(1, ParameterAttributes.None, "isPriming"); methodbuilder.DefineParameter(2, ParameterAttributes.None, "typeIslandArguments"); // Define the parameter names // Note that we can hide implementation-specific arguments from VS by not defining parameter // info for them. Eg., the StepInTarget argument doesn't appear to show up in VS at all. if (numberOfEarlyLocals > 0) { int i = numberOfBaseArguments + 1; foreach (LocalsItemDescription localsItemDescription in earlyLocals) { methodbuilder.DefineParameter(i, ParameterAttributes.None, localsItemDescription.Name); i++; } } return methodbuilder; } [Fx.Tag.InheritThrows(From = "GetILGenerator", FromDeclaringType = typeof(MethodBuilder))] [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic module.")] [SecurityCritical] MethodBuilder CreateIsland(TypeBuilder typeBuilder, State state, bool withPrimingTest, DictionarychecksumCache) { MethodBuilder methodbuilder = this.CreateMethodBuilder(typeBuilder, threadWorkerControllerType, state, withPrimingTest); ILGenerator ilGenerator = methodbuilder.GetILGenerator(); const int lineHidden = 0xFeeFee; // #line hidden directive // Island: // void MethodName(Manager m) // { // .line // nop // call Worker(m) // ret; // } SourceLocation stateLocation = state.Location; ISymbolDocumentWriter document = this.GetSourceDocument(stateLocation.FileName, stateLocation.Checksum, checksumCache); Label islandWorkerLabel = ilGenerator.DefineLabel(); // Hide all the opcodes before the real source line. // This is needed for Island which is called during priming (Attach to Process): // It should skip the line directive during priming, thus it won't break at user's // breakpoints at the beginning during priming the callstack. if (withPrimingTest) { ilGenerator.MarkSequencePoint(document, lineHidden, 1, lineHidden, 100); ilGenerator.Emit(OpCodes.Ldarg_0); ilGenerator.Emit(OpCodes.Brtrue, islandWorkerLabel); } // Emit sequence point before the IL instructions to map it to a source location. ilGenerator.MarkSequencePoint(document, stateLocation.StartLine, stateLocation.StartColumn, stateLocation.EndLine, stateLocation.EndColumn); ilGenerator.Emit(OpCodes.Nop); ilGenerator.MarkLabel(islandWorkerLabel); ilGenerator.Emit(OpCodes.Ldarg_1); ilGenerator.EmitCall(OpCodes.Call, islandWorkerMethodInfo, null); ilGenerator.Emit(OpCodes.Ret); return methodbuilder; } [SuppressMessage(FxCop.Category.Security, FxCop.Rule.SecureAsserts, Justification = "The validations of the user input are done elsewhere.")] [Fx.Tag.SecurityNote(Critical = "Because we Assert UnmanagedCode in order to be able to emit symbols.")] [SecurityCritical] void InitDynamicModule(string asmName) { // See http://blogs.msdn.com/Microsoft/archive/2005/02/03/366429.aspx for a simple example // of debuggable reflection-emit. Fx.Assert(dynamicModule == null, "can only be initialized once"); // create a dynamic assembly and module AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = asmName; AssemblyBuilder assemblyBuilder; // The temporary assembly needs to be Transparent. ConstructorInfo transparentCtor = typeof(SecurityTransparentAttribute).GetConstructor( Type.EmptyTypes); CustomAttributeBuilder transparent = new CustomAttributeBuilder( transparentCtor, new Object[] { }); assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run, null, true, new CustomAttributeBuilder[] { transparent }); // Mark generated code as debuggable. // See http://blogs.msdn.com/rmbyers/archive/2005/06/26/432922.aspx for explanation. Type debuggableAttributeType = typeof(DebuggableAttribute); ConstructorInfo constructorInfo = debuggableAttributeType.GetConstructor(new Type[] { typeof(DebuggableAttribute.DebuggingModes) }); CustomAttributeBuilder builder = new CustomAttributeBuilder(constructorInfo, new object[] { DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.Default }); assemblyBuilder.SetCustomAttribute(builder); // We need UnmanagedCode permissions because we are asking for Symbols to be emitted. // We are protecting the dynamicModule so that only Critical code modifies it. PermissionSet unmanagedCodePermissionSet = new PermissionSet(PermissionState.None); unmanagedCodePermissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.UnmanagedCode)); unmanagedCodePermissionSet.Assert(); try { dynamicModule = assemblyBuilder.DefineDynamicModule(asmName, true); // <-- pass 'true' to track debug info. } finally { CodeAccessPermission.RevertAssert(); } } [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic module.", Safe = "State validates its SecurityCritical members itself")] [SecuritySafeCritical] public MethodInfo GetIsland(State state, bool isPriming) { MethodInfo island = null; if (isPriming) { lock (islandsWithPriming) { if (!islandsWithPriming.TryGetValue(state, out island)) { island = state.GetMethodInfo(true); islandsWithPriming[state] = island; } } } else { lock (islands) { if (!islands.TryGetValue(state, out island)) { island = state.GetMethodInfo(false); islands[state] = island; } } } return island; } // This method is only called from CreateIsland, which is only called from Bake. // Bake does a "lock(this)" before calling CreateIsland, so access to the sourceDocuments // dictionary is protected by that lock. If this changes, locking will need to be added // to this method to protect the sourceDocuments dictionary. [Fx.Tag.SecurityNote(Critical = "Used in generating the dynamic module.")] [SecurityCritical] private ISymbolDocumentWriter GetSourceDocument(string fileName, byte[] checksum, Dictionary checksumCache) { ISymbolDocumentWriter documentWriter; string sourceDocKey = fileName + SymbolHelper.GetHexStringFromChecksum(checksum); if (!this.sourceDocuments.TryGetValue(sourceDocKey, out documentWriter)) { documentWriter = dynamicModule.DefineDocument( fileName, StateManager.WorkflowLanguageGuid, SymLanguageVendor.Microsoft, SymDocumentType.Text); this.sourceDocuments.Add(sourceDocKey, documentWriter); byte[] checksumBytes; if (checksumCache == null || !checksumCache.TryGetValue(fileName.ToUpperInvariant(), out checksumBytes)) { checksumBytes = SymbolHelper.CalculateChecksum(fileName); } if (checksumBytes != null) { documentWriter.SetCheckSum(SymbolHelper.ChecksumProviderId, checksumBytes); } } return documentWriter; } } } }