// BreakpointStore.cs // // Author: // Lluis Sanchez Gual // // Copyright (c) 2008 Novell, Inc (http://www.novell.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. // // using System; using System.IO; using System.Linq; using System.Xml; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Runtime.InteropServices; using System.Collections.Concurrent; namespace Mono.Debugging.Client { public sealed class BreakpointStore: ICollection { static readonly StringComparer PathComparer; static readonly bool IsWindows; static readonly bool IsMac; readonly object breakpointLock = new object (); List breakpoints = new List(); static BreakpointStore () { IsWindows = Path.DirectorySeparatorChar == '\\'; IsMac = !IsWindows && IsRunningOnMac (); PathComparer = IsWindows || IsMac ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; } [DllImport ("libc")] static extern int uname (IntPtr buf); //From Managed.Windows.Forms/XplatUI static bool IsRunningOnMac () { var buf = IntPtr.Zero; try { buf = Marshal.AllocHGlobal (8192); // This is a hacktastic way of getting sysname from uname () if (uname (buf) == 0) { var os = Marshal.PtrToStringAnsi (buf); if (os == "Darwin") return true; } } catch { } finally { if (buf != IntPtr.Zero) Marshal.FreeHGlobal (buf); } return false; } public int Count { get { return InternalGetBreakpoints().Count; } } public bool IsReadOnly { get { var args = new ReadOnlyCheckEventArgs (); CheckingReadOnly?.Invoke (this, args); return args.IsReadOnly; } } public Breakpoint Add (string filename, int line, int column) { return Add (filename, line, column, true); } public Breakpoint Add (string filename, int line) { return Add (filename, line, 1, true); } public Breakpoint Add (string filename, int line, int column, bool activate) { if (filename == null) throw new ArgumentNullException (nameof (filename)); if (line < 1) throw new ArgumentOutOfRangeException (nameof (line)); if (column < 1) throw new ArgumentOutOfRangeException (nameof (column)); if (IsReadOnly) return null; var bp = new Breakpoint (filename, line, column); Add (bp); return bp; } void ICollection.Add (BreakEvent bp) { Add (bp); } public bool Add (BreakEvent be) { if (be == null) throw new ArgumentNullException (nameof (be)); if (IsReadOnly) return false; if (be is Breakpoint bp) { bp.SetFileName (ResolveFullPath(bp.FileName)); } lock (breakpointLock) { breakpoints.Add(be); SetBreakpoints(breakpoints); be.Store = this; } OnBreakEventAdded (be); return true; } public Catchpoint AddCatchpoint (string exceptionName) { return AddCatchpoint (exceptionName, true); } public Catchpoint AddCatchpoint (string exceptionName, bool includeSubclasses) { if (exceptionName == null) throw new ArgumentNullException (nameof (exceptionName)); if (IsReadOnly) return null; var cp = new Catchpoint (exceptionName, includeSubclasses); Add (cp); return cp; } public bool Remove (string filename, int line, int column) { if (filename == null) throw new ArgumentNullException (nameof (filename)); if (IsReadOnly) return false; filename = Path.GetFullPath (filename); var breakpointsToRemove = new List (); foreach (var b in InternalGetBreakpoints ()) { if (b is Breakpoint bp && FileNameEquals(bp.FileName, filename) && (bp.OriginalLine == line || bp.Line == line) && (bp.OriginalColumn == column || bp.Column == column)) { breakpointsToRemove.Add (bp); } } RemoveRange (breakpointsToRemove); return true; } public bool RemoveCatchpoint (string exceptionName) { if (exceptionName == null) throw new ArgumentNullException (nameof (exceptionName)); if (IsReadOnly) return false; var breakpointsToRemove = new List (); foreach (var b in InternalGetBreakpoints ()) { if (b is Catchpoint cp && cp.ExceptionName == exceptionName) { breakpointsToRemove.Add (cp); } } RemoveRange (breakpointsToRemove); return true; } public void RemoveRunToCursorBreakpoints () { if (IsReadOnly) return; var breakpointsToRemove = InternalGetBreakpoints ().OfType ().ToList (); RemoveRange (breakpointsToRemove); } public bool Remove (BreakEvent bp) { if (bp == null) throw new ArgumentNullException (nameof (bp)); if (!IsReadOnly) { var wasRemoved = false; lock (breakpointLock) { if (breakpoints.Contains (bp)) { breakpoints.Remove(bp); SetBreakpoints(breakpoints); wasRemoved = true; } } if (wasRemoved) { OnBreakEventRemoved (bp); return true; } } return false; } void RemoveRange (IEnumerable breakEvents) { List oldEvents; if (!IsReadOnly) { lock (breakpointLock) { foreach (var b in breakEvents) breakpoints.Remove(b); oldEvents = SetBreakpoints(breakpoints); } List breakEventsRemoved = new List (); foreach (var bp in breakEvents) { if (oldEvents.Contains(bp)) { breakEventsRemoved.Add (bp); } } OnBreakEventsRemoved (breakEventsRemoved); } } public Breakpoint Toggle (string filename, int line, int column) { if (filename == null) throw new ArgumentNullException (nameof (filename)); if (line < 1) throw new ArgumentOutOfRangeException (nameof (line)); if (column < 1) throw new ArgumentOutOfRangeException (nameof (column)); if (IsReadOnly) return null; var col = GetBreakpointsAtFileLine (filename, line); if (col.Count > 0) { // Remove only the most-recently-added breakpoint on the specified line Remove (col[col.Count - 1]); return null; } return Add (filename, line, column); } public ReadOnlyCollection GetBreakevents () { return new ReadOnlyCollection(InternalGetBreakpoints ()); } public ReadOnlyCollection GetBreakpoints () { var list = new List (); foreach (var bp in InternalGetBreakpoints ().OfType ()) { if (!(bp is RunToCursorBreakpoint)) list.Add (bp); } return list.AsReadOnly (); } public ReadOnlyCollection GetCatchpoints () { return InternalGetBreakpoints ().OfType ().ToList ().AsReadOnly (); } public ReadOnlyCollection GetBreakpointsAtFile (string filename) { if (filename == null) throw new ArgumentNullException (nameof (filename)); var list = new List (); if (string.IsNullOrEmpty (filename)) return list.AsReadOnly (); try { filename = Path.GetFullPath (filename); } catch { return list.AsReadOnly (); } foreach (var bp in InternalGetBreakpoints ().OfType ()) { if (!(bp is RunToCursorBreakpoint) && FileNameEquals(bp.FileName, filename)) list.Add (bp); } return list.AsReadOnly (); } public ReadOnlyCollection GetBreakpointsAtFileLine (string filename, int line) { if (filename == null) throw new ArgumentNullException (nameof (filename)); var list = new List (); try { filename = Path.GetFullPath (filename); } catch { return list.AsReadOnly (); } foreach (var bp in InternalGetBreakpoints ().OfType ()) { if (!(bp is RunToCursorBreakpoint) && FileNameEquals(bp.FileName, filename) && (bp.OriginalLine == line || bp.Line == line)) list.Add (bp); } return list.AsReadOnly (); } public IEnumerator GetEnumerator () { return ((IEnumerable)InternalGetBreakpoints()).GetEnumerator (); } IEnumerator IEnumerable.GetEnumerator () { return ((IEnumerable)InternalGetBreakpoints ()).GetEnumerator (); } public void Clear () { Clear (true); } public void Clear (bool clearNonUserBreakpoints) { var breakpointsToRemove = InternalGetBreakpoints () .Where (bp => clearNonUserBreakpoints || !bp.NonUserBreakpoint); RemoveRange (breakpointsToRemove); } public void ClearBreakpoints () { RemoveRange (GetBreakpoints ()); } public void ClearCatchpoints () { RemoveRange (GetCatchpoints ()); } public bool Contains (BreakEvent item) { return InternalGetBreakpoints ().Contains (item); } public void CopyTo (BreakEvent[] array, int arrayIndex) { InternalGetBreakpoints ().CopyTo (array, arrayIndex); } public void UpdateBreakpointLine (Breakpoint bp, int newLine) { if (IsReadOnly) return; bp.SetLine (newLine); NotifyBreakEventChanged (bp); } internal void AdjustBreakpointLine (Breakpoint bp, int newLine, int newColumn) { if (IsReadOnly) return; bp.SetAdjustedColumn (newColumn); bp.SetAdjustedLine (newLine); NotifyBreakEventChanged (bp); } internal void ResetBreakpoints () { if (IsReadOnly) return; foreach (var bp in InternalGetBreakpoints ()) { if (bp.Reset ()) { NotifyBreakEventChanged (bp); } } } public XmlElement Save (string baseDir = null) { XmlDocument doc = new XmlDocument (); XmlElement elem = doc.CreateElement ("BreakpointStore"); foreach (BreakEvent ev in this) { if (ev.NonUserBreakpoint) continue; XmlElement be = ev.ToXml (doc, baseDir); elem.AppendChild (be); } return elem; } public void Load (XmlElement rootElem, string baseDir = null) { Clear (); var loadedBreakpoints = new List (); foreach (XmlNode n in rootElem.ChildNodes) { XmlElement elem = n as XmlElement; if (elem == null) continue; BreakEvent ev = BreakEvent.FromXml (elem, baseDir); if (ev != null) { loadedBreakpoints.Add (ev); ev.Store = this; } } lock (breakpointLock) { SetBreakpoints (new List (loadedBreakpoints)); } // preserve behaviour by sending an event for each breakpoint that was loaded OnBreakEventsAdded (loadedBreakpoints); } [DllImport("libc")] static extern string realpath(string path, IntPtr buffer); /// /// Resolves the full path of the given file /// /// /// The full path if a file is given, or returns the parameter if it is null or empty static string ResolveFullPath (string path) { // If there is no path given, return the same path back if (string.IsNullOrEmpty(path)) return path; string result = null; try { result = realpath(path, IntPtr.Zero); } catch { } return string.IsNullOrEmpty(result) ? Path.GetFullPath(path) : result; } public static bool FileNameEquals (string file1, string file2) { if (file1 == null) return file2 == null; if (file2 == null) return false; return PathComparer.Compare (file1, file2) == 0; } internal bool EnableBreakEvent (BreakEvent be, bool previouslyEnabled, bool enabled) { if (IsReadOnly) return false; if (previouslyEnabled != enabled) { OnChanged (); BreakEventEnableStatusChanged?.Invoke (this, new BreakEventArgs (be)); NotifyStatusChanged (be); } return true; } void OnBreakEventAdded (BreakEvent be) { BreakEventAdded?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointAdded?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointAdded?.Invoke (this, new CatchpointEventArgs (ce)); } OnChanged (); } void OnBreakEventsAdded (List bes) { foreach (BreakEvent be in bes) { BreakEventAdded?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointAdded?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointAdded?.Invoke (this, new CatchpointEventArgs (ce)); } } OnChanged (); } void OnBreakEventRemoved (BreakEvent be) { BreakEventRemoved?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointRemoved?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointRemoved?.Invoke (this, new CatchpointEventArgs (ce)); } OnChanged (); } void OnBreakEventsRemoved (List bes) { foreach (BreakEvent be in bes) { BreakEventRemoved?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointRemoved?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointRemoved?.Invoke (this, new CatchpointEventArgs (ce)); } } OnChanged (); } void OnChanged () { Changed?.Invoke (this, EventArgs.Empty); } internal void NotifyStatusChanged (BreakEvent be) { try { BreakEventStatusChanged?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointStatusChanged?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointStatusChanged?.Invoke (this, new CatchpointEventArgs (ce)); } } catch { // Ignore } } internal void NotifyBreakEventChanged (BreakEvent be) { try { BreakEventModified?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointModified?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointModified?.Invoke (this, new CatchpointEventArgs (ce)); } OnChanged (); } catch { // Ignore } } internal void NotifyBreakEventUpdated (BreakEvent be) { try { BreakEventUpdated?.Invoke (this, new BreakEventArgs (be)); if (be is Breakpoint bp) { BreakpointUpdated?.Invoke (this, new BreakpointEventArgs (bp)); } else if (be is Catchpoint ce) { CatchpointUpdated?.Invoke (this, new CatchpointEventArgs (ce)); } } catch { // Ignore } } public event EventHandler BreakpointAdded; public event EventHandler BreakpointRemoved; public event EventHandler BreakpointStatusChanged; public event EventHandler BreakpointModified; public event EventHandler BreakpointUpdated; public event EventHandler CatchpointAdded; public event EventHandler CatchpointRemoved; public event EventHandler CatchpointStatusChanged; public event EventHandler CatchpointModified; public event EventHandler CatchpointUpdated; public event EventHandler BreakEventAdded; public event EventHandler BreakEventRemoved; public event EventHandler BreakEventStatusChanged; public event EventHandler BreakEventModified; public event EventHandler BreakEventUpdated; public event EventHandler Changed; public event EventHandler CheckingReadOnly; internal event EventHandler BreakEventEnableStatusChanged; public void FileRenamed (string oldPath, string newPath) { if (IsReadOnly) return; foreach (var bp in GetBreakpointsAtFile (oldPath)) { Remove (bp); bp.SetFileName (newPath); Add (bp); } } List SetBreakpoints (List newBreakpoints) { System.Diagnostics.Debug.Assert (System.Threading.Monitor.IsEntered (breakpointLock), "SetBreakpoints must be called during a lock"); foreach (BreakEvent be in newBreakpoints) { if (be is Breakpoint bp) { bp.SetFileName (ResolveFullPath(bp.FileName)); } } var oldEvents = breakpoints; breakpoints = newBreakpoints; return oldEvents; } List InternalGetBreakpoints () { lock (breakpointLock) { return breakpoints; } } } public class ReadOnlyCheckEventArgs: EventArgs { internal bool IsReadOnly; public void SetReadOnly (bool isReadOnly) { IsReadOnly = IsReadOnly || isReadOnly; } } }