// // SoftDebuggerSession.cs // // Authors: Lluis Sanchez Gual // Jeffrey Stedfast // // Copyright (c) 2009 Novell, Inc (http://www.novell.com) // Copyright (c) 2012 Xamarin Inc. (http://www.xamarin.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. //#define DEBUG_EVENT_QUEUEING using System; using System.IO; using System.Net; using System.Linq; using System.Text; using System.Threading; using System.Reflection; using System.Net.Sockets; using System.Globalization; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Newtonsoft.Json; using Mono.Debugger.Soft; using Mono.Debugging.Client; using Mono.Debugging.Evaluation; using StackFrame = Mono.Debugger.Soft.StackFrame; using System.Collections.Immutable; using Assembly = Mono.Debugging.Client.Assembly; namespace Mono.Debugging.Soft { public class SoftDebuggerSession : DebuggerSession { readonly Dictionary> domainAssembliesToUnload = new Dictionary> (); readonly Dictionary, MethodMirror[]> overloadResolveCache = new Dictionary, MethodMirror[]> (); readonly Dictionary> source_to_type = new Dictionary> (PathComparer); readonly Dictionary activeExceptionsByThread = new Dictionary (); readonly Dictionary breakpoints = new Dictionary (); readonly Dictionary type_to_source = new Dictionary (); readonly Dictionary> aliases = new Dictionary> (); readonly Dictionary> types = new Dictionary> (); readonly LinkedList> queuedEventSets = new LinkedList> (); readonly Dictionary localThreadIds = new Dictionary (); readonly List pending_bes = new List (); TypeLoadEventRequest typeLoadReq, typeLoadTypeNameReq; ExceptionEventRequest unhandledExceptionRequest; Dictionary assemblyPathMap; Dictionary symbolPathMap; ThreadMirror current_thread, recent_thread; List assemblyFilters; StepEventRequest currentStepRequest; IConnectionDialog connectionDialog; Thread outputReader, errorReader; bool loggedSymlinkedRuntimesBug; SoftDebuggerStartArgs startArgs; List userAssemblyNames; List sourceUpdates; ThreadInfo[] current_threads; string remoteProcessName; long currentAddress = -1; IAsyncResult connection; int currentStackDepth; ProcessInfo[] procs; Thread eventHandler; VirtualMachine vm; bool autoStepInto; bool disposed; bool started; internal int StackVersion; public SoftDebuggerAdaptor Adaptor { get; private set; } public SoftDebuggerSession () { Adaptor = CreateSoftDebuggerAdaptor (); Adaptor.BusyStateChanged += (sender, e) => SetBusyState (e); Adaptor.DebuggerSession = this; sourceUpdates = new List (); } protected virtual SoftDebuggerAdaptor CreateSoftDebuggerAdaptor () { return new SoftDebuggerAdaptor (); } public Version ProtocolVersion { get { return new Version (vm.Version.MajorVersion, vm.Version.MinorVersion); } } protected override void OnRun (DebuggerStartInfo startInfo) { if (HasExited) throw new InvalidOperationException ("Already exited"); SetupProcessStartHooks (); var dsi = (SoftDebuggerStartInfo) startInfo; if (dsi.StartArgs is SoftDebuggerLaunchArgs) { remoteProcessName = Path.GetFileNameWithoutExtension (dsi.Command); StartLaunching (dsi); } else if (dsi.StartArgs is SoftDebuggerConnectArgs) { StartConnecting (dsi); } else if (dsi.StartArgs is SoftDebuggerListenArgs) { StartListening (dsi); } else if (dsi.StartArgs.ConnectionProvider != null) { StartConnection (dsi); } else { throw new ArgumentException ("StartArgs has no ConnectionProvider"); } } void StartConnection (SoftDebuggerStartInfo dsi) { startArgs = dsi.StartArgs; RegisterUserAssemblies (dsi); if (!String.IsNullOrEmpty (dsi.LogMessage)) OnDebuggerOutput (false, dsi.LogMessage + Environment.NewLine); AsyncCallback callback = null; int attemptNumber = 0; int maxAttempts = startArgs.MaxConnectionAttempts; int timeBetweenAttempts = startArgs.TimeBetweenConnectionAttempts; callback = delegate (IAsyncResult ar) { try { string appName; VirtualMachine machine; startArgs.ConnectionProvider.EndConnect (ar, out machine, out appName); remoteProcessName = appName; ConnectionStarted (machine); return; } catch (Exception ex) { attemptNumber++; if (!ShouldRetryConnection (ex, attemptNumber) || startArgs?.ConnectionProvider?.ShouldRetryConnection (ex) == false || attemptNumber == maxAttempts || HasExited) { OnConnectionError (ex); return; } } try { if (timeBetweenAttempts > 0) Thread.Sleep (timeBetweenAttempts); ConnectionStarting (startArgs.ConnectionProvider.BeginConnect (dsi, callback), dsi, false, attemptNumber); } catch (Exception ex2) { OnConnectionError (ex2); } }; //the "listening" value is never used, pass a dummy value ConnectionStarting (startArgs.ConnectionProvider.BeginConnect (dsi, callback), dsi, false, 0); } void StartLaunching (SoftDebuggerStartInfo dsi) { var args = (SoftDebuggerLaunchArgs) dsi.StartArgs; var executable = string.IsNullOrEmpty (args.MonoExecutableFileName) ? "mono" : args.MonoExecutableFileName; var runtime = string.IsNullOrEmpty (args.MonoRuntimePrefix) ? executable : Path.Combine (Path.Combine (args.MonoRuntimePrefix, "bin"), executable); RegisterUserAssemblies (dsi); var psi = new System.Diagnostics.ProcessStartInfo (runtime) { Arguments = string.Format ("{2} \"{0}\" {1}", dsi.Command, dsi.Arguments, dsi.RuntimeArguments), WorkingDirectory = dsi.WorkingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; LaunchOptions options = null; if (dsi.UseExternalConsole && args.ExternalConsoleLauncher != null) { options = new LaunchOptions (); options.CustomTargetProcessLauncher = args.ExternalConsoleLauncher; psi.RedirectStandardOutput = false; psi.RedirectStandardError = false; } if (args.CustomProcessLauncher != null) { options = options ?? new LaunchOptions (); options.CustomProcessLauncher = args.CustomProcessLauncher; } var sdbLog = Environment.GetEnvironmentVariable ("MONODEVELOP_SDB_LOG"); if (!string.IsNullOrEmpty (sdbLog)) { options = options ?? new LaunchOptions (); options.AgentArgs = string.Format ("loglevel=10,logfile='{0}',setpgid=y", sdbLog); } foreach (var env in args.MonoRuntimeEnvironmentVariables) psi.EnvironmentVariables[env.Key] = env.Value; foreach (var env in dsi.EnvironmentVariables) psi.EnvironmentVariables[env.Key] = env.Value; if (!string.IsNullOrEmpty (dsi.LogMessage)) OnDebuggerOutput (false, dsi.LogMessage + Environment.NewLine); var callback = HandleConnectionCallbackErrors (ar => ConnectionStarted (VirtualMachineManager.EndLaunch (ar))); ConnectionStarting (VirtualMachineManager.BeginLaunch (psi, callback, options), dsi, true, 0); } /// Starts the debugger listening for a connection over TCP/IP protected void StartListening (SoftDebuggerStartInfo dsi) { int dp, cp; StartListening (dsi, out dp, out cp); } /// Starts the debugger listening for a connection over TCP/IP protected void StartListening (SoftDebuggerStartInfo dsi, out int assignedDebugPort) { int cp; StartListening (dsi, out assignedDebugPort, out cp); } /// Starts the debugger listening for a connection over TCP/IP protected void StartListening (SoftDebuggerStartInfo dsi, out int assignedDebugPort, out int assignedConsolePort) { IPEndPoint dbgEP, conEP; InitForRemoteSession (dsi, out dbgEP, out conEP); var callback = HandleConnectionCallbackErrors (ar => ConnectionStarted (VirtualMachineManager.EndListen (ar))); var a = VirtualMachineManager.BeginListen (dbgEP, conEP, callback, out assignedDebugPort, out assignedConsolePort); ConnectionStarting (a, dsi, true, 0); } protected virtual bool ShouldRetryConnection (Exception ex, int attemptNumber) { var sx = ex as SocketException; if (sx != null) { if (sx.ErrorCode == 10061) //connection refused return true; } //retry if it receives 0 byte and threw an exception in the handshake return true; } protected void StartConnecting (SoftDebuggerStartInfo dsi) { StartConnecting (dsi, dsi.StartArgs.MaxConnectionAttempts, dsi.StartArgs.TimeBetweenConnectionAttempts); } /// Starts the debugger connecting to a remote IP protected void StartConnecting (SoftDebuggerStartInfo dsi, int maxAttempts, int timeBetweenAttempts) { if (timeBetweenAttempts < 0 || timeBetweenAttempts > 10000) throw new ArgumentException ("timeBetweenAttempts"); IPEndPoint dbgEP, conEP; InitForRemoteSession (dsi, out dbgEP, out conEP); AsyncCallback callback = null; int attemptNumber = 0; callback = delegate (IAsyncResult ar) { try { ConnectionStarted (VirtualMachineManager.EndConnect (ar)); return; } catch (Exception ex) { attemptNumber++; if (!ShouldRetryConnection (ex, attemptNumber) || attemptNumber == maxAttempts || HasExited) { OnConnectionError (ex); return; } } try { if (timeBetweenAttempts > 0) Thread.Sleep (timeBetweenAttempts); ConnectionStarting (VirtualMachineManager.BeginConnect (dbgEP, conEP, callback), dsi, false, attemptNumber); } catch (Exception ex2) { OnConnectionError (ex2); } }; ConnectionStarting (VirtualMachineManager.BeginConnect (dbgEP, conEP, callback), dsi, false, 0); } void InitForRemoteSession (SoftDebuggerStartInfo dsi, out IPEndPoint dbgEP, out IPEndPoint conEP) { if (remoteProcessName != null) throw new InvalidOperationException ("Cannot initialize connection more than once"); var args = (SoftDebuggerRemoteArgs) dsi.StartArgs; remoteProcessName = args.AppName; RegisterUserAssemblies (dsi); dbgEP = new IPEndPoint (args.Address, args.DebugPort); conEP = args.RedirectOutput? new IPEndPoint (args.Address, args.OutputPort) : null; if (!String.IsNullOrEmpty (dsi.LogMessage)) OnDebuggerOutput (false, dsi.LogMessage + Environment.NewLine); } ///Catches errors in async callbacks and hands off to OnConnectionError AsyncCallback HandleConnectionCallbackErrors (AsyncCallback callback) { return delegate (IAsyncResult ar) { connection = null; try { callback (ar); } catch (Exception ex) { OnConnectionError (ex); } }; } /// /// Called if an error happens while making the connection. Default terminates the session. /// protected virtual void OnConnectionError (Exception ex) { //if the exception was caused by cancelling the session if (HasExited) return; if (!HandleException (new ConnectionException (ex))) { DebuggerLoggingService.LogAndShowException ("Unhandled error launching soft debugger", ex); } // The session is dead // HandleException doesn't actually handle exceptions, it just displays them. try { EndSession (); } catch (Exception e) { DebuggerLoggingService.LogError ("Unhandled error ending the debugger session", e); } } void ConnectionStarting (IAsyncResult connectionHandle, DebuggerStartInfo dsi, bool listening, int attemptNumber) { if (connection != null && (attemptNumber == 0 || !connection.IsCompleted)) throw new InvalidOperationException ("Already connecting"); connection = connectionHandle; if (attemptNumber == 0) { if (ConnectionDialogCreatorExtended != null) connectionDialog = ConnectionDialogCreatorExtended (dsi); else if (ConnectionDialogCreator != null) connectionDialog = ConnectionDialogCreator (); if (connectionDialog != null) { connectionDialog.UserCancelled += delegate { EndSession (); }; } } if (connectionDialog != null) connectionDialog.SetMessage (dsi, GetConnectingMessage (dsi), listening, attemptNumber); } protected virtual string GetConnectingMessage (DebuggerStartInfo dsi) { return null; } void EndLaunch () { HideConnectionDialog (); if (connection != null) { try { if (startArgs != null && startArgs.ConnectionProvider != null) { startArgs.ConnectionProvider.CancelConnect(connection); startArgs = null; } else { VirtualMachineManager.CancelConnection(connection); } } catch(Exception e) { DebuggerLoggingService.LogError("Unhandled error canceling the debugger connection", e); } connection = null; } } protected virtual void EndSession () { if (!HasExited) { EndLaunch (); OnTargetEvent (new TargetEventArgs (TargetEventType.TargetExited)); } } const string StartWithShellExecuteExMarker = "___Process_StartWithShellExecuteEx_Marker___"; const string StartWithCreateProcessMarker = "___Process_StartWithCreateProcess_Marker__"; const string SetProcessIdMarker = "___Process_SetProcessId_Marker__"; void SetupProcessStartHooks () { if (!Options.DebugSubprocesses) return; // Subprocess lauch is intercepted using 3 function breakpoints. // The first breakpoint is used to intercept calls to start a process with shell execute. var bp = Breakpoints.OfType< FunctionBreakpoint> ().FirstOrDefault (b => b.CustomActionId == StartWithShellExecuteExMarker); if (bp == null) { bp = new FunctionBreakpoint ("System.Diagnostics.Process.StartWithShellExecuteEx", "C#"); bp.HitAction = HitAction.CustomAction; bp.CustomActionId = StartWithShellExecuteExMarker; bp.NonUserBreakpoint = true; Breakpoints.Add (bp); } // The second breakpoint is used to intercept calls to start a process with CreateProcess bp = Breakpoints.OfType ().FirstOrDefault (b => b.CustomActionId == StartWithCreateProcessMarker); if (bp == null) { bp = new FunctionBreakpoint ("System.Diagnostics.Process.StartWithCreateProcess", "C#"); bp.HitAction = HitAction.CustomAction; bp.CustomActionId = StartWithCreateProcessMarker; bp.NonUserBreakpoint = true; Breakpoints.Add (bp); } // The third breakpoint is used to intercept the method that assigns an ID to the process. This is used to // retrieve the process id, and also as a signal that the process has started running bp = Breakpoints.OfType ().FirstOrDefault (b => b.CustomActionId == SetProcessIdMarker); if (bp == null) { bp = new FunctionBreakpoint ("System.Diagnostics.Process.SetProcessId", "C#"); bp.HitAction = HitAction.CustomAction; bp.CustomActionId = SetProcessIdMarker; bp.NonUserBreakpoint = true; Breakpoints.Add (bp); } } bool HandleProcessStartHook (ThreadMirror thread, BreakEvent b) { if (!Options.DebugSubprocesses) return false; // Subprocess debugging is achieved by intercepting calls to start a process using function breakpoints. // This is achieved in 3 stages: // 1) Process start interception: this is done by adding a function breakpoint to Process.StartWithShellExecuteEx // and Process.StartWithCreateProcess. If the process being launched is a mono process a new debug session // is creaated, and the process start info object is patched to add the debugger agent configuration options. // 2) Process launch. This is actually done by the current executing process. The new process is bound to // the new debug session. However, to complete the debug session initialization we need an actual reference // to the process, so that the session can get the process id and subscribe process events, and that's // done in step 3. // 3) Process launched interception: this is done by adding a function breakpoint to Process.SetProcessId. // This method is called after starting the process, so at this point it is possible to get a reference // to the running process object and assign it to the debug session to complete the initialization. // A potential process start call has been intercepted. var evalOptions = EvaluationOptions.DefaultOptions.Clone (); evalOptions.AllowTargetInvoke = evalOptions.AllowMethodEvaluation = evalOptions.FlattenHierarchy = true; evalOptions.GroupPrivateMembers = evalOptions.GroupStaticMembers = evalOptions.AllowToStringCalls = evalOptions.EllipsizeStrings = false; if (b.CustomActionId == StartWithShellExecuteExMarker || b.CustomActionId == StartWithCreateProcessMarker) { // A project start call has been intercepted. We need to check if a mono process is being started. var fr = thread.GetFrames ()[0]; var ctx = new SoftEvaluationContext (this, fr, evalOptions); var eval = ctx.Evaluator; var parameters = eval.GetParameters (ctx); var startInfo = parameters.First(); var exe = (string) startInfo.GetChild ("FileName", evalOptions).GetRawValue (evalOptions); // The process being launched is a mono process if the target file is a .exe or .dll, or if the launcher is mono. var ext = Path.GetExtension (exe).ToLower (); if (ext == ".exe" || ext == ".dll" || Subprocess.IsMonoLauncher (exe)) { // If the runtime arguments are already setting up a debugger agent, don't override it var arguments = (string)startInfo.GetChild ("Arguments", evalOptions).GetRawValue (evalOptions); if (arguments.Contains ("--debugger-agent")) return false; // Create the new debug session Subprocess sp; lock (subprocessesStarting) { sp = new Subprocess (GetId (thread), this); subprocessesStarting.Add (sp); } if (sp.CreateSession (startInfo, evalOptions)) { // Notify that the new session has started OnSubprocessStarted (sp.SubprocessSession); } } return true; } else if (b.CustomActionId == SetProcessIdMarker) { // A call to SetProcessId has been intercepted // Check if there is a subprocess waiting for the start signal var tid = GetId (thread); Subprocess subprocess; lock (subprocessesStarting) subprocess = subprocessesStarting.FirstOrDefault (s => s.ThreadId == tid); if (subprocess != null) { lock (subprocessesStarting) subprocessesStarting.Remove (subprocess); var fr = thread.GetFrames ()[0]; var ctx = new SoftEvaluationContext (this, fr, evalOptions); var eval = ctx.Evaluator; try { // The first parameter is the process ID var parameters = eval.GetParameters (ctx); var id = (int)parameters.First ().GetRawValue (evalOptions); // Get the start info object from the process var ts = eval.GetThisReference (ctx); var si = ts.GetChild ("startInfo", evalOptions); // Tell the subprocess that the process has started. This will finish the initialization // of the debug session. subprocess.SetStarted (si, id, ctx); } catch { subprocess.Shutdown (); throw; } } return true; } return false; } List subprocessesStarting = new List (); public Dictionary, MethodMirror[]> OverloadResolveCache { get { return overloadResolveCache; } } void HideConnectionDialog () { if (connectionDialog != null) { connectionDialog.Dispose (); connectionDialog = null; } } /// /// If subclasses do an async connect in OnRun, they should pass the resulting VM to this method. /// If the vm is null, the session will be closed. /// void ConnectionStarted (VirtualMachine machine) { if (vm != null) throw new InvalidOperationException ("The VM has already connected"); if (machine == null) { EndSession (); return; } connection = null; vm = machine; // Allocate the process list now, so it is cached and can be queried while the session is running OnGetProcesses (); ConnectOutput (machine.StandardOutput, false); ConnectOutput (machine.StandardError, true); HideConnectionDialog (); machine.EnableEvents (EventType.AssemblyLoad, EventType.ThreadStart, EventType.ThreadDeath, EventType.AppDomainUnload, EventType.UserBreak, EventType.UserLog); try { unhandledExceptionRequest = machine.CreateExceptionRequest (null, false, true); unhandledExceptionRequest.Enable (); } catch (NotSupportedException) { //Mono < 2.6.3 doesn't support catching unhandled exceptions } if (machine.Version.AtLeast (2, 9)) { /* Created later */ } else { machine.EnableEvents (EventType.TypeLoad); } machine.EnableEvents (EventType.MethodUpdate); started = true; /* Wait for the VMStart event */ HandleEventSet (machine.GetNextEventSet ()); eventHandler = new Thread (EventHandler); eventHandler.Name = "SDB Event Handler"; eventHandler.IsBackground = true; eventHandler.Start (); } void RegisterUserAssemblies (SoftDebuggerStartInfo dsi) { if (Options.ProjectAssembliesOnly && dsi.UserAssemblyNames != null) { assemblyFilters = new List (); userAssemblyNames = dsi.UserAssemblyNames.Select (x => x.ToString ()).ToList (); } assemblyPathMap = dsi.AssemblyPathMap; if (assemblyPathMap == null) assemblyPathMap = new Dictionary (); if (dsi.SymbolPathMap == null) symbolPathMap = new Dictionary(); else symbolPathMap = dsi.SymbolPathMap; } public void AddOrReplaceAssemblyPathMap(string assembly, string path) { assemblyPathMap[assembly] = path; } public void AddOrReplaceSymbolPathMap (string assembly, string path) { symbolPathMap[assembly] = path; } protected bool SetSocketTimeouts (int sendTimeout, int receiveTimeout, int keepaliveInterval) { try { if (vm.Version.AtLeast (2, 4)) { vm.EnableEvents (EventType.KeepAlive); vm.SetSocketTimeouts (sendTimeout, receiveTimeout, keepaliveInterval); return true; } return false; } catch { return false; } } protected void ConnectOutput (StreamReader reader, bool error) { Thread t = (error ? errorReader : outputReader); if (t != null || reader == null) return; t = new Thread (() => ReadOutput (reader, error)); t.Name = error ? "SDB error reader" : "SDB output reader"; t.IsBackground = true; t.Start (); if (error) errorReader = t; else outputReader = t; } void ReadOutput (TextReader reader, bool isError) { try { var buffer = new char [1024]; while (!HasExited) { int c = reader.Read (buffer, 0, buffer.Length); if (c > 0) { OnTargetOutput (isError, new string (buffer, 0, c)); } else { //FIXME: workaround for buggy console stream that never blocks Thread.Sleep (250); } } } catch (IOException) { // Ignore } } protected virtual void OnResumed () { current_threads = null; current_thread = null; activeExceptionsByThread.Clear (); } public VirtualMachine VirtualMachine { get { return vm; } } public TypeMirror GetType (string fullName) { List typesList; if (!types.TryGetValue (fullName, out typesList)) aliases.TryGetValue (fullName, out typesList); if (typesList == null) return null; if (typesList.Count == 1) return typesList [0]; //Idea here is... If we have multiple types with same name... they must be in different .dlls //so find 1st matching assembly with stackframes and then return type that belongs to that assembly var assembly = current_thread.GetFrames ().Select (f => f.Method.DeclaringType.Assembly).FirstOrDefault (asm => typesList.Select (t => t.Assembly).Contains (asm)); return typesList.FirstOrDefault (t => t.Assembly == assembly) ?? typesList.FirstOrDefault (); } public IEnumerable GetAllTypes () { return types.Values.SelectMany (l => l); } protected override bool AllowBreakEventChanges { get { return true; } } public override void Dispose () { base.Dispose (); if (disposed) return; disposed = true; lock (subprocessesStarting) { foreach (var s in subprocessesStarting) s.Shutdown (); } if (!HasExited) EndLaunch (); exceptionRequests.Clear(); if (!HasExited) { if (vm != null) { ThreadPool.QueueUserWorkItem (delegate { try { vm.Exit (0); } catch (VMDisconnectedException) { } catch (Exception ex) { DebuggerLoggingService.LogError ("Error exiting SDB VM:", ex); } }); } } Adaptor.Dispose (); } protected override void OnAttachToProcess (long processId) { throw new NotSupportedException (); } protected override void OnContinue () { ThreadPool.QueueUserWorkItem (delegate { try { Adaptor.CancelAsyncOperations (); // This call can block, so it has to run in background thread to avoid keeping the main session lock OnResumed (); vm.Resume (); DequeueEventsForFirstThread (); } catch (Exception ex) { if (!HandleException (ex) && outputOptions.ExceptionMessage) OnDebuggerOutput (true, ex.ToString ()); } }); } protected override void OnDetach () { vm.Detach (); } protected override void OnExit () { HasExited = true; EndLaunch (); if (vm != null) { try { vm.Exit (0); } catch (VMDisconnectedException) { // The VM was already disconnected, ignore. } catch (SocketException se) { // This will often happen during normal operation DebuggerLoggingService.LogError ("Error closing debugger session", se); } catch (IOException ex) { // This will often happen during normal operation DebuggerLoggingService.LogError ("Error closing debugger session", ex); } } QueueEnsureExited (); } void QueueEnsureExited () { if (vm != null) { //FIXME: this might never get reached if the IDE is Exited first try { if (vm.Process != null) { ThreadPool.QueueUserWorkItem (delegate { // This is a workaround for a mono bug // Without this call, the process may become zombie in mono < 2.10.2 vm.Process.WaitForExit (); }); } } catch (InvalidOperationException) { // ignore - this is thrown by the vm.Process getter when the process has already exited } catch (Exception ex) { DebuggerLoggingService.LogError ("Failed to launch a thread to wait for the process to exit", ex); } var t = new System.Timers.Timer (); t.Interval = 3000; t.Elapsed += delegate { try { t.Enabled = false; t.Dispose (); EnsureExited (); } catch (Exception ex) { DebuggerLoggingService.LogError ("Failed to force-terminate process", ex); } try { if (vm != null) { //this is a no-op if it already closed vm.ForceDisconnect (); } } catch (Exception ex) { DebuggerLoggingService.LogError ("Failed to force-close debugger connection", ex); } }; t.Enabled = true; } } /// This is a fallback in case the debugger agent doesn't respond to an exit call protected virtual void EnsureExited () { try { if (vm != null && vm.TargetProcess != null && !vm.TargetProcess.HasExited) vm.TargetProcess.Kill (); } catch (Exception ex) { DebuggerLoggingService.LogError ("Error force-terminating soft debugger process", ex); } } protected override void OnFinish () { Step (StepDepth.Out, StepSize.Line); } protected override ProcessInfo[] OnGetProcesses () { if (procs == null) { if (vm == null)//process didn't start yet return new ProcessInfo [0]; if (vm.TargetProcess == null) { procs = new [] { new ProcessInfo (0, remoteProcessName ?? "mono") }; } else { try { procs = new [] { new ProcessInfo (vm.TargetProcess.Id, remoteProcessName ?? vm.TargetProcess.ProcessName) }; } catch (Exception ex) { if (!loggedSymlinkedRuntimesBug) { loggedSymlinkedRuntimesBug = true; DebuggerLoggingService.LogError ("Error getting debugger process info. Known Mono bug with symlinked runtimes.", ex); } procs = new [] { new ProcessInfo (0, "mono") }; } } } return new [] { new ProcessInfo (procs[0].Id, procs[0].Name) }; } internal PortablePdbData GetPdbData (AssemblyMirror asm) { string pdbFileName; if (!symbolPathMap.TryGetValue(asm.GetName().FullName, out pdbFileName) || Path.GetExtension(pdbFileName) != ".pdb") { string assemblyFileName; if (!assemblyPathMap.TryGetValue (asm.GetName ().FullName, out assemblyFileName)) assemblyFileName = asm.Location; pdbFileName = Path.ChangeExtension (assemblyFileName, ".pdb"); } if (PortablePdbData.IsPortablePdb (pdbFileName)) return new PortablePdbData (pdbFileName); // Attempt to fetch pdb from the debuggee over the wire var pdbBlob = asm.GetPdbBlob (); return pdbBlob != null ? new PortablePdbData (pdbBlob) : null; } internal PortablePdbData GetPdbData (MethodMirror method) { return GetPdbData (method.DeclaringType.Assembly); } readonly Dictionary SourceLinkCache = new Dictionary (); SourceLinkMap [] GetSourceLinkMaps (MethodMirror method) { var asm = method.DeclaringType.Assembly; SourceLinkMap[] maps; lock (SourceLinkCache) { if (SourceLinkCache.TryGetValue (asm, out maps)) return maps; } string jsonString = null; if (asm.VirtualMachine.Version.AtLeast (2, 48)) { jsonString = asm.ManifestModule.SourceLink; } else { // Read the pdb ourselves jsonString = GetPdbData (asm)?.GetSourceLinkBlob (); } if (!string.IsNullOrWhiteSpace(jsonString)) { try { var jsonSourceLink = JsonConvert.DeserializeObject (jsonString); if (jsonSourceLink != null && jsonSourceLink.Maps != null && jsonSourceLink.Maps.Any ()) maps = jsonSourceLink.Maps.Select (kv => new SourceLinkMap (kv.Key, kv.Value)).ToArray (); } catch (JsonException ex) { DebuggerLoggingService.LogError ("Error reading source link", ex); } } lock (SourceLinkCache) { SourceLinkCache[asm] = maps ?? Array.Empty (); } return maps; } internal SourceLink GetSourceLink (MethodMirror method, string originalFileName) { if (originalFileName == null) return null; var maps = GetSourceLinkMaps (method); if (maps == null || maps.Length == 0) return null; originalFileName = originalFileName.Replace ('\\', '/'); foreach (var map in maps) { var pattern = map.RelativePathWildcard.Replace ("*", "").Replace ('\\', '/'); if (originalFileName.StartsWith (pattern, StringComparison.Ordinal)) { var localPath = originalFileName.Replace (pattern.Replace (".*", ""), ""); var httpBasePath = map.UriWildcard.Replace ("*", ""); // org/project-name/git-sha (usually) var pathAndQuery = new Uri (httpBasePath).GetComponents (UriComponents.Path, UriFormat.SafeUnescaped); // org/projectname/git-sha/path/to/file.cs var relativePath = Path.Combine (pathAndQuery, localPath); // Replace something like "f:/build/*" with "https://raw.githubusercontent.com/org/projectname/git-sha/*" var httpPath = Regex.Replace (originalFileName, pattern, httpBasePath); return new SourceLink (httpPath, relativePath); } } return null; } protected override Backtrace OnGetThreadBacktrace (long processId, long threadId) { return GetThreadBacktrace (GetThread (threadId)); } protected override long OnGetElapsedTime (long processId, long threadId) { return GetElapsedTime (GetThread (threadId)); } Backtrace GetThreadBacktrace (ThreadMirror thread) { return new Backtrace (new SoftDebuggerBacktrace (this, thread)); } long GetElapsedTime (ThreadMirror thread) { return thread.ElapsedTime (); } string GetThreadName (ThreadMirror t) { string name = t.Name; if (string.IsNullOrEmpty (name)) { try { if (t.IsThreadPoolThread) return ""; } catch { if (vm.Version.AtLeast (2, 2)) { throw; } return ""; } } return name; } protected override void OnFetchFrames (ThreadInfo [] threads) { var mirrorThreads = new List (threads.Length); for (int i = 0; i < threads.Length; i++) { var thread = GetThread (threads [i].Id); if (thread != null) { mirrorThreads.Add (thread); } } ThreadMirror.FetchFrames (mirrorThreads); } protected override ThreadInfo[] OnGetThreads (long processId) { if (current_threads == null) { var mirrors = vm.GetThreads (); var threads = new ThreadInfo[mirrors.Count]; for (int i = 0; i < mirrors.Count; i++) { var thread = mirrors[i]; threads[i] = new ThreadInfo (processId, GetId (thread), GetThreadName (thread), null); } current_threads = threads; } return current_threads; } ThreadMirror GetThread (long threadId) { foreach (var thread in vm.GetThreads ()) { if (GetId (thread) == threadId) return thread; } return null; } ThreadInfo GetThread (ProcessInfo process, ThreadMirror thread) { long id = GetId (thread); foreach (var threadInfo in OnGetThreads (process.Id)) { if (threadInfo.Id == id) return threadInfo; } return null; } public override bool CanSetNextStatement { get { return vm.Version.AtLeast (2, 29); } } protected override void OnSetNextStatement (long threadId, string fileName, int line, int column) { if (!CanSetNextStatement) throw new NotSupportedException (); var thread = GetThread (threadId); if (thread == null) throw new ArgumentException ("Unknown thread."); var frames = thread.GetFrames (); if (frames.Length == 0) throw new NotSupportedException (); bool dummy = false; var location = FindLocationByMethod (frames[0].Method, fileName, line, column, ref dummy); if (location == null) throw new NotSupportedException ("Unable to set the next statement. The next statement cannot be set to another function."); try { thread.SetIP (location); currentAddress = location.ILOffset; currentStackDepth = frames.Length; } catch (ArgumentException) { throw new NotSupportedException (); } } protected override void OnSetNextStatement (long threadId, int ilOffset) { if (!CanSetNextStatement) throw new NotSupportedException (); var thread = GetThread (threadId); if (thread == null) throw new ArgumentException ("Unknown thread."); var frames = thread.GetFrames (); if (frames.Length == 0) throw new NotSupportedException (); var location = frames[0].Method.LocationAtILOffset (ilOffset); if (location == null) throw new NotSupportedException (); try { thread.SetIP (location); StackVersion++; RaiseStopEvent (); } catch (ArgumentException) { throw new NotSupportedException (); } } protected override BreakEventInfo OnInsertBreakEvent (BreakEvent breakEvent) { var breakInfo = new BreakInfo (); if (HasExited) { breakInfo.SetStatus (BreakEventStatus.Disconnected, null); return breakInfo; } if (breakEvent is FunctionBreakpoint function) { foreach (var method in FindMethodsByName (function.FunctionName, function.ParamTypes)) { if (!ResolveFunctionBreakpoint (breakInfo, function, method)) { breakInfo.SetStatus (BreakEventStatus.NotBound, null); } } // FIXME: handle types like GenericType<>, GenericType, and GenericType<...>+NestedGenricType<...> var openParen = function.FunctionName.IndexOf ('('); int dot; if (openParen != -1) { //Handle stuff like SomeNamespace.SomeType.Method(SomeOtherNamespace.SomeOtherType) dot = function.FunctionName.LastIndexOf ('.', openParen); } else { dot = function.FunctionName.LastIndexOf ('.'); } if (dot != -1) breakInfo.TypeName = function.FunctionName.Substring (0, dot); lock (pending_bes) { pending_bes.Add (breakInfo); } } else if (breakEvent is InstructionBreakpoint instruction) { Location location; breakInfo.FileName = instruction.FileName; if ((location = FindLocationByILOffset (instruction, instruction.FileName, out _, out var insideTypeRange)) != null) { breakInfo.Location = location; InsertBreakpoint (instruction, breakInfo); breakInfo.SetStatus (BreakEventStatus.Bound, null); } else if (insideTypeRange) breakInfo.SetStatus (BreakEventStatus.Invalid, null); else breakInfo.SetStatus (BreakEventStatus.NotBound, null); lock (pending_bes) { pending_bes.Add (breakInfo); } } else if (breakEvent is Breakpoint breakpoint) { bool insideLoadedRange; bool found = false; breakInfo.FileName = breakpoint.FileName; foreach (var location in FindLocationsByFile (breakpoint.FileName, breakpoint.Line, breakpoint.Column, out _, out insideLoadedRange)) { OnDebuggerOutput (false, string.Format ("Resolved pending breakpoint at '{0}:{1},{2}' to {3} [0x{4:x5}].\n", breakpoint.FileName, breakpoint.Line, breakpoint.Column, GetPrettyMethodName (location.Method), location.ILOffset)); breakInfo.Location = location; InsertBreakpoint (breakpoint, breakInfo); found = true; breakInfo.SetStatus (BreakEventStatus.Bound, null); } lock (pending_bes) { pending_bes.Add (breakInfo); } if (!found) { breakInfo.Breakpoint = breakpoint; if (insideLoadedRange) breakInfo.SetStatus (BreakEventStatus.Invalid, null); else breakInfo.SetStatus (BreakEventStatus.NotBound, null); } } else if (breakEvent is Catchpoint catchpoint) { if (!types.ContainsKey (catchpoint.ExceptionName)) { // // Same as in FindLocationByFile (), fetch types matching the type name if (vm.Version.AtLeast (2, 9)) { foreach (TypeMirror t in vm.GetTypes (catchpoint.ExceptionName, false)) ProcessType (t); } } List typesList; if (types.TryGetValue (catchpoint.ExceptionName, out typesList)) { foreach (var type in typesList) InsertCatchpoint (catchpoint, breakInfo, type); breakInfo.SetStatus (BreakEventStatus.Bound, null); } else { breakInfo.SetStatus (BreakEventStatus.NotBound, null); } breakInfo.TypeName = catchpoint.ExceptionName; lock (pending_bes) { pending_bes.Add (breakInfo); } } UpdateTypeLoadFilters (); return breakInfo; } void UpdateTypeLoadFilters() { /* * TypeLoad events lead to too much wire traffic + suspend/resume work, so * filter them using the file names used by pending breakpoints. */ if (vm.Version.AtLeast (2, 9)) { string [] sourceFileList; lock (pending_bes) { sourceFileList = pending_bes.Where (b => b.FileName != null).SelectMany ((b, i) => new [] { Path.GetFileName (b.FileName), b.FileName }).Distinct ().ToArray (); } if (sourceFileList.Length > 0) { //HACK: with older versions of sdb that don't support case-insenitive compares, //explicitly try lowercased drivename on windows, since csc (when not hosted in VS) lowercases //the drivename in the pdb files that get converted to mdbs as-is if (IsWindows && !vm.Version.AtLeast (2, 12)) { int originalCount = sourceFileList.Length; Array.Resize (ref sourceFileList, originalCount * 2); for (int i = 0; i < originalCount; i++) { string n = sourceFileList[i]; sourceFileList[originalCount + i] = char.ToLower (n[0]) + n.Substring (1); } } if (typeLoadReq == null) { typeLoadReq = vm.CreateTypeLoadRequest (); } typeLoadReq.Enabled = false; typeLoadReq.SourceFileFilter = sourceFileList; typeLoadReq.Enabled = true; } string [] typeNameList; lock (pending_bes) { typeNameList = pending_bes.Where (b => b.TypeName != null).Select (b => b.TypeName).ToArray (); } if (typeNameList.Length > 0) { // Use a separate request since the filters are ANDed together if (typeLoadTypeNameReq == null) { typeLoadTypeNameReq = vm.CreateTypeLoadRequest (); } typeLoadTypeNameReq.Enabled = false; typeLoadTypeNameReq.TypeNameFilter = typeNameList; typeLoadTypeNameReq.Enabled = true; } } } private Location FindLocationByILOffset (InstructionBreakpoint bp, string filename, out bool isGeneric, out bool insideTypeRange) { var typesInFile = new List (); AddFileToSourceMapping (filename); insideTypeRange = true; isGeneric = false; lock (source_to_type) { if (source_to_type.TryGetValue (filename, out typesInFile)) { foreach (var type in typesInFile) { var method = type.GetMethod (bp.MethodName); if (method != null) { foreach (var location in method.Locations) { if (location.ILOffset == bp.ILOffset) { isGeneric = type.IsGenericType; return location; } } } } } } return null; } protected override void OnRemoveBreakEvent (BreakEventInfo eventInfo) { if (HasExited) return; var bi = (BreakInfo) eventInfo; if (bi.Requests.Count != 0) { foreach (var request in bi.Requests.Keys) { request.Enabled = false; breakpoints.Remove (request); } RemoveQueuedBreakEvents (bi.Requests); } lock (pending_bes) { pending_bes.Remove (bi); } } protected override void OnEnableBreakEvent (BreakEventInfo eventInfo, bool enable) { if (HasExited) return; var bi = (BreakInfo) eventInfo; if (bi.Requests.Count != 0) { foreach (var request in bi.Requests.Keys) request.Enabled = enable; if (!enable) RemoveQueuedBreakEvents (bi.Requests); } } protected override void OnUpdateBreakEvent (BreakEventInfo eventInfo) { } void InsertBreakpoint (Breakpoint bp, BreakInfo bi) { InsertBreakpoint (bp, bi, bi.Location.Method, bi.Location.ILOffset); } void UpdateBreakpoint (Breakpoint bp, BreakInfo bi) { foreach (var req in bi.Requests) { req.Key.Disable (); var request = vm.SetBreakpoint (bi.Location.Method, bi.Location.ILOffset); req.Key.UpdateReqId (request.GetId()); req.Key.Enabled = bp.Enabled; } } void InsertBreakpoint (Breakpoint bp, BreakInfo bi, MethodMirror method, int ilOffset) { EventRequest request; request = vm.SetBreakpoint (method, ilOffset); request.Enabled = bp.Enabled; bi.Requests.Add (request, method.DeclaringType.Assembly); breakpoints [request] = bi; if (bi.Location != null && (bi.Location.LineNumber != bp.Line || bi.Location.ColumnNumber != bp.Column)) bi.AdjustBreakpointLocation (bi.Location.LineNumber, bi.Location.ColumnNumber); } void InsertCatchpoint (Catchpoint cp, BreakInfo bi, TypeMirror excType) { ExceptionEventRequest request; request = vm.CreateExceptionRequest (excType, true, true); //Commenting Count so we have better control of counting //because VM only allows count equal to some number but we need also //lower, greater, equal or greater... //Plus some day we might want to put filtering before counting... //request.Count = cp.HitCount; // Note: need to set HitCount *before* enabling if (vm.Version.AtLeast (2, 25)) request.IncludeSubclasses = cp.IncludeSubclasses; // Note: need to set IncludeSubclasses *before* enabling request.Enabled = cp.Enabled; bi.Requests.Add (request, excType.Assembly); breakpoints[request] = bi; } #region ExceptionRequest //VS in Windows doesn't have the concept of a Catchpoint. In the past we did use it anyway, but that leads to all sort of issues and bugs. //The most important difference is that subclasses are never caught, and that Others is a concept that the Catchpoint doesn't support properly. //This region is for Windows, to be able to properly handle exceptions with an exception list. The concept of Other is supported starting with vm version 2.54. readonly Dictionary exceptionRequests = new Dictionary(); ExceptionEventRequest otherExceptions; public void EnableException(string exceptionType, bool caught = true) { lock (exceptionRequests) { if (!types.ContainsKey (exceptionType)) { if (vm.Version.AtLeast (2, 9)) { try { foreach (TypeMirror t in vm.GetTypes (exceptionType, false)) ProcessType (t); } catch (CommandException exc) { if (outputOptions.ExceptionMessage) OnDebuggerOutput (false, string.Format ("Error while parsing type ‘{0}’.\n", exceptionType)); } } } if (types.TryGetValue(exceptionType, out var typemirrors)) { var exception = typemirrors.First(); var request = vm.CreateExceptionRequest (exception, caught, true, false); request.Enable(); exceptionRequests[exceptionType] = request; } } } public void EnableOtherExceptions() { if (!vm.Version.AtLeast(2, 54)) return; if (otherExceptions == null) otherExceptions = vm.CreateExceptionRequest (null, true, true, true); otherExceptions.Enable (); } public void DisableException(string exceptionType, bool setuncaught = true) { lock (exceptionRequests) { if (exceptionRequests.TryGetValue (exceptionType, out var request)) { request.Disable(); exceptionRequests.Remove(exceptionType); } if (setuncaught) EnableException (exceptionType, false); } } public void DisableOtherExceptions() { if (otherExceptions != null) { otherExceptions.Disable(); } } public void RemoveException(string exceptionType) { DisableException(exceptionType, false); } #endregion static bool CheckTypeName (string typeName, string name) { // if the name provided is empty, it matches anything. if (string.IsNullOrEmpty (name)) return true; if (name.StartsWith ("global::", StringComparison.Ordinal)) { if (typeName != name.Substring ("global::".Length)) return false; } else if (name.StartsWith ("::", StringComparison.Ordinal)) { if (typeName != name.Substring ("::".Length)) return false; } else { // be a little more flexible with what we match... i.e. "Console" should match "System.Console" if (typeName != null && typeName.Length > name.Length) { if (!typeName.EndsWith (name, StringComparison.Ordinal)) return false; char delim = typeName[typeName.Length - name.Length]; if (delim != '.' && delim != '+') return false; } else if (typeName != name) { return false; } } return true; } static bool CheckTypeName (TypeMirror type, string name) { if (string.IsNullOrEmpty (name)) { // empty name matches anything return true; } if (name[name.Length - 1] == '?') { // canonicalize the user-specified nullable type return CheckTypeName (type, string.Format ("System.Nullable<{0}>", name.Substring (0, name.Length - 1))); } if (type.IsArray) { int startIndex = name.LastIndexOf ('['); int endIndex = name.Length - 1; if (startIndex == -1 || name[endIndex] != ']') { // the user-specified type is not an array return false; } var rank = name.Substring (startIndex + 1, endIndex - (startIndex + 1)).Split (new [] { ',' }); if (rank.Length != type.GetArrayRank ()) return false; return CheckTypeName (type.GetElementType (), name.Substring (0, startIndex).TrimEnd ()); } if (type.IsPointer) { if (name.Length < 2 || name[name.Length - 1] != '*') return false; return CheckTypeName (type.GetElementType (), name.Substring (0, name.Length - 1).TrimEnd ()); } if (type.IsGenericType) { int startIndex = name.IndexOf ('<'); int endIndex = name.Length - 1; if (startIndex == -1 || name[endIndex] != '>') { // the user-specified type is not a generic type return false; } // make sure that the type name matches (minus generics) string subName = name.Substring (0, startIndex); string typeName = type.FullName; int tick; if ((tick = typeName.IndexOf ('`')) != -1) typeName = typeName.Substring (0, tick); if (!CheckTypeName (typeName, subName)) return false; string[] paramTypes; if (!FunctionBreakpoint.TryParseParameters (name, startIndex + 1, endIndex, out paramTypes)) return false; TypeMirror[] argTypes = type.GetGenericArguments (); if (paramTypes.Length != argTypes.Length) return false; for (int i = 0; i < paramTypes.Length; i++) { if (!CheckTypeName (argTypes[i], paramTypes[i])) return false; } } else if (!CheckTypeName (type.CSharpName, name)) { if (!CheckTypeName (type.FullName, name)) return false; } return true; } static bool CheckMethodParams (MethodMirror method, string[] paramTypes) { if (paramTypes == null) { // User supplied no params to match against, match anything we find. return true; } var parameters = method.GetParameters (); if (parameters.Length != paramTypes.Length) return false; for (int i = 0; i < paramTypes.Length; i++) { if (!CheckTypeName (parameters[i].ParameterType, paramTypes[i])) return false; } return true; } bool IsGenericMethod (MethodMirror method) { return vm.Version.AtLeast (2, 12) && method.IsGenericMethod; } //If paramType == null all overloads are returned IEnumerable FindMethodsByName (string function, string[] paramTypes) { if (!started) yield break; if (vm.Version.AtLeast (2, 9)) { var bracket = function.IndexOf ('('); int dot; if (bracket != -1) { //Handle stuff like SomeNamespace.SomeType.Method(SomeOtherNamespace.SomeOtherType) dot = function.LastIndexOf ('.', bracket); } else { dot = function.LastIndexOf ('.'); } if (dot == -1 || dot + 1 == function.Length) yield break; // FIXME: handle types like GenericType<>, GenericType, and GenericType<...>+NestedGenricType<...> string methodName; string typeName = function.Substring (0, dot); if (bracket == -1) { methodName = function.Substring (dot + 1); } else { methodName = function.Substring (dot + 1, bracket - (dot + 1)); } // FIXME: need a way of querying all types so we can substring match typeName (e.g. user may have typed "Console" instead of "System.Console") foreach (var type in vm.GetTypes (typeName, false)) { ProcessType (type); foreach (var method in type.GetMethodsByNameFlags (methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, false)) { if (!CheckMethodParams (method, paramTypes)) continue; yield return method; } } } yield break; } IList FindLocationsByFile (string file, int line, int column, out bool genericTypeOrMethod, out bool insideLoadedRange) { var locations = new List (); genericTypeOrMethod = false; insideLoadedRange = false; if (!started) return locations; var filename = Path.GetFileName (file); AddFileToSourceMapping (filename); lock (source_to_type) { // Try already loaded types in the current source file if (source_to_type.TryGetValue (filename, out var mirrors)) { foreach (var type in mirrors) { var location = FindLocationByType (type, file, line, column, out var genericMethod, out var insideRange); if (insideRange) insideLoadedRange = true; if (location != null) { if (genericMethod || type.IsGenericType) genericTypeOrMethod = true; locations.Add (location); } } } } return locations; } private void AddFileToSourceMapping (string filename) { // // Fetch types matching the source file from the debuggee, and add them // to the source file->type mapping tables. // This is needed because we don't receive type load events for all types, // just the ones which match a source file with an existing breakpoint. // if (vm.Version.AtLeast (2, 9)) { IList typesInFile; if (vm.Version.AtLeast (2, 12)) { typesInFile = vm.GetTypesForSourceFile (filename, IgnoreFilenameCase); } else { typesInFile = vm.GetTypesForSourceFile (filename, false); //HACK: with older versions of sdb that don't support case-insenitive compares, //explicitly try lowercased drivename on windows, since csc (when not hosted in VS) lowercases //the drivename in the pdb files that get converted to mdbs as-is if (typesInFile.Count == 0 && IsWindows) { string alternateCaseFilename = char.ToLower (filename [0]) + filename.Substring (1); typesInFile = vm.GetTypesForSourceFile (alternateCaseFilename, false); } } foreach (TypeMirror t in typesInFile) ProcessType (t); } } public override bool CanCancelAsyncEvaluations { get { return Adaptor.IsEvaluating; } } protected override void OnCancelAsyncEvaluations () { Adaptor.CancelAsyncOperations (); } protected override void OnNextInstruction () { Step (StepDepth.Over, StepSize.Min); } protected override void OnNextLine () { Step (StepDepth.Over, StepSize.Line); } void Step (StepDepth depth, StepSize size) { try { Adaptor.CancelAsyncOperations (); // This call can block, so it has to run in background thread to avoid keeping the main session lock var req = vm.CreateStepRequest (current_thread); req.Depth = depth; req.Size = size; req.Filter = ShouldFilterStaticCtor () | StepFilter.DebuggerHidden | StepFilter.DebuggerStepThrough; if (Options.ProjectAssembliesOnly) req.Filter |= StepFilter.DebuggerNonUserCode; if (assemblyFilters != null && assemblyFilters.Count > 0) req.AssemblyFilter = assemblyFilters; try { req.Enabled = true; } catch (NotSupportedException e) { if (vm.Version.AtLeast (2, 19)) //catch NotSupportedException thrown by old version of protocol throw e; } currentStepRequest = req; OnResumed (); vm.Resume (); DequeueEventsForFirstThread (); } catch (CommandException ex) { string reason; switch (ex.ErrorCode) { case ErrorCode.INVALID_FRAMEID: reason = "invalid frame id"; break; case ErrorCode.NOT_SUSPENDED: reason = "VM not suspended"; break; case ErrorCode.ERR_UNLOADED: reason = "AppDomain has been unloaded"; break; case ErrorCode.NO_SEQ_POINT_AT_IL_OFFSET: reason = "no sequence point at the specified IL offset"; break; default: reason = ex.ErrorCode.ToString (); break; } if (outputOptions.ExceptionMessage) OnDebuggerOutput (true, string.Format ("Step request failed: {0}.", reason)); DebuggerLoggingService.LogError ("Step request failed", ex); } catch (Exception ex) { if (outputOptions.ExceptionMessage) OnDebuggerOutput(true, string.Format ("Step request failed: {0}", ex.Message)); DebuggerLoggingService.LogError ("Step request failed", ex); } } private StepFilter ShouldFilterStaticCtor() { var frames = current_thread.GetFrames(); return (frames.Any(f => f.Method.Name == ".cctor" && f.Method.IsSpecialName && f.Method.IsStatic)) ? StepFilter.None : StepFilter.StaticCtor; } void EventHandler () { int? exit_code = null; while (true) { try { EventSet e = vm.GetNextEventSet (); var type = e[0].EventType; if (type == EventType.VMDeath || type == EventType.VMDisconnect) { if (type == EventType.VMDeath && vm.Version.AtLeast (2, 27)) { exit_code = ((VMDeathEvent) e[0]).ExitCode; } break; } HandleEventSet (e); } catch (Exception ex) { if (HasExited) break; if (!HandleException (ex) && outputOptions.ExceptionMessage) OnDebuggerOutput (true, ex.ToString ()); if (ex is VMDisconnectedException || ex is IOException || ex is SocketException) break; } } try { // This is a workaround for a mono bug // Without this call, the process may become zombie in mono < 2.10.2 if (vm.Process != null) vm.Process.WaitForExit (1); } catch (SystemException) { } OnTargetEvent (new TargetEventArgs (TargetEventType.TargetExited) { ExitCode = exit_code }); } protected override bool HandleException (Exception ex) { HideConnectionDialog (); if (HasExited) return true; if (ex is VMDisconnectedException || ex is IOException) { ex = new DisconnectedException (ex); HasExited = true; } else if (ex is SocketException) { ex = new DebugSocketException (ex); HasExited = true; } return base.HandleException (ex); } void HandleEventSet (EventSet es) { var type = es[0].EventType; #if DEBUG_EVENT_QUEUEING if (type != TypeLoadEvent) Console.WriteLine ("pp eventset({0}): {1}", es.Events.Length, es[0]); #endif // If we are currently stopped on a thread, and the break events are on a different thread, we must queue // that event set and dequeue it next time we resume. This eliminates race conditions when multiple threads // hit breakpoints or catchpoints simultaneously. // bool isBreakEvent = type == EventType.Step || type == EventType.Breakpoint || type == EventType.Exception || type == EventType.UserBreak; if (isBreakEvent) { if (current_thread != null && es[0].Thread.Id != current_thread.Id) { QueueBreakEventSet (es.Events); } else { HandleBreakEventSet (es.Events, false); } return; } switch (type) { case EventType.AssemblyLoad: HandleAssemblyLoadEvents (Array.ConvertAll (es.Events, item => (AssemblyLoadEvent)item)); break; case EventType.AppDomainUnload: HandleDomainUnloadEvents (Array.ConvertAll (es.Events, item => (AppDomainUnloadEvent)item)); break; case EventType.VMStart: HandleVMStartEvents (Array.ConvertAll (es.Events, item => (VMStartEvent)item)); break; case EventType.TypeLoad: HandleTypeLoadEvents (Array.ConvertAll (es.Events, item => (TypeLoadEvent)item)); break; case EventType.ThreadStart: HandleThreadStartEvents (Array.ConvertAll (es.Events, item => (ThreadStartEvent)item)); break; case EventType.ThreadDeath: HandleThreadDeathEvents (Array.ConvertAll (es.Events, item => (ThreadDeathEvent)item)); break; case EventType.UserLog: HandleUserLogEvents (Array.ConvertAll (es.Events, item => (UserLogEvent)item)); break; case EventType.MethodUpdate: HandleMethodUpdateEvents (Array.ConvertAll (es.Events, item => (MethodUpdateEvent)item)); break; default: DebuggerLoggingService.LogMessage ("Ignoring unknown debugger event type {0}", type); break; } try { if (!vm.Version.AtLeast (2, 55) || es.SuspendPolicy != SuspendPolicy.None) vm.Resume (); } catch (VMNotSuspendedException) { if (type != EventType.VMStart && vm.Version.AtLeast (2, 2)) throw; } } static bool IsStepIntoRequest (StepEventRequest stepRequest) { return stepRequest.Depth == StepDepth.Into; } static bool IsStepOutRequest (StepEventRequest stepRequest) { return stepRequest.Depth == StepDepth.Out; } static bool IsPropertyOrOperatorMethod (MethodMirror method) { string name = method.Name; return method.IsSpecialName && (name.StartsWith ("get_", StringComparison.Ordinal) || name.StartsWith ("set_", StringComparison.Ordinal) || name.StartsWith ("op_", StringComparison.Ordinal)); } static bool IsCompilerGenerated (MethodMirror method) { foreach (var attr in method.GetCustomAttributes(false)) { if (attr.Constructor.DeclaringType.FullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute") return true; } return false; } bool IsUserAssembly (AssemblyMirror assembly) { if (userAssemblyNames == null) return true; var name = assembly.GetName ().FullName; foreach (var n in userAssemblyNames) { if (n == name) return true; } return false; } bool IsAutoGeneratedFrameworkEnumerator (TypeMirror type) { if (IsUserAssembly (type.Assembly)) return false; if (!SoftDebuggerAdaptor.IsGeneratedType (type)) return false; foreach (var iface in type.GetInterfaces ()) { if (iface.Namespace == "System.Collections" && iface.Name == "IEnumerator") return true; } return false; } bool StepThrough (MethodMirror method) { if (Options.ProjectAssembliesOnly && !IsUserAssembly (method.DeclaringType.Assembly)) return true; //With Sdb 2.30 this logic was moved to Runtime no need to spend time on checking this if (!vm.Version.AtLeast (2, 30)) { if (vm.Version.AtLeast (2, 21)) { foreach (var attr in method.GetCustomAttributes (false)) { var attrName = attr.Constructor.DeclaringType.FullName; switch (attrName) { case "System.Diagnostics.DebuggerHiddenAttribute": return true; case "System.Diagnostics.DebuggerStepThroughAttribute": return true; case "System.Diagnostics.DebuggerNonUserCodeAttribute": return Options.ProjectAssembliesOnly; } } } if (Options.ProjectAssembliesOnly) { foreach (var attr in method.DeclaringType.GetCustomAttributes (false)) { var attrName = attr.Constructor.DeclaringType.FullName; if (attrName == "System.Diagnostics.DebuggerNonUserCodeAttribute") return Options.ProjectAssembliesOnly; } } } return false; } bool ContinueOnStepInto (MethodMirror method) { if (vm.Version.AtLeast (2, 21)) { foreach (var attr in method.GetCustomAttributes (false)) { var attrName = attr.Constructor.DeclaringType.FullName; if (attrName == "System.Diagnostics.DebuggerStepperBoundaryAttribute") return true; } } return false; } bool IgnoreBreakpoint (MethodMirror method) { if (vm.Version.AtLeast (2, 21)) { foreach (var attr in method.GetCustomAttributes (false)) { var attrName = attr.Constructor.DeclaringType.FullName; switch (attrName) { case "System.Diagnostics.DebuggerHiddenAttribute": return true; case "System.Diagnostics.DebuggerStepThroughAttribute": return true; case "System.Diagnostics.DebuggerNonUserCodeAttribute": return Options.ProjectAssembliesOnly; case "System.Diagnostics.DebuggerStepperBoundaryAttribute": return true; } } } if (Options.ProjectAssembliesOnly) { foreach (var attr in method.DeclaringType.GetCustomAttributes (false)) { var attrName = attr.Constructor.DeclaringType.FullName; if (attrName == "System.Diagnostics.DebuggerNonUserCodeAttribute") return Options.ProjectAssembliesOnly; } } return false; } /// /// Checks all frames in thread where exception occured and if any frame has user code it returns true. /// Also notice that this method already check if Options.ProjectAssembliesOnly==false /// bool ExceptionInUserCode (ExceptionEvent ev) { // this is just optimization to prevent need to fetch Frames if (Options.ProjectAssembliesOnly == false) return true; foreach (var frame in ev.Thread.GetFrames ()) { if (!IsExternalCode (frame)) return true; } return false; } bool ShouldIgnore (ExceptionEvent ev) { BreakInfo binfo; if (!breakpoints.TryGetValue (ev.Request, out binfo)) return false; var cp = binfo.BreakEvent as Catchpoint; if (cp == null) return false; var backtrace = GetThreadBacktrace (ev.Thread); if (backtrace.FrameCount == 0) { return cp.ShouldIgnore (ev.Exception.Type.FullName, null); } else { var frame = backtrace.GetFrame (0); return cp.ShouldIgnore (ev.Exception.Type.FullName, frame.GetLocationSignature ()); } } void HandleBreakEventSet (Event[] es, bool dequeuing) { if (dequeuing && HasExited) return; TargetEventType etype = TargetEventType.TargetStopped; ObjectMirror exception = null; BreakEvent breakEvent = null; bool redoCurrentStep = false; bool steppedInto = false; bool steppedOut = false; bool resume = true; BreakInfo binfo; if (es [0].EventType == EventType.Exception) { var bad = es.FirstOrDefault (ee => ee.EventType != EventType.Exception); if (bad != null) throw new Exception ("Catchpoint eventset had unexpected event type " + bad.GetType ()); var ev = (ExceptionEvent)es [0]; exception = ev.Exception; if (ev.Request == unhandledExceptionRequest) { etype = TargetEventType.UnhandledException; if (exception.Type.FullName != "System.Threading.ThreadAbortException") resume = false; } else { // Set the exception for this thread so that CatchPoint Print message(tracing) of {$exception} works activeExceptionsByThread [es[0].Thread.ThreadId] = exception; if (ExceptionInUserCode(ev) && !ShouldIgnore(ev) && !HandleBreakpoint (es [0].Thread, ev.Request)) { etype = TargetEventType.ExceptionThrown; resume = false; } // Remove exception from the thread so that when the program stops due to stepFinished/programPause/breakPoint... // we don't have on out-dated exception(setting and unsetting few lines later is needed because it's used inside HandleBreakpoint) activeExceptionsByThread.Remove (es[0].Thread.ThreadId); // Get the breakEvent so that we can check if we should ignore it later if (breakpoints.TryGetValue (ev.Request, out binfo)) breakEvent = binfo.BreakEvent; } } else { //always need to evaluate all breakpoints, some might be tracepoints or conditional bps with counters foreach (Event e in es) { if (e.EventType == EventType.Breakpoint) { var be = (BreakpointEvent) e; var hasBreakInfo = breakpoints.TryGetValue (be.Request, out binfo); if (!HandleBreakpoint (e.Thread, be.Request)) { etype = TargetEventType.TargetHitBreakpoint; autoStepInto = false; resume = false; if (hasBreakInfo) breakEvent = binfo.BreakEvent; } if (hasBreakInfo) { if (currentStepRequest != null && currentStepRequest.Depth != StepDepth.Out && binfo.Location.ILOffset == currentAddress && e.Thread.Id == currentStepRequest.Thread.Id && currentStackDepth == e.Thread.GetFrames ().Length) redoCurrentStep = true; } } else if (e.EventType == EventType.Step) { var stepRequest = e.Request as StepEventRequest; steppedInto = IsStepIntoRequest (stepRequest); steppedOut = IsStepOutRequest (stepRequest); etype = TargetEventType.TargetStopped; resume = false; } else if (e.EventType == EventType.UserBreak) { etype = TargetEventType.TargetStopped; autoStepInto = false; resume = false; } else { throw new Exception ("Break eventset had unexpected event type " + e.GetType ()); } } } if (redoCurrentStep) { StepDepth depth = currentStepRequest.Depth; StepSize size = currentStepRequest.Size; current_thread = recent_thread = es[0].Thread; currentStepRequest.Enabled = false; currentStepRequest = null; Step (depth, size); } else if (resume) { // all breakpoints were conditional and evaluated as false current_thread = null; vm.Resume (); DequeueEventsForFirstThread (); } else { if (currentStepRequest != null) { currentStepRequest.Enabled = false; currentStepRequest = null; } current_thread = recent_thread = es[0].Thread; if (exception != null) activeExceptionsByThread [current_thread.ThreadId] = exception; var backtrace = GetThreadBacktrace (current_thread); bool stepInto = false; bool stepOut = false; if (backtrace.FrameCount > 0) { var frame = backtrace.GetFrame (0) as SoftDebuggerStackFrame; currentAddress = frame != null ? frame.Address : -1; currentStackDepth = backtrace.FrameCount; if (frame != null && steppedInto) { if (ContinueOnStepInto (frame.StackFrame.Method)) { current_thread = null; vm.Resume (); DequeueEventsForFirstThread (); return; } if (StepThrough (frame.StackFrame.Method)) { // The method has a Debugger[Hidden,StepThrough,NonUserCode]Attribute on it // Keep calling StepInto until we land somewhere without one of these attributes stepInto = true; } else if (frame.StackFrame.ILOffset == 0 && IsPropertyOrOperatorMethod (frame.StackFrame.Method) && (Options.StepOverPropertiesAndOperators || IsCompilerGenerated (frame.StackFrame.Method))) { //We want to skip property only when we just stepped into property(ILOffset==0) //so if user puts breakpoint inside property we don't want to StepOut for him when he steps after breakpoint is hit //mcs.exe and Roslyn are also emmiting Sequence point inside auto-properties so breakpoint can be placed //we want to always skip auto-properties also when StepOverProperties is disabled hence "|| IsCompilerGenerated" // We will want to call StepInto once StepOut returns... autoStepInto = true; stepOut = true; } else if (IsAutoGeneratedFrameworkEnumerator (frame.StackFrame.Method.DeclaringType)) { // User asked to step in, but we landed in an autogenerated type (probably an iterator) autoStepInto = true; stepOut = true; } } else if (etype == TargetEventType.TargetHitBreakpoint && breakEvent != null && !breakEvent.NonUserBreakpoint && IgnoreBreakpoint (frame.StackFrame.Method)) { current_thread = null; vm.Resume (); DequeueEventsForFirstThread (); return; } } if (stepOut) { Step (StepDepth.Out, StepSize.Min); } else if (stepInto) { Step (StepDepth.Into, StepSize.Min); } else if (steppedOut && autoStepInto) { autoStepInto = false; Step (StepDepth.Into, StepSize.Min); } else { var args = new TargetEventArgs (etype); args.Process = OnGetProcesses () [0]; args.Thread = GetThread (args.Process, current_thread); args.Backtrace = backtrace; args.BreakEvent = breakEvent; OnTargetEvent (args); } } } public void AddUserAssembly(string userAssembly) { if (userAssemblyNames != null && !userAssemblyNames.Contains(userAssembly)) userAssemblyNames.Add(userAssembly); } void HandleAssemblyLoadEvents (AssemblyLoadEvent[] events) { var asm = events [0].Assembly; if (events.Length > 1 && events.Any (a => a.Assembly != asm)) throw new InvalidOperationException ("Simultaneous AssemblyLoadEvent for multiple assemblies"); HandleAssemblyLoaded (asm); RegisterAssembly (asm); bool isExternal; isExternal = !UpdateAssemblyFilters (asm) && userAssemblyNames != null; if (outputOptions.ModuleLoaded) { string flagExt = isExternal ? " [External]" : ""; OnDebuggerOutput (false, string.Format ("Loaded assembly: {0}{1}\n", asm.Location, flagExt)); } } private void HandleAssemblyLoaded (AssemblyMirror asm) { var name = asm.GetName (); var assemblyObject = asm.GetAssemblyObject (); bool isDynamic = asm.IsDynamic; string assemblyName; bool hasSymbol; if (!isDynamic) { var metaData = asm?.GetMetadata() ?? null; assemblyName = metaData?.MainModule?.Name ?? string.Empty; hasSymbol = metaData?.MainModule?.HasSymbols ?? false; } else { assemblyName = string.Empty; hasSymbol = false; } var assembly = new Assembly ( assemblyName, asm?.Location ?? string.Empty, true, hasSymbol, string.Empty, string.Empty, -1, name?.Version?.Major.ToString () ?? string.Empty, // TODO: module time stamp string.Empty, assemblyObject?.Address.ToString () ?? string.Empty, string.Format ("[{0}]{1}", asm?.VirtualMachine?.TargetProcess?.Id ?? -1, asm?.VirtualMachine?.TargetProcess?.ProcessName ?? string.Empty), asm?.Domain?.FriendlyName ?? string.Empty, asm?.VirtualMachine?.TargetProcess?.Id ?? -1, hasSymbol, isDynamic ); OnAssemblyLoaded (assembly); } void RegisterAssembly (AssemblyMirror asm) { var domain = vm.Version.AtLeast (2, 45) ? asm.Domain : asm.GetAssemblyObject ().Domain; if (domainAssembliesToUnload.TryGetValue (domain, out var asmList)) { asmList.Add (asm); } else { domainAssembliesToUnload.Add (domain, new HashSet (new [] { asm })); } } void HandleDomainUnloadEvents (AppDomainUnloadEvent [] events) { var domain = events [0].Domain; if (events.Length > 1 && events.Any (a => a.Domain != domain)) throw new InvalidOperationException ("Simultaneous DomainUnloadEvents for multiple domains"); if (domainAssembliesToUnload.TryGetValue (domain, out var asmList)) { foreach (var asm in asmList) { HandleAssemblyUnloadEvents (asm); } } } void HandleAssemblyUnloadEvents (AssemblyMirror asm) { assemblyFilters?.Remove (asm); // Mark affected breakpoints as pending again var affectedBreakpoints = new List> (breakpoints.Where (x => x.Value.Requests.TryGetValue (x.Key, out var a) && a == asm)); foreach (var breakpoint in affectedBreakpoints) { var affectedRequests = breakpoint.Value.Requests.Where (r => r.Value == asm).ToArray (); foreach (var item in affectedRequests) { breakpoint.Value.Requests.Remove (item.Key); breakpoints.Remove (breakpoint.Key); } if (!breakpoint.Value.Requests.Any ()) { string file = breakpoint.Value.Location.SourceFile; int line = breakpoint.Value.Location.LineNumber; OnDebuggerOutput (false, string.Format ("Re-pending breakpoint at {0}:{1}\n", file, line)); breakpoint.Value.SetStatus (BreakEventStatus.NotBound, "Assembly unloaded"); } } // Remove affected types from the loaded types list var affectedTypes = types.SelectMany (l => l.Value).Where (t => t.Assembly == asm).ToArray (); foreach (var tm in affectedTypes) { List typesList; if (tm.IsNested) { if (aliases.TryGetValue (NestedTypeNameToAlias (tm.FullName), out typesList)) { typesList.Remove (tm); if (typesList.Count == 0) aliases.Remove (NestedTypeNameToAlias (tm.FullName)); } } if (types.TryGetValue (tm.FullName, out typesList)) { typesList.Remove (tm); if (typesList.Count == 0) types.Remove (tm.FullName); } } lock (source_to_type) { foreach (var pair in source_to_type) pair.Value.RemoveAll (m => m.Assembly == asm); } if (outputOptions.ModuleUnoaded) OnDebuggerOutput (false, string.Format ("Unloaded assembly: {0}\n", GetAssemblyLocation (asm) ?? "")); } static string GetAssemblyLocation (AssemblyMirror asm) { if (asm == null) return null; try { return asm.Location; } catch (CommandException ex) { if (ex.ErrorCode != ErrorCode.ERR_UNLOADED) throw ex; return null; } } void HandleVMStartEvents (VMStartEvent[] events) { var thread = events [0].Thread; if (events.Length > 1) throw new InvalidOperationException ("Simultaneous VMStartEvents"); OnStarted (new ThreadInfo (0, GetId (thread), GetThreadName (thread), null)); //HACK: 2.6.1 VM doesn't emit type load event, so work around it var t = vm.RootDomain.Corlib.GetType ("System.Exception", false, false); if (t != null) { ResolveBreakpoints (t); } } void HandleTypeLoadEvents (TypeLoadEvent[] events) { var type = events [0].Type; if (events.Length > 1 && events.Any (a => a.Type != type)) throw new InvalidOperationException ("Simultaneous TypeLoadEvents for multiple types"); List typesList; if (!(types.TryGetValue (type.FullName, out typesList) && typesList.Contains (type))) ResolveBreakpoints (type); } void HandleThreadStartEvents (ThreadStartEvent[] events) { current_threads = null; var thread = events [0].Thread; if (events.Length > 1 && events.Any (a => a.Thread != thread)) throw new InvalidOperationException ("Simultaneous ThreadStartEvents for multiple threads"); var name = GetThreadName (thread); var id = GetId (thread); OnDebuggerOutput (false, string.Format ("Thread started: {0} #{1}\n", name, id)); OnTargetEvent (new TargetEventArgs (TargetEventType.ThreadStarted) { Thread = new ThreadInfo (0, id, name, null), }); } void HandleThreadDeathEvents (ThreadDeathEvent[] events) { current_threads = null; ThreadMirror thread; try { thread = events [0].Thread; } catch (ObjectCollectedException) { // A 2.1 debugger-agent can send a ThreadDeath event for a ThreadMirror // already collected, in that case just allow the session to continue if (!vm.Version.AtLeast (2, 2)) return; // Otherwise just report the error throw; } if (events.Length > 1 && events.Any (a => a.Thread != thread)) throw new InvalidOperationException ("Simultaneous ThreadDeathEvents for multiple threads"); var name = GetThreadName (thread); var id = GetId (thread); if (outputOptions.ThreadExited) OnDebuggerOutput (false, string.Format ("Thread finished: {0} #{1}\n", name, id)); OnTargetEvent (new TargetEventArgs (TargetEventType.ThreadStopped) { Thread = new ThreadInfo (0, id, name, null), }); } void HandleUserLogEvents (UserLogEvent[] events) { foreach (var ul in events) OnTargetDebug (ul.Level, ul.Category, ul.Message); } void HandleMethodUpdateEvents(MethodUpdateEvent[] methods) { foreach (var method in methods) //add new methods to type { method.GetMethod ().ClearCachedLocalsDebugInfo (); method.GetMethod ().DeclaringType.AddMethodIfNotExist (method.GetMethod ()); } foreach (var breakpoint in breakpoints) { Breakpoint bp = ((Breakpoint)breakpoint.Value.BreakEvent); if (bp.UpdatedByEnC) { bool dummy = false; var l = FindLocationByMethod (breakpoint.Value.Location.Method, bp.FileName, bp.Line, bp.Column, ref dummy); if (l != null) { breakpoint.Value.Location = l; UpdateBreakpoint (bp, breakpoint.Value); bp.UpdatedByEnC = false; } } } foreach (var bp in pending_bes) { if (bp.Status != BreakEventStatus.Bound) { foreach (var location in FindLocationsByFile (bp.Breakpoint.FileName, bp.Breakpoint.Line, bp.Breakpoint.Column, out _, out bool insideLoadedRange)) { OnDebuggerOutput (false, string.Format ("Resolved pending breakpoint at '{0}:{1},{2}' to {3} [0x{4:x5}].\n", bp.Breakpoint.FileName, bp.Breakpoint.Line, bp.Breakpoint.Column, GetPrettyMethodName (location.Method), location.ILOffset)); bp.Location = location; InsertBreakpoint (bp.Breakpoint, bp); bp.SetStatus (BreakEventStatus.Bound, null); } } } } public ObjectMirror GetExceptionObject (ThreadMirror thread) { ObjectMirror obj; return activeExceptionsByThread.TryGetValue (thread.ThreadId, out obj) ? obj : null; } void QueueBreakEventSet (Event[] eventSet) { #if DEBUG_EVENT_QUEUEING Console.WriteLine ("qq eventset({0}): {1}", eventSet.Length, eventSet[0]); #endif var events = new List (eventSet); lock (queuedEventSets) { queuedEventSets.AddLast (events); } } void RemoveQueuedBreakEvents (Dictionary requests) { int resume = 0; lock (queuedEventSets) { var node = queuedEventSets.First; while (node != null) { List q = node.Value; for (int i = 0; i < q.Count; i++) { foreach (var request in requests.Keys) { if (q[i].Request == request) { q.RemoveAt (i--); break; } } } if (q.Count == 0) { var d = node; node = node.Next; queuedEventSets.Remove (d); resume++; } else { node = node.Next; } } } for (int i = 0; i < resume; i++) vm.Resume (); } void DequeueEventsForFirstThread () { List> dequeuing; lock (queuedEventSets) { if (queuedEventSets.Count < 1) return; dequeuing = new List> (); var node = queuedEventSets.First; //making this the current thread means that all events from other threads will get queued current_thread = node.Value[0].Thread; while (node != null) { if (node.Value[0].Thread.Id == current_thread.Id) { var d = node; node = node.Next; dequeuing.Add (d.Value); queuedEventSets.Remove (d); } else { node = node.Next; } } } #if DEBUG_EVENT_QUEUEING foreach (var e in dequeuing) Console.WriteLine ("dq eventset({0}): {1}", e.Count, e[0]); #endif //firing this off in a thread prevents possible infinite recursion ThreadPool.QueueUserWorkItem (delegate { if (!HasExited) { foreach (var es in dequeuing) { try { HandleBreakEventSet (es.ToArray (), true); } catch (Exception ex) { if (!HandleException (ex) && outputOptions.ExceptionMessage) OnDebuggerOutput (true, ex.ToString ()); if (ex is VMDisconnectedException || ex is IOException || ex is SocketException) { OnTargetEvent (new TargetEventArgs (TargetEventType.TargetExited)); break; } } } } }); } bool HandleBreakpoint (ThreadMirror thread, EventRequest er) { BreakInfo binfo; if (!breakpoints.TryGetValue (er, out binfo)) return false; var bp = binfo.BreakEvent; if (bp == null) return false; binfo.IncrementHitCount (); if (!binfo.HitCountReached) return true; if (!string.IsNullOrEmpty (bp.ConditionExpression)) { string res = EvaluateExpression (thread, bp.ConditionExpression, bp); if (bp.BreakIfConditionChanges) { if (res == binfo.LastConditionValue) return true; binfo.LastConditionValue = res; } else { if (res == null || res.ToLowerInvariant () != "true") return true; } } if ((bp.HitAction & HitAction.CustomAction) != HitAction.None) { if (HandleProcessStartHook (thread, bp)) return true; // If custom action returns true, execution must continue return binfo.RunCustomBreakpointAction (bp.CustomActionId); } if ((bp.HitAction & HitAction.PrintTrace) != HitAction.None) { OnTargetDebug (0, "", "Breakpoint reached: " + binfo.FileName + ":" + binfo.Location.LineNumber + Environment.NewLine); } if ((bp.HitAction & HitAction.PrintExpression) != HitAction.None) { string exp = EvaluateTrace (thread, bp.TraceExpression); binfo.UpdateLastTraceValue (exp); } // Continue execution if we don't have break action. return (bp.HitAction & HitAction.Break) == HitAction.None; } string EvaluateTrace (ThreadMirror thread, string exp) { var sb = new StringBuilder (); int last = 0; int i = exp.IndexOf ('{'); while (i != -1) { if (i < exp.Length - 1 && exp [i+1] == '{') { sb.Append (exp, last, i - last + 1); last = i + 2; i = exp.IndexOf ('{', i + 2); continue; } int j = exp.IndexOf ('}', i + 1); if (j == -1) break; string se = exp.Substring (i + 1, j - i - 1); se = EvaluateExpression (thread, se, null); sb.Append (exp, last, i - last); sb.Append (se); last = j + 1; i = exp.IndexOf ('{', last); } sb.Append (exp, last, exp.Length - last); return sb.ToString (); } static SourceLocation GetSourceLocation (StackFrame frame) { return new SourceLocation (frame.Method.Name, frame.FileName, frame.LineNumber, frame.ColumnNumber, frame.EndLineNumber, frame.EndColumnNumber); } static string FormatSourceLocation (BreakEvent breakEvent) { var bp = breakEvent as Breakpoint; if (bp == null || string.IsNullOrEmpty (bp.FileName)) return null; var location = Path.GetFileName (bp.FileName); if (bp.OriginalLine > 0) { location += ":" + bp.OriginalLine; if (bp.OriginalColumn > 0) location += "," + bp.OriginalColumn; } return location; } static bool IsBoolean (ValueReference vr) { if (vr.Type is Type && ((Type) vr.Type) == typeof (bool)) return true; if (vr.Type is TypeMirror && ((TypeMirror) vr.Type).FullName == "System.Boolean") return true; return false; } string EvaluateExpression (ThreadMirror thread, string expression, BreakEvent bp) { try { var frames = thread.GetFrames (); if (frames.Length == 0) return string.Empty; EvaluationOptions ops = Options.EvaluationOptions.Clone (); ops.AllowTargetInvoke = true; ops.EllipsizedLength = 1000; var ctx = new SoftEvaluationContext (this, frames[0], ops); if (bp != null) { // validate conditional breakpoint expressions so that we can provide error reporting to the user var vr = ctx.Evaluator.ValidateExpression (ctx, expression); if (!vr.IsValid) { string message = string.Format ("Invalid expression in conditional breakpoint. {0}", vr.Message); string location = FormatSourceLocation (bp); if (!string.IsNullOrEmpty (location)) message = location + ": " + message; OnDebuggerOutput (true, message); return string.Empty; } // resolve types... if (ctx.SourceCodeAvailable) expression = ctx.Evaluator.Resolve (this, GetSourceLocation (frames[0]), expression); } ValueReference val = ctx.Evaluator.Evaluate (ctx, expression); if (bp != null && !bp.BreakIfConditionChanges && !IsBoolean (val)) { string message = string.Format ("Expression in conditional breakpoint did not evaluate to a boolean value: {0}", bp.ConditionExpression); string location = FormatSourceLocation (bp); if (!string.IsNullOrEmpty (location)) message = location + ": " + message; OnDebuggerOutput (true, message); return string.Empty; } return val.CreateObjectValue (false).Value; } catch (EvaluatorException ex) { string message; if (bp != null) { message = string.Format ("Failed to evaluate expression in conditional breakpoint. {0}", ex.Message); string location = FormatSourceLocation (bp); if (!string.IsNullOrEmpty (location)) message = location + ": " + message; } else { message = ex.ToString (); } OnDebuggerOutput (true, message); return string.Empty; } catch (Exception ex) { OnDebuggerOutput (true, ex.ToString ()); return string.Empty; } } static string NestedTypeNameToAlias (string typeName) { int index = typeName.IndexOfAny (new [] { '[', ',' }); if (index == -1) return typeName.Replace ('+', '.'); var prefix = typeName.Substring (0, index).Replace ('+', '.'); var suffix = typeName.Substring (index); return prefix + suffix; } void ProcessType (TypeMirror t) { string typeName = t.FullName; List typesList; if (types.TryGetValue (typeName, out typesList) && typesList.Contains (t)) return; RegisterAssembly (t.Assembly); if (t.IsNested) { var alias = NestedTypeNameToAlias (typeName); List aliasesList; if (aliases.TryGetValue (alias, out aliasesList)) { aliasesList.Add (t); } else { aliases [alias] = new List (new [] { t }); } } types [typeName] = typesList ?? new List (); types [typeName].Add (t); //get the source file paths //full paths, from GetSourceFiles (true), are only supported by sdb protocol 2.2 and later string[] sourceFiles; if (vm.Version.AtLeast (2, 2)) { sourceFiles = t.GetSourceFiles ().Select ((fullPath) => Path.GetFileName (fullPath)).ToArray (); } else { sourceFiles = t.GetSourceFiles (); //HACK: if mdb paths are windows paths but the sdb agent is on unix, it won't map paths to filenames correctly if (IsWindows) { for (int i = 0; i < sourceFiles.Length; i++) { string s = sourceFiles[i]; if (s != null && !s.StartsWith ("/", StringComparison.Ordinal)) sourceFiles[i] = Path.GetFileName (s); } } } for (int n = 0; n < sourceFiles.Length; n++) sourceFiles[n] = NormalizePath (sourceFiles[n]); foreach (string s in sourceFiles) { lock (source_to_type) { if (source_to_type.TryGetValue (s, out typesList)) { typesList.Add (t); } else { typesList = new List (); typesList.Add (t); source_to_type[s] = typesList; } } } type_to_source [t] = sourceFiles; } static string[] GetParamTypes (MethodMirror method) { var paramTypes = new List (); foreach (var param in method.GetParameters ()) paramTypes.Add (param.ParameterType.CSharpName); return paramTypes.ToArray (); } string GetPrettyMethodName (MethodMirror method) { var name = new StringBuilder (); name.Append (Adaptor.GetDisplayTypeName (method.ReturnType.FullName)); name.Append (" "); name.Append (Adaptor.GetDisplayTypeName (method.DeclaringType.FullName)); name.Append ("."); name.Append (method.Name); if (method.VirtualMachine.Version.AtLeast (2, 12)) { if (method.IsGenericMethodDefinition || method.IsGenericMethod) { name.Append ("<"); if (method.VirtualMachine.Version.AtLeast (2, 15)) { var argTypes = method.GetGenericArguments (); for (int i = 0; i < argTypes.Length; i++) { if (i != 0) name.Append (", "); name.Append (Adaptor.GetDisplayTypeName (argTypes[i].FullName)); } } name.Append (">"); } } name.Append (" ("); var @params = method.GetParameters (); for (int i = 0; i < @params.Length; i++) { if (i != 0) name.Append (", "); if (@params[i].Attributes.HasFlag (ParameterAttributes.Out)) { if (@params[i].Attributes.HasFlag (ParameterAttributes.In)) name.Append ("ref "); else name.Append ("out "); } name.Append (Adaptor.GetDisplayTypeName (@params[i].ParameterType.FullName)); name.Append (" "); name.Append (@params[i].Name); } name.Append (")"); return name.ToString (); } void ResolveBreakpoints (TypeMirror type) { Location loc; ProcessType (type); // First, resolve FunctionBreakpoints BreakInfo [] tempPendingBes; lock (pending_bes) { tempPendingBes = pending_bes.Where (b => b.BreakEvent is FunctionBreakpoint).ToArray (); } foreach (var bi in tempPendingBes) { if (CheckTypeName (type, bi.TypeName)) { var bp = (FunctionBreakpoint)bi.BreakEvent; foreach (var method in FindMethodsByName (bp.FunctionName, bp.ParamTypes)) { ResolveFunctionBreakpoint (bi, bp, method); } } } // Now resolve normal Breakpoints foreach (string s in type_to_source [type]) { lock (pending_bes) { tempPendingBes = pending_bes.Where (b => (b.BreakEvent is Breakpoint) && !(b.BreakEvent is FunctionBreakpoint)).ToArray (); } foreach (var bi in tempPendingBes) { var bp = (Breakpoint) bi.BreakEvent; if (PathComparer.Compare (Path.GetFileName (bp.FileName), s) == 0) { bool insideLoadedRange; bool genericMethod; if (bi.BreakEvent is InstructionBreakpoint) { loc = FindLocationByILOffset ((InstructionBreakpoint)bi.BreakEvent, bp.FileName, out genericMethod, out insideLoadedRange); } else { loc = FindLocationByType (type, bp.FileName, bp.Line, bp.Column, out genericMethod, out insideLoadedRange); } if (loc != null) { OnDebuggerOutput (false, string.Format ("Resolved pending breakpoint at '{0}:{1},{2}' to {3} [0x{4:x5}].\n", s, bp.Line, bp.Column, GetPrettyMethodName (loc.Method), loc.ILOffset)); ResolvePendingBreakpoint (bi, loc); } else { if (insideLoadedRange) { bi.SetStatus (BreakEventStatus.Invalid, null); } } } } } // Thirdly, resolve pending catchpoints lock (pending_bes) { tempPendingBes = pending_bes.Where (b => b.BreakEvent is Catchpoint).ToArray (); } foreach (var bi in tempPendingBes) { var cp = (Catchpoint) bi.BreakEvent; if (cp.ExceptionName == type.FullName) { ResolvePendingCatchpoint (bi, type); } } } bool ResolveFunctionBreakpoint (BreakInfo bi, FunctionBreakpoint bp, MethodMirror method) { var loc = method.Locations.FirstOrDefault (); string paramList = "(" + string.Join (", ", bp.ParamTypes ?? GetParamTypes (method)) + ")"; if (loc != null) { bi.Location = loc; InsertBreakpoint (bp, bi); OnDebuggerOutput (false, string.Format ("Resolved pending breakpoint for '{0}{1}' to {2}:{3} [0x{4:x5}].\n", bp.FunctionName, paramList, loc.SourceFile, loc.LineNumber, loc.ILOffset)); } else { InsertBreakpoint (bp, bi, method, 0); OnDebuggerOutput (false, string.Format ("Resolved pending breakpoint for '{0}{1}' to [0x0](no debug symbols).\n", bp.FunctionName, paramList)); } bi.SetStatus (BreakEventStatus.Bound, null); // Note: if the type or method is generic, there may be more instances so don't assume we are done resolving the breakpoint return bp.ParamTypes != null && !method.DeclaringType.IsGenericType && !IsGenericMethod (method); } internal static string NormalizePath (string path) { if (!IsWindows && (path.StartsWith ("\\", StringComparison.Ordinal) || (path.Length > 3 && path [1] == ':' && path [2] == '\\'))) return path.Replace ('\\', '/'); return path; } [DllImport ("libc")] static extern IntPtr realpath (string path, IntPtr buffer); static string ResolveFullPath (string path) { if (IsWindows) return Path.GetFullPath (path); const int PATHMAX = 4096 + 1; IntPtr buffer = IntPtr.Zero; try { buffer = Marshal.AllocHGlobal (PATHMAX); var result = realpath (path, buffer); var realPath = result == IntPtr.Zero ? "" : Marshal.PtrToStringAuto (buffer); if (string.IsNullOrEmpty(realPath) && !File.Exists(path)) { // if the file does not exist then `realpath` will return empty string // default to what we would do if calling this on windows realPath = Path.GetFullPath (path); } return realPath; } finally { if (buffer != IntPtr.Zero) Marshal.FreeHGlobal (buffer); } } static string ResolveSymbolicLink (string path) { if (path.Length == 0) return path; if (IsWindows) return ResolveWindowsSymbolicLink (path); return ResolveUnixSymbolicLink (path); } static string ResolveWindowsSymbolicLink (string path) { return Path.GetFullPath (path); } static string ResolveUnixSymbolicLink (string path) { try { var alreadyVisted = new HashSet (); while (true) { if (alreadyVisted.Contains (path)) return string.Empty; alreadyVisted.Add (path); var linkInfo = new Mono.Unix.UnixSymbolicLinkInfo (path); if (linkInfo.IsSymbolicLink && linkInfo.HasContents) { string contentsPath = linkInfo.ContentsPath; if (!Path.IsPathRooted (contentsPath)) path = Path.Combine (Path.GetDirectoryName (path), contentsPath); else path = contentsPath; path = ResolveFullPath (path); continue; } path = Path.Combine (ResolveSymbolicLink (Path.GetDirectoryName (path)), Path.GetFileName (path)); return ResolveFullPath (path); } } catch { return path; } } static bool PathsAreEqual (string p1, string p2) { if (string.IsNullOrWhiteSpace (p1) || string.IsNullOrWhiteSpace (p2)) return false; if (PathComparer.Compare (p1, p2) == 0) return true; var rp1 = ResolveSymbolicLink (p1); var rp2 = ResolveSymbolicLink (p2); return PathComparer.Compare (rp1, rp2) == 0; } Location FindLocationByMethod (MethodMirror method, string file, int line, int column, ref bool insideTypeRange) { int rangeFirstLine = int.MaxValue; int rangeLastLine = -1; Location target = null; //Console.WriteLine ("FindLocationByMethod: method = {0}, file = {1}, line = {2}, column = {3}", method.Name, Path.GetFileName (file), line, column); foreach (var location in method.Locations) { var srcFile = location.SourceFile; //Console.WriteLine ("\tChecking location {0}:{1},{2}...", Path.GetFileName(srcFile), location.LineNumber, location.ColumnNumber); //Check if file names match if (srcFile != null && PathComparer.Compare (Path.GetFileName (NormalizePath (srcFile)), Path.GetFileName (file)) == 0) { //Check if full path match(we don't care about md5 if full path match): //1. For backward compatibility //2. If full path matches user himself probably modified code and is aware of modifications //OR if md5 match, useful for alternative location files with breakpoints if (!PathsAreEqual (NormalizePath (srcFile), file) && !SourceLocation.CheckFileHash (file, location.SourceFileHash)) continue; if (location.LineNumber < rangeFirstLine) rangeFirstLine = location.LineNumber; if (location.LineNumber > rangeLastLine) rangeLastLine = location.LineNumber; if (line >= rangeFirstLine && line <= rangeLastLine) insideTypeRange = true; if (line == location.LineNumber && (column <= 1 || column == location.ColumnNumber)) { if (target != null) { if (location.ILOffset < target.ILOffset) target = location; } else { target = location; } } } else { rangeFirstLine = int.MaxValue; rangeLastLine = -1; } } return target; } Location FindLocationByType (TypeMirror type, string file, int line, int column, out bool genericMethod, out bool insideTypeRange) { var methodInsideTypeRange = false; Location target = null; insideTypeRange = false; genericMethod = false; foreach (var method in type.GetMethods ()) { Location location = null; if ((location = FindLocationByMethod (method, file, line, column, ref methodInsideTypeRange)) != null) { insideTypeRange |= methodInsideTypeRange;//If any method returns true return true if (target != null) { // Use the location with the lowest ILOffset if (location.ILOffset < target.ILOffset) { genericMethod = IsGenericMethod (method); target = location; } } else { genericMethod = IsGenericMethod (method); target = location; } } } return target; } void ResolvePendingBreakpoint (BreakInfo bi, Location l) { bi.Location = l; InsertBreakpoint ((Breakpoint) bi.BreakEvent, bi); bi.SetStatus (BreakEventStatus.Bound, null); } void ResolvePendingCatchpoint (BreakInfo bi, TypeMirror type) { InsertCatchpoint ((Catchpoint) bi.BreakEvent, bi, type); bi.SetStatus (BreakEventStatus.Bound, null); } bool UpdateAssemblyFilters (AssemblyMirror asm) { var name = asm.GetName ().FullName; bool found = false; if (userAssemblyNames != null) { //HACK: not sure how else to handle xsp-compiled pages if (name.StartsWith ("App_", StringComparison.Ordinal)) { found = true; } else { foreach (var n in userAssemblyNames) { if (n == name) { found = true; break; } } } } if (found) { assemblyFilters.Add (asm); return true; } return false; } internal void WriteDebuggerOutput (bool isError, string msg) { OnDebuggerOutput (isError, msg); } protected override void OnSetActiveThread (long processId, long threadId) { } protected override void OnStepInstruction () { Step (StepDepth.Into, StepSize.Min); } protected override void OnStepLine () { Step (StepDepth.Into, StepSize.Line); } protected override void OnStop () { vm.Suspend (); //emit a stop event at the current position of the most recent thread //we use "getprocesses" instead of "ongetprocesses" because it attaches the process to the session //using private Mono.Debugging API, so our thread/backtrace calls will cache stuff that will get used later var process = GetProcesses () [0]; EnsureRecentThreadIsValid (process); current_thread = recent_thread; OnTargetEvent (new TargetEventArgs (TargetEventType.TargetStopped) { Process = process, Thread = GetThread (process, recent_thread), Backtrace = GetThreadBacktrace (recent_thread)}); } void EnsureRecentThreadIsValid (ProcessInfo process) { var infos = process.GetThreads (); if (ThreadIsAlive (recent_thread) && HasUserFrame (GetId (recent_thread), infos)) return; var threads = vm.GetThreads (); foreach (var thread in threads) { if (ThreadIsAlive (thread) && HasUserFrame (GetId (thread), infos)) { recent_thread = thread; return; } } recent_thread = threads[0]; } long GetId (ThreadMirror thread) { long id; if (!localThreadIds.TryGetValue (thread.ThreadId, out id)) { id = localThreadIds.Count + 1; localThreadIds [thread.ThreadId] = id; } return id; } static bool ThreadIsAlive (ThreadMirror thread) { if (thread == null) return false; ThreadState state; try { state = thread.ThreadState; } catch (ObjectCollectedException) { return false;//Thread was already collected by garbage collector, hence it's not alive } return state != ThreadState.Stopped && state != ThreadState.Aborted; } //we use the Mono.Debugging classes because they are cached static bool HasUserFrame (long tid, ThreadInfo[] threads) { foreach (var thread in threads) { if (thread.Id != tid) continue; var bt = thread.Backtrace; for (int i = 0; i < bt.FrameCount; i++) { var frame = bt.GetFrame (i); if (frame != null && !frame.IsExternalCode) return true; } return false; } return false; } public bool IsExternalCode (StackFrame frame) { return frame.Method == null || string.IsNullOrEmpty (frame.FileName) || (assemblyFilters != null && !assemblyFilters.Contains (frame.Method.DeclaringType.Assembly)); } public bool IsExternalCode (TypeMirror type) { return assemblyFilters != null && !assemblyFilters.Contains (type.Assembly); } protected override AssemblyLine[] OnDisassembleFile (string file) { List mirrors; if (!source_to_type.TryGetValue (file, out mirrors)) return new AssemblyLine [0]; var lines = new List (); foreach (var type in mirrors) { foreach (var method in type.GetMethods ()) { string srcFile = method.SourceFile != null ? NormalizePath (method.SourceFile) : null; if (srcFile == null || !PathsAreEqual (srcFile, file)) continue; var body = method.GetMethodBody (); int lastLine = -1; int firstPos = lines.Count; string addrSpace = method.FullName; foreach (var ins in body.Instructions) { var loc = method.LocationAtILOffset (ins.Offset); if (loc != null && lastLine == -1) { lastLine = loc.LineNumber; for (int n = firstPos; n < lines.Count; n++) { AssemblyLine old = lines [n]; lines [n] = new AssemblyLine (old.Address, old.AddressSpace, old.Code, loc.LineNumber); } } lines.Add (new AssemblyLine (ins.Offset, addrSpace, Disassemble (ins), loc != null ? loc.LineNumber : lastLine)); } } } lines.Sort (delegate (AssemblyLine a1, AssemblyLine a2) { int res = a1.SourceLine.CompareTo (a2.SourceLine); return res != 0 ? res : a1.Address.CompareTo (a2.Address); }); return lines.ToArray (); } public AssemblyLine[] Disassemble (StackFrame frame) { var body = frame.Method.GetMethodBody (); var instructions = body.Instructions; var lines = new List (); foreach (var instruction in instructions) { var location = frame.Method.LocationAtILOffset (instruction.Offset); int lineNumber = location != null ? location.LineNumber : -1; var code = Disassemble (instruction); lines.Add (new AssemblyLine (instruction.Offset, frame.Method.FullName, code, lineNumber)); } return lines.ToArray (); } public AssemblyLine[] Disassemble (StackFrame frame, int firstLine, int count) { var body = frame.Method.GetMethodBody (); var instructions = body.Instructions; ILInstruction current = null; foreach (var instruction in instructions) { if (instruction.Offset >= frame.ILOffset) { current = instruction; break; } } if (current == null) return new AssemblyLine [0]; var lines = new List (); while (firstLine < 0 && count > 0) { if (current.Previous == null) { lines.Add (AssemblyLine.OutOfRange); firstLine = 0; break; } current = current.Previous; firstLine++; } while (current != null && firstLine > 0) { current = current.Next; firstLine--; } while (count > 0) { if (current != null) { var location = frame.Method.LocationAtILOffset (current.Offset); int lineNumber = location != null ? location.LineNumber : -1; var code = Disassemble (current); lines.Add (new AssemblyLine (current.Offset, frame.Method.FullName, code, lineNumber)); current = current.Next; } else { lines.Add (AssemblyLine.OutOfRange); } count--; } return lines.ToArray (); } public void AddSourceUpdate (string fileName) { sourceUpdates.Add (new SourceUpdate(fileName)); } public void AddLineUpdate (int oldLine, int newLine) { sourceUpdates.Last().LineUpdates.Add (new Tuple(oldLine, newLine)); } public void ApplyChanges (ModuleMirror module, byte[] metadataDelta, byte[] ilDelta, byte[] pdbDelta = null) { var rootDomain = VirtualMachine.RootDomain; var metadataArray = rootDomain.CreateByteArray (metadataDelta); var ilArray = rootDomain.CreateByteArray (ilDelta); Value pdbArray; if (pdbDelta == null) pdbArray = VirtualMachine.CreateValue (null); else pdbArray = rootDomain.CreateByteArray (pdbDelta); module.Assembly.ApplyChanges_DebugInformation (metadataDelta, pdbDelta); module.ApplyChanges (metadataArray, ilArray, pdbArray); foreach (var sourceUpdate in sourceUpdates) { var types = VirtualMachine.GetTypesForSourceFile (sourceUpdate.FileName, false); foreach (var type in types) { type.ApplySourceChanges (sourceUpdate); } } sourceUpdates.Clear (); } public string GetEnCCapabilities() { return vm.GetEnCCapabilities (); } static string EscapeString (string text) { var escaped = new StringBuilder (); escaped.Append ('"'); for (int i = 0; i < text.Length; i++) { char c = text[i]; string txt; switch (c) { case '"': txt = "\\\""; break; case '\0': txt = @"\0"; break; case '\\': txt = @"\\"; break; case '\a': txt = @"\a"; break; case '\b': txt = @"\b"; break; case '\f': txt = @"\f"; break; case '\v': txt = @"\v"; break; case '\n': txt = @"\n"; break; case '\r': txt = @"\r"; break; case '\t': txt = @"\t"; break; default: if (char.GetUnicodeCategory (c) == UnicodeCategory.OtherNotAssigned) { escaped.AppendFormat ("\\u{0:X4}", c); } else { escaped.Append (c); } continue; } escaped.Append (txt); } escaped.Append ('"'); return escaped.ToString (); } static string Disassemble (ILInstruction ins) { string oper; if (ins.Operand is MethodMirror) oper = ((MethodMirror)ins.Operand).FullName; else if (ins.Operand is FieldInfoMirror) oper = ((FieldInfoMirror)ins.Operand).FullName; else if (ins.Operand is TypeMirror) oper = ((TypeMirror)ins.Operand).FullName; else if (ins.Operand is ILInstruction) oper = ((ILInstruction)ins.Operand).Offset.ToString ("x8"); else if (ins.Operand is string) oper = EscapeString ((string) ins.Operand); else if (ins.Operand == null) oper = string.Empty; else oper = ins.Operand.ToString (); return ins.OpCode + " " + oper; } readonly static bool IsWindows; readonly static bool IsMac; readonly static StringComparer PathComparer; static bool IgnoreFilenameCase { get { return IsMac || IsWindows; } } static SoftDebuggerSession () { IsWindows = Path.DirectorySeparatorChar == '\\'; IsMac = !IsWindows && IsRunningOnMac (); PathComparer = IgnoreFilenameCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; ThreadMirror.NativeTransitions = true; } //From Managed.Windows.Forms/XplatUI static bool IsRunningOnMac () { IntPtr buf = IntPtr.Zero; try { buf = Marshal.AllocHGlobal (8192); // This is a hacktastic way of getting sysname from uname () if (uname (buf) == 0) { string os = Marshal.PtrToStringAnsi (buf); if (os == "Darwin") return true; } } catch { return false; } finally { if (buf != IntPtr.Zero) Marshal.FreeHGlobal (buf); } return false; } [System.Runtime.InteropServices.DllImport ("libc")] static extern int uname (IntPtr buf); } class LocationComparer : IComparer { public int Compare (Location loc0, Location loc1) { if (loc0.LineNumber < loc1.LineNumber) return -1; if (loc0.LineNumber > loc1.LineNumber) return 1; if (loc0.ColumnNumber < loc1.ColumnNumber) return -1; if (loc0.ColumnNumber > loc1.ColumnNumber) return 1; return loc0.ILOffset - loc1.ILOffset; } } class BreakInfo: BreakEventInfo { public Location Location; public Dictionary Requests = new Dictionary (); public string LastConditionValue; public string FileName; public string TypeName; } class DisconnectedException: DebuggerException { public DisconnectedException (Exception ex): base ("The connection with the debugger has been lost. The target application may have exited.", ex) { } } class DebugSocketException: DebuggerException { public DebugSocketException (Exception ex): base ("Could not open port for debugger. Another process may be using the port.", ex) { } } class ConnectionException : DebuggerException { public ConnectionException (Exception ex): base ("Could not connect to the debugger.", ex) { } } }