Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mono/mono.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'sdks/wasm/BrowserDebugProxy/DebugStore.cs')
-rw-r--r--sdks/wasm/BrowserDebugProxy/DebugStore.cs1693
1 files changed, 874 insertions, 819 deletions
diff --git a/sdks/wasm/BrowserDebugProxy/DebugStore.cs b/sdks/wasm/BrowserDebugProxy/DebugStore.cs
index f633fe786d5..9f9c7a6bd08 100644
--- a/sdks/wasm/BrowserDebugProxy/DebugStore.cs
+++ b/sdks/wasm/BrowserDebugProxy/DebugStore.cs
@@ -1,828 +1,883 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
using System;
-using System.IO;
using System.Collections.Generic;
-using Mono.Cecil;
-using Mono.Cecil.Cil;
+using System.IO;
using System.Linq;
-using Newtonsoft.Json.Linq;
using System.Net.Http;
-using Mono.Cecil.Pdb;
-using Newtonsoft.Json;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
using System.Text.RegularExpressions;
-using System.Threading.Tasks;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using System.Runtime.CompilerServices;
-using System.Security.Cryptography;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Pdb;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
-namespace WebAssembly.Net.Debugging {
- internal class BreakpointRequest {
- public string Id { get; private set; }
- public string Assembly { get; private set; }
- public string File { get; private set; }
- public int Line { get; private set; }
- public int Column { get; private set; }
- public MethodInfo Method { get; private set; }
-
- JObject request;
-
- public bool IsResolved => Assembly != null;
- public List<Breakpoint> Locations { get; } = new List<Breakpoint> ();
-
- public override string ToString ()
- => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
-
- public object AsSetBreakpointByUrlResponse (IEnumerable<object> jsloc)
- => new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation ()).Concat (jsloc) };
-
- public BreakpointRequest () {
- }
-
- public BreakpointRequest (string id, MethodInfo method) {
- Id = id;
- Method = method;
- }
-
- public BreakpointRequest (string id, JObject request) {
- Id = id;
- this.request = request;
- }
-
- public static BreakpointRequest Parse (string id, JObject args)
- {
- return new BreakpointRequest (id, args);
- }
-
- public BreakpointRequest Clone ()
- => new BreakpointRequest { Id = Id, request = request };
-
- public bool IsMatch (SourceFile sourceFile)
- {
- var url = request? ["url"]?.Value<string> ();
- if (url == null) {
- var urlRegex = request?["urlRegex"].Value<string>();
- var regex = new Regex (urlRegex);
- return regex.IsMatch (sourceFile.Url.ToString ()) || regex.IsMatch (sourceFile.DocUrl);
- }
-
- return sourceFile.Url.ToString () == url || sourceFile.DotNetUrl == url;
- }
-
- public bool TryResolve (SourceFile sourceFile)
- {
- if (!IsMatch (sourceFile))
- return false;
-
- var line = request? ["lineNumber"]?.Value<int> ();
- var column = request? ["columnNumber"]?.Value<int> ();
-
- if (line == null || column == null)
- return false;
-
- Assembly = sourceFile.AssemblyName;
- File = sourceFile.DebuggerFileName;
- Line = line.Value;
- Column = column.Value;
- return true;
- }
-
- public bool TryResolve (DebugStore store)
- {
- if (request == null || store == null)
- return false;
-
- return store.AllSources().FirstOrDefault (source => TryResolve (source)) != null;
- }
- }
-
- internal class VarInfo {
- public VarInfo (VariableDebugInformation v)
- {
- this.Name = v.Name;
- this.Index = v.Index;
- }
-
- public VarInfo (ParameterDefinition p)
- {
- this.Name = p.Name;
- this.Index = (p.Index + 1) * -1;
- }
-
- public string Name { get; }
- public int Index { get; }
-
- public override string ToString ()
- => $"(var-info [{Index}] '{Name}')";
- }
-
- internal class CliLocation {
- public CliLocation (MethodInfo method, int offset)
- {
- Method = method;
- Offset = offset;
- }
-
- public MethodInfo Method { get; }
- public int Offset { get; }
- }
-
- internal class SourceLocation {
- SourceId id;
- int line;
- int column;
- CliLocation cliLoc;
-
- public SourceLocation (SourceId id, int line, int column)
- {
- this.id = id;
- this.line = line;
- this.column = column;
- }
-
- public SourceLocation (MethodInfo mi, SequencePoint sp)
- {
- this.id = mi.SourceId;
- this.line = sp.StartLine - 1;
- this.column = sp.StartColumn - 1;
- this.cliLoc = new CliLocation (mi, sp.Offset);
- }
-
- public SourceId Id { get => id; }
- public int Line { get => line; }
- public int Column { get => column; }
- public CliLocation CliLocation => this.cliLoc;
-
- public override string ToString ()
- => $"{id}:{Line}:{Column}";
-
- public static SourceLocation Parse (JObject obj)
- {
- if (obj == null)
- return null;
-
- if (!SourceId.TryParse (obj ["scriptId"]?.Value<string> (), out var id))
- return null;
-
- var line = obj ["lineNumber"]?.Value<int> ();
- var column = obj ["columnNumber"]?.Value<int> ();
- if (id == null || line == null || column == null)
- return null;
-
- return new SourceLocation (id, line.Value, column.Value);
- }
-
-
- internal class LocationComparer : EqualityComparer<SourceLocation>
- {
- public override bool Equals (SourceLocation l1, SourceLocation l2)
- {
- if (l1 == null && l2 == null)
- return true;
- else if (l1 == null || l2 == null)
- return false;
-
- return (l1.Line == l2.Line &&
- l1.Column == l2.Column &&
- l1.Id == l2.Id);
- }
-
- public override int GetHashCode (SourceLocation loc)
- {
- int hCode = loc.Line ^ loc.Column;
- return loc.Id.GetHashCode () ^ hCode.GetHashCode ();
- }
- }
-
- internal object AsLocation ()
- => new {
- scriptId = id.ToString (),
- lineNumber = line,
- columnNumber = column
- };
- }
-
- internal class SourceId {
- const string Scheme = "dotnet://";
-
- readonly int assembly, document;
-
- public int Assembly => assembly;
- public int Document => document;
-
- internal SourceId (int assembly, int document)
- {
- this.assembly = assembly;
- this.document = document;
- }
-
- public SourceId (string id)
- {
- if (!TryParse (id, out assembly, out document))
- throw new ArgumentException ("invalid source identifier", nameof (id));
- }
-
- public static bool TryParse (string id, out SourceId source)
- {
- source = null;
- if (!TryParse (id, out var assembly, out var document))
- return false;
-
- source = new SourceId (assembly, document);
- return true;
- }
-
- static bool TryParse (string id, out int assembly, out int document)
- {
- assembly = document = 0;
- if (id == null || !id.StartsWith (Scheme, StringComparison.Ordinal))
- return false;
-
- var sp = id.Substring (Scheme.Length).Split ('_');
- if (sp.Length != 2)
- return false;
-
- if (!int.TryParse (sp [0], out assembly))
- return false;
-
- if (!int.TryParse (sp [1], out document))
- return false;
-
- return true;
- }
-
- public override string ToString ()
- => $"{Scheme}{assembly}_{document}";
-
- public override bool Equals (object obj)
- {
- if (obj == null)
- return false;
- SourceId that = obj as SourceId;
- return that.assembly == this.assembly && that.document == this.document;
- }
-
- public override int GetHashCode ()
- => assembly.GetHashCode () ^ document.GetHashCode ();
-
- public static bool operator == (SourceId a, SourceId b)
- => ((object)a == null) ? (object)b == null : a.Equals (b);
-
- public static bool operator != (SourceId a, SourceId b)
- => !a.Equals (b);
- }
-
- internal class MethodInfo {
- MethodDefinition methodDef;
- SourceFile source;
-
- public SourceId SourceId => source.SourceId;
-
- public string Name => methodDef.Name;
- public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
-
- public SourceLocation StartLocation { get; }
- public SourceLocation EndLocation { get; }
- public AssemblyInfo Assembly { get; }
- public uint Token => methodDef.MetadataToken.RID;
-
- public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
- {
- this.Assembly = assembly;
- this.methodDef = methodDef;
- this.source = source;
-
- var sps = DebugInformation.SequencePoints;
- if (sps == null || sps.Count() < 1)
- return;
-
- SequencePoint start = sps [0];
- SequencePoint end = sps [0];
-
- foreach (var sp in sps) {
- if (sp.StartLine < start.StartLine)
- start = sp;
- else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
- start = sp;
-
- if (sp.EndLine > end.EndLine)
- end = sp;
- else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
- end = sp;
- }
-
- StartLocation = new SourceLocation (this, start);
- EndLocation = new SourceLocation (this, end);
- }
-
- public SourceLocation GetLocationByIl (int pos)
- {
- SequencePoint prev = null;
- foreach (var sp in DebugInformation.SequencePoints) {
- if (sp.Offset > pos)
- break;
- prev = sp;
- }
-
- if (prev != null)
- return new SourceLocation (this, prev);
-
- return null;
- }
-
- public VarInfo [] GetLiveVarsAt (int offset)
- {
- var res = new List<VarInfo> ();
-
- res.AddRange (methodDef.Parameters.Select (p => new VarInfo (p)));
- res.AddRange (methodDef.DebugInformation.GetScopes ()
- .Where (s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
- .SelectMany (s => s.Variables)
- .Where (v => !v.IsDebuggerHidden)
- .Select (v => new VarInfo (v)));
-
- return res.ToArray ();
- }
-
- public override string ToString () => "MethodInfo(" + methodDef.FullName + ")";
- }
-
- internal class TypeInfo {
- AssemblyInfo assembly;
- TypeDefinition type;
- List<MethodInfo> methods;
-
- public TypeInfo (AssemblyInfo assembly, TypeDefinition type) {
- this.assembly = assembly;
- this.type = type;
- methods = new List<MethodInfo> ();
- }
-
- public string Name => type.Name;
- public string FullName => type.FullName;
- public List<MethodInfo> Methods => methods;
-
- public override string ToString () => "TypeInfo('" + FullName + "')";
- }
-
- class AssemblyInfo {
- static int next_id;
- ModuleDefinition image;
- readonly int id;
- readonly ILogger logger;
- Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo> ();
- Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
- Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo> ();
- readonly List<SourceFile> sources = new List<SourceFile>();
- internal string Url { get; }
-
- public AssemblyInfo (IAssemblyResolver resolver, string url, byte[] assembly, byte[] pdb)
- {
- this.id = Interlocked.Increment (ref next_id);
-
- try {
- Url = url;
- ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
- rp.AssemblyResolver = resolver;
- // set ReadSymbols = true unconditionally in case there
- // is an embedded pdb then handle ArgumentException
- // and assume that if pdb == null that is the cause
- rp.ReadSymbols = true;
- rp.SymbolReaderProvider = new PdbReaderProvider ();
- if (pdb != null)
- rp.SymbolStream = new MemoryStream (pdb);
- rp.ReadingMode = ReadingMode.Immediate;
-
- this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
- } catch (BadImageFormatException ex) {
- logger.LogWarning ($"Failed to read assembly as portable PDB: {ex.Message}");
- } catch (ArgumentException) {
- // if pdb == null this is expected and we
- // read the assembly without symbols below
- if (pdb != null)
- throw;
- }
-
- if (this.image == null) {
- ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
- rp.AssemblyResolver = resolver;
- if (pdb != null) {
- rp.ReadSymbols = true;
- rp.SymbolReaderProvider = new PdbReaderProvider ();
- rp.SymbolStream = new MemoryStream (pdb);
- }
-
- rp.ReadingMode = ReadingMode.Immediate;
-
- this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
- }
-
- Populate ();
- }
-
- public AssemblyInfo (ILogger logger)
- {
- this.logger = logger;
- }
-
- void Populate ()
- {
- ProcessSourceLink();
-
- var d2s = new Dictionary<Document, SourceFile> ();
-
- SourceFile FindSource (Document doc)
- {
- if (doc == null)
- return null;
-
- if (d2s.TryGetValue (doc, out var source))
- return source;
-
- var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url));
- sources.Add (src);
- d2s [doc] = src;
- return src;
- };
-
- foreach (var type in image.GetTypes()) {
- var typeInfo = new TypeInfo (this, type);
- typesByName [type.FullName] = typeInfo;
-
- foreach (var method in type.Methods) {
- foreach (var sp in method.DebugInformation.SequencePoints) {
- var source = FindSource (sp.Document);
- var methodInfo = new MethodInfo (this, method, source);
- methods [method.MetadataToken.RID] = methodInfo;
- if (source != null)
- source.AddMethod (methodInfo);
-
- typeInfo.Methods.Add (methodInfo);
- }
- }
- }
- }
-
- private void ProcessSourceLink ()
- {
- var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault (i => i.Kind == CustomDebugInformationKind.SourceLink);
-
- if (sourceLinkDebugInfo != null) {
- var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
-
- if (sourceLinkContent != null) {
- var jObject = JObject.Parse (sourceLinkContent) ["documents"];
- sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>> (jObject.ToString ());
- }
- }
- }
-
- private Uri GetSourceLinkUrl (string document)
- {
- if (sourceLinkMappings.TryGetValue (document, out string url))
- return new Uri (url);
-
- foreach (var sourceLinkDocument in sourceLinkMappings) {
- string key = sourceLinkDocument.Key;
-
- if (Path.GetFileName (key) != "*") {
- continue;
- }
-
- var keyTrim = key.TrimEnd ('*');
-
- if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) {
- var docUrlPart = document.Replace (keyTrim, "");
- return new Uri (sourceLinkDocument.Value.TrimEnd ('*') + docUrlPart);
- }
- }
-
- return null;
- }
-
- public IEnumerable<SourceFile> Sources
- => this.sources;
-
- public Dictionary<string, TypeInfo> TypesByName => this.typesByName;
- public int Id => id;
- public string Name => image.Name;
-
- public SourceFile GetDocById (int document)
- {
- return sources.FirstOrDefault (s => s.SourceId.Document == document);
- }
-
- public MethodInfo GetMethodByToken (uint token)
- {
- methods.TryGetValue (token, out var value);
- return value;
- }
-
- public TypeInfo GetTypeByName (string name) {
- typesByName.TryGetValue (name, out var res);
- return res;
- }
- }
-
- internal class SourceFile {
- Dictionary<uint, MethodInfo> methods;
- AssemblyInfo assembly;
- int id;
- Document doc;
-
- internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
- {
- this.methods = new Dictionary<uint, MethodInfo> ();
- this.SourceLinkUri = sourceLinkUri;
- this.assembly = assembly;
- this.id = id;
- this.doc = doc;
- this.DebuggerFileName = doc.Url.Replace ("\\", "/").Replace (":", "");
-
- this.SourceUri = new Uri ((Path.IsPathRooted (doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
- if (SourceUri.IsFile && File.Exists (SourceUri.LocalPath)) {
- this.Url = this.SourceUri.ToString ();
- } else {
- this.Url = DotNetUrl;
- }
- }
-
- internal void AddMethod (MethodInfo mi)
- {
- if (!this.methods.ContainsKey (mi.Token))
- this.methods [mi.Token] = mi;
- }
-
- public string DebuggerFileName { get; }
- public string Url { get; }
- public string AssemblyName => assembly.Name;
- public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
-
- public SourceId SourceId => new SourceId (assembly.Id, this.id);
- public Uri SourceLinkUri { get; }
- public Uri SourceUri { get; }
-
- public IEnumerable<MethodInfo> Methods => this.methods.Values;
-
- public string DocUrl => doc.Url;
-
- public (int startLine, int startColumn, int endLine, int endColumn) GetExtents ()
- {
- var start = Methods.OrderBy (m => m.StartLocation.Line).ThenBy (m => m.StartLocation.Column).First ();
- var end = Methods.OrderByDescending (m => m.EndLocation.Line).ThenByDescending (m => m.EndLocation.Column).First ();
- return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
- }
-
- async Task<MemoryStream> GetDataAsync (Uri uri, CancellationToken token)
- {
- var mem = new MemoryStream ();
- try {
- if (uri.IsFile && File.Exists (uri.LocalPath)) {
- using (var file = File.Open (SourceUri.LocalPath, FileMode.Open)) {
- await file.CopyToAsync (mem, token).ConfigureAwait (false);
- mem.Position = 0;
- }
- } else if (uri.Scheme == "http" || uri.Scheme == "https") {
- var client = new HttpClient ();
- using (var stream = await client.GetStreamAsync (uri)) {
- await stream.CopyToAsync (mem, token).ConfigureAwait (false);
- mem.Position = 0;
- }
- }
- } catch (Exception) {
- return null;
- }
- return mem;
- }
-
- static HashAlgorithm GetHashAlgorithm (DocumentHashAlgorithm algorithm)
- {
- switch (algorithm) {
- case DocumentHashAlgorithm.SHA1: return SHA1.Create ();
- case DocumentHashAlgorithm.SHA256: return SHA256.Create ();
- case DocumentHashAlgorithm.MD5: return MD5.Create ();
- }
- return null;
- }
-
- bool CheckPdbHash (byte [] computedHash)
- {
- if (computedHash.Length != doc.Hash.Length)
- return false;
-
- for (var i = 0; i < computedHash.Length; i++)
- if (computedHash[i] != doc.Hash[i])
- return false;
-
- return true;
- }
-
- byte[] ComputePdbHash (Stream sourceStream)
- {
- var algorithm = GetHashAlgorithm (doc.HashAlgorithm);
- if (algorithm != null)
- using (algorithm)
- return algorithm.ComputeHash (sourceStream);
-
- return Array.Empty<byte> ();
- }
-
- public async Task<Stream> GetSourceAsync (bool checkHash, CancellationToken token = default(CancellationToken))
- {
- if (doc.EmbeddedSource.Length > 0)
- return new MemoryStream (doc.EmbeddedSource, false);
-
- foreach (var url in new [] { SourceUri, SourceLinkUri }) {
- var mem = await GetDataAsync (url, token).ConfigureAwait (false);
- if (mem != null && (!checkHash || CheckPdbHash (ComputePdbHash (mem)))) {
- mem.Position = 0;
- return mem;
- }
- }
-
- return MemoryStream.Null;
- }
-
- public object ToScriptSource (int executionContextId, object executionContextAuxData)
- {
- return new {
- scriptId = SourceId.ToString (),
- url = Url,
- executionContextId,
- executionContextAuxData,
- //hash: should be the v8 hash algo, managed implementation is pending
- dotNetUrl = DotNetUrl,
- };
- }
- }
-
- internal class DebugStore {
- List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
- readonly HttpClient client;
- readonly ILogger logger;
-
- public DebugStore (ILogger logger, HttpClient client) {
- this.client = client;
- this.logger = logger;
- }
-
- public DebugStore (ILogger logger) : this (logger, new HttpClient ())
- {
- }
-
- class DebugItem {
- public string Url { get; set; }
- public Task<byte[][]> Data { get; set; }
- }
-
- public async IAsyncEnumerable<SourceFile> Load (SessionId sessionId, string [] loaded_files, [EnumeratorCancellation] CancellationToken token)
- {
- static bool MatchPdb (string asm, string pdb)
- => Path.ChangeExtension (asm, "pdb") == pdb;
-
- var asm_files = new List<string> ();
- var pdb_files = new List<string> ();
- foreach (var file_name in loaded_files) {
- if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase))
- pdb_files.Add (file_name);
- else
- asm_files.Add (file_name);
- }
-
- List<DebugItem> steps = new List<DebugItem> ();
- foreach (var url in asm_files) {
- try {
- var pdb = pdb_files.FirstOrDefault (n => MatchPdb (url, n));
- steps.Add (
- new DebugItem {
- Url = url,
- Data = Task.WhenAll (client.GetByteArrayAsync (url), pdb != null ? client.GetByteArrayAsync (pdb) : Task.FromResult<byte []> (null))
- });
- } catch (Exception e) {
- logger.LogDebug ($"Failed to read {url} ({e.Message})");
- }
- }
-
- var resolver = new DefaultAssemblyResolver ();
- foreach (var step in steps) {
- AssemblyInfo assembly = null;
- try {
- var bytes = await step.Data.ConfigureAwait (false);
- assembly = new AssemblyInfo (resolver, step.Url, bytes [0], bytes [1]);
- } catch (Exception e) {
- logger.LogDebug ($"Failed to load {step.Url} ({e.Message})");
- }
- if (assembly == null)
- continue;
-
- assemblies.Add (assembly);
- foreach (var source in assembly.Sources)
- yield return source;
- }
- }
-
- public IEnumerable<SourceFile> AllSources ()
- => assemblies.SelectMany (a => a.Sources);
-
- public SourceFile GetFileById (SourceId id)
- => AllSources ().SingleOrDefault (f => f.SourceId.Equals (id));
-
- public AssemblyInfo GetAssemblyByName (string name)
- => assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
-
- /*
- V8 uses zero based indexing for both line and column.
- PPDBs uses one based indexing for both line and column.
- */
- static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
- {
- var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
- var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
-
- if (start.Line > spEnd.Line)
- return false;
-
- if (start.Column > spEnd.Column && start.Line == spEnd.Line)
- return false;
-
- if (end.Line < spStart.Line)
- return false;
-
- if (end.Column < spStart.Column && end.Line == spStart.Line)
- return false;
-
- return true;
- }
-
- public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end)
- {
- //XXX FIXME no idea what todo with locations on different files
- if (start.Id != end.Id) {
- logger.LogDebug ($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
- return null;
- }
-
- var sourceId = start.Id;
-
- var doc = GetFileById (sourceId);
-
- var res = new List<SourceLocation> ();
- if (doc == null) {
- logger.LogDebug ($"Could not find document {sourceId}");
- return res;
- }
-
- foreach (var method in doc.Methods) {
- foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
- if (!sequencePoint.IsHidden && Match (sequencePoint, start, end))
- res.Add (new SourceLocation (method, sequencePoint));
- }
- }
- return res;
- }
-
- /*
- V8 uses zero based indexing for both line and column.
- PPDBs uses one based indexing for both line and column.
- */
- static bool Match (SequencePoint sp, int line, int column)
- {
- var bp = (line: line + 1, column: column + 1);
-
- if (sp.StartLine > bp.line || sp.EndLine < bp.line)
- return false;
-
- //Chrome sends a zero column even if getPossibleBreakpoints say something else
- if (column == 0)
- return true;
-
- if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
- return false;
-
- if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
- return false;
-
- return true;
- }
-
- public IEnumerable<SourceLocation> FindBreakpointLocations (BreakpointRequest request)
- {
- request.TryResolve (this);
-
- var asm = assemblies.FirstOrDefault (a => a.Name.Equals (request.Assembly, StringComparison.OrdinalIgnoreCase));
- var sourceFile = asm?.Sources?.SingleOrDefault (s => s.DebuggerFileName.Equals (request.File, StringComparison.OrdinalIgnoreCase));
-
- if (sourceFile == null)
- yield break;
-
- foreach (var method in sourceFile.Methods) {
- foreach (var sequencePoint in method.DebugInformation.SequencePoints) {
- if (!sequencePoint.IsHidden && Match (sequencePoint, request.Line, request.Column))
- yield return new SourceLocation (method, sequencePoint);
- }
- }
- }
-
- public string ToUrl (SourceLocation location)
- => location != null ? GetFileById (location.Id).Url : "";
- }
+namespace Microsoft.WebAssembly.Diagnostics
+{
+ internal class BreakpointRequest
+ {
+ public string Id { get; private set; }
+ public string Assembly { get; private set; }
+ public string File { get; private set; }
+ public int Line { get; private set; }
+ public int Column { get; private set; }
+ public MethodInfo Method { get; private set; }
+
+ JObject request;
+
+ public bool IsResolved => Assembly != null;
+ public List<Breakpoint> Locations { get; } = new List<Breakpoint>();
+
+ public override string ToString() => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
+
+ public object AsSetBreakpointByUrlResponse(IEnumerable<object> jsloc) => new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation()).Concat(jsloc) };
+
+ public BreakpointRequest()
+ { }
+
+ public BreakpointRequest(string id, MethodInfo method)
+ {
+ Id = id;
+ Method = method;
+ }
+
+ public BreakpointRequest(string id, JObject request)
+ {
+ Id = id;
+ this.request = request;
+ }
+
+ public static BreakpointRequest Parse(string id, JObject args)
+ {
+ return new BreakpointRequest(id, args);
+ }
+
+ public BreakpointRequest Clone() => new BreakpointRequest { Id = Id, request = request };
+
+ public bool IsMatch(SourceFile sourceFile)
+ {
+ var url = request?["url"]?.Value<string>();
+ if (url == null)
+ {
+ var urlRegex = request?["urlRegex"].Value<string>();
+ var regex = new Regex(urlRegex);
+ return regex.IsMatch(sourceFile.Url.ToString()) || regex.IsMatch(sourceFile.DocUrl);
+ }
+
+ return sourceFile.Url.ToString() == url || sourceFile.DotNetUrl == url;
+ }
+
+ public bool TryResolve(SourceFile sourceFile)
+ {
+ if (!IsMatch(sourceFile))
+ return false;
+
+ var line = request?["lineNumber"]?.Value<int>();
+ var column = request?["columnNumber"]?.Value<int>();
+
+ if (line == null || column == null)
+ return false;
+
+ Assembly = sourceFile.AssemblyName;
+ File = sourceFile.DebuggerFileName;
+ Line = line.Value;
+ Column = column.Value;
+ return true;
+ }
+
+ public bool TryResolve(DebugStore store)
+ {
+ if (request == null || store == null)
+ return false;
+
+ return store.AllSources().FirstOrDefault(source => TryResolve(source)) != null;
+ }
+ }
+
+ internal class VarInfo
+ {
+ public VarInfo(VariableDebugInformation v)
+ {
+ this.Name = v.Name;
+ this.Index = v.Index;
+ }
+
+ public VarInfo(ParameterDefinition p)
+ {
+ this.Name = p.Name;
+ this.Index = (p.Index + 1) * -1;
+ }
+
+ public string Name { get; }
+ public int Index { get; }
+
+ public override string ToString() => $"(var-info [{Index}] '{Name}')";
+ }
+
+ internal class CliLocation
+ {
+ public CliLocation(MethodInfo method, int offset)
+ {
+ Method = method;
+ Offset = offset;
+ }
+
+ public MethodInfo Method { get; }
+ public int Offset { get; }
+ }
+
+ internal class SourceLocation
+ {
+ SourceId id;
+ int line;
+ int column;
+ CliLocation cliLoc;
+
+ public SourceLocation(SourceId id, int line, int column)
+ {
+ this.id = id;
+ this.line = line;
+ this.column = column;
+ }
+
+ public SourceLocation(MethodInfo mi, SequencePoint sp)
+ {
+ this.id = mi.SourceId;
+ this.line = sp.StartLine - 1;
+ this.column = sp.StartColumn - 1;
+ this.cliLoc = new CliLocation(mi, sp.Offset);
+ }
+
+ public SourceId Id { get => id; }
+ public int Line { get => line; }
+ public int Column { get => column; }
+ public CliLocation CliLocation => this.cliLoc;
+
+ public override string ToString() => $"{id}:{Line}:{Column}";
+
+ public static SourceLocation Parse(JObject obj)
+ {
+ if (obj == null)
+ return null;
+
+ if (!SourceId.TryParse(obj["scriptId"]?.Value<string>(), out var id))
+ return null;
+
+ var line = obj["lineNumber"]?.Value<int>();
+ var column = obj["columnNumber"]?.Value<int>();
+ if (id == null || line == null || column == null)
+ return null;
+
+ return new SourceLocation(id, line.Value, column.Value);
+ }
+
+ internal class LocationComparer : EqualityComparer<SourceLocation>
+ {
+ public override bool Equals(SourceLocation l1, SourceLocation l2)
+ {
+ if (l1 == null && l2 == null)
+ return true;
+ else if (l1 == null || l2 == null)
+ return false;
+
+ return (l1.Line == l2.Line &&
+ l1.Column == l2.Column &&
+ l1.Id == l2.Id);
+ }
+
+ public override int GetHashCode(SourceLocation loc)
+ {
+ int hCode = loc.Line ^ loc.Column;
+ return loc.Id.GetHashCode() ^ hCode.GetHashCode();
+ }
+ }
+
+ internal object AsLocation() => new
+ {
+ scriptId = id.ToString(),
+ lineNumber = line,
+ columnNumber = column
+ };
+ }
+
+ internal class SourceId
+ {
+ const string Scheme = "dotnet://";
+
+ readonly int assembly, document;
+
+ public int Assembly => assembly;
+ public int Document => document;
+
+ internal SourceId(int assembly, int document)
+ {
+ this.assembly = assembly;
+ this.document = document;
+ }
+
+ public SourceId(string id)
+ {
+ if (!TryParse(id, out assembly, out document))
+ throw new ArgumentException("invalid source identifier", nameof(id));
+ }
+
+ public static bool TryParse(string id, out SourceId source)
+ {
+ source = null;
+ if (!TryParse(id, out var assembly, out var document))
+ return false;
+
+ source = new SourceId(assembly, document);
+ return true;
+ }
+
+ static bool TryParse(string id, out int assembly, out int document)
+ {
+ assembly = document = 0;
+ if (id == null || !id.StartsWith(Scheme, StringComparison.Ordinal))
+ return false;
+
+ var sp = id.Substring(Scheme.Length).Split('_');
+ if (sp.Length != 2)
+ return false;
+
+ if (!int.TryParse(sp[0], out assembly))
+ return false;
+
+ if (!int.TryParse(sp[1], out document))
+ return false;
+
+ return true;
+ }
+
+ public override string ToString() => $"{Scheme}{assembly}_{document}";
+
+ public override bool Equals(object obj)
+ {
+ if (obj == null)
+ return false;
+ SourceId that = obj as SourceId;
+ return that.assembly == this.assembly && that.document == this.document;
+ }
+
+ public override int GetHashCode() => assembly.GetHashCode() ^ document.GetHashCode();
+
+ public static bool operator ==(SourceId a, SourceId b) => ((object)a == null) ? (object)b == null : a.Equals(b);
+
+ public static bool operator !=(SourceId a, SourceId b) => !a.Equals(b);
+ }
+
+ internal class MethodInfo
+ {
+ MethodDefinition methodDef;
+ SourceFile source;
+
+ public SourceId SourceId => source.SourceId;
+
+ public string Name => methodDef.Name;
+ public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
+
+ public SourceLocation StartLocation { get; }
+ public SourceLocation EndLocation { get; }
+ public AssemblyInfo Assembly { get; }
+ public uint Token => methodDef.MetadataToken.RID;
+
+ public MethodInfo(AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
+ {
+ this.Assembly = assembly;
+ this.methodDef = methodDef;
+ this.source = source;
+
+ var sps = DebugInformation.SequencePoints;
+ if (sps == null || sps.Count() < 1)
+ return;
+
+ SequencePoint start = sps[0];
+ SequencePoint end = sps[0];
+
+ foreach (var sp in sps)
+ {
+ if (sp.StartLine < start.StartLine)
+ start = sp;
+ else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
+ start = sp;
+
+ if (sp.EndLine > end.EndLine)
+ end = sp;
+ else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
+ end = sp;
+ }
+
+ StartLocation = new SourceLocation(this, start);
+ EndLocation = new SourceLocation(this, end);
+ }
+
+ public SourceLocation GetLocationByIl(int pos)
+ {
+ SequencePoint prev = null;
+ foreach (var sp in DebugInformation.SequencePoints)
+ {
+ if (sp.Offset > pos)
+ break;
+ prev = sp;
+ }
+
+ if (prev != null)
+ return new SourceLocation(this, prev);
+
+ return null;
+ }
+
+ public VarInfo[] GetLiveVarsAt(int offset)
+ {
+ var res = new List<VarInfo>();
+
+ res.AddRange(methodDef.Parameters.Select(p => new VarInfo(p)));
+ res.AddRange(methodDef.DebugInformation.GetScopes()
+ .Where(s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
+ .SelectMany(s => s.Variables)
+ .Where(v => !v.IsDebuggerHidden)
+ .Select(v => new VarInfo(v)));
+
+ return res.ToArray();
+ }
+
+ public override string ToString() => "MethodInfo(" + methodDef.FullName + ")";
+ }
+
+ internal class TypeInfo
+ {
+ AssemblyInfo assembly;
+ TypeDefinition type;
+ List<MethodInfo> methods;
+
+ public TypeInfo(AssemblyInfo assembly, TypeDefinition type)
+ {
+ this.assembly = assembly;
+ this.type = type;
+ methods = new List<MethodInfo>();
+ }
+
+ public string Name => type.Name;
+ public string FullName => type.FullName;
+ public List<MethodInfo> Methods => methods;
+
+ public override string ToString() => "TypeInfo('" + FullName + "')";
+ }
+
+ class AssemblyInfo
+ {
+ static int next_id;
+ ModuleDefinition image;
+ readonly int id;
+ readonly ILogger logger;
+ Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo>();
+ Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
+ Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo>();
+ readonly List<SourceFile> sources = new List<SourceFile>();
+ internal string Url { get; }
+
+ public AssemblyInfo(IAssemblyResolver resolver, string url, byte[] assembly, byte[] pdb)
+ {
+ this.id = Interlocked.Increment(ref next_id);
+
+ try
+ {
+ Url = url;
+ ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+ rp.AssemblyResolver = resolver;
+ // set ReadSymbols = true unconditionally in case there
+ // is an embedded pdb then handle ArgumentException
+ // and assume that if pdb == null that is the cause
+ rp.ReadSymbols = true;
+ rp.SymbolReaderProvider = new PdbReaderProvider();
+ if (pdb != null)
+ rp.SymbolStream = new MemoryStream(pdb);
+ rp.ReadingMode = ReadingMode.Immediate;
+
+ this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+ }
+ catch (BadImageFormatException ex)
+ {
+ logger.LogWarning($"Failed to read assembly as portable PDB: {ex.Message}");
+ }
+ catch (ArgumentException)
+ {
+ // if pdb == null this is expected and we
+ // read the assembly without symbols below
+ if (pdb != null)
+ throw;
+ }
+
+ if (this.image == null)
+ {
+ ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+ rp.AssemblyResolver = resolver;
+ if (pdb != null)
+ {
+ rp.ReadSymbols = true;
+ rp.SymbolReaderProvider = new PdbReaderProvider();
+ rp.SymbolStream = new MemoryStream(pdb);
+ }
+
+ rp.ReadingMode = ReadingMode.Immediate;
+
+ this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+ }
+
+ Populate();
+ }
+
+ public AssemblyInfo(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ void Populate()
+ {
+ ProcessSourceLink();
+
+ var d2s = new Dictionary<Document, SourceFile>();
+
+ SourceFile FindSource(Document doc)
+ {
+ if (doc == null)
+ return null;
+
+ if (d2s.TryGetValue(doc, out var source))
+ return source;
+
+ var src = new SourceFile(this, sources.Count, doc, GetSourceLinkUrl(doc.Url));
+ sources.Add(src);
+ d2s[doc] = src;
+ return src;
+ };
+
+ foreach (var type in image.GetTypes())
+ {
+ var typeInfo = new TypeInfo(this, type);
+ typesByName[type.FullName] = typeInfo;
+
+ foreach (var method in type.Methods)
+ {
+ foreach (var sp in method.DebugInformation.SequencePoints)
+ {
+ var source = FindSource(sp.Document);
+ var methodInfo = new MethodInfo(this, method, source);
+ methods[method.MetadataToken.RID] = methodInfo;
+ if (source != null)
+ source.AddMethod(methodInfo);
+
+ typeInfo.Methods.Add(methodInfo);
+ }
+ }
+ }
+ }
+
+ private void ProcessSourceLink()
+ {
+ var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault(i => i.Kind == CustomDebugInformationKind.SourceLink);
+
+ if (sourceLinkDebugInfo != null)
+ {
+ var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
+
+ if (sourceLinkContent != null)
+ {
+ var jObject = JObject.Parse(sourceLinkContent)["documents"];
+ sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
+ }
+ }
+ }
+
+ private Uri GetSourceLinkUrl(string document)
+ {
+ if (sourceLinkMappings.TryGetValue(document, out string url))
+ return new Uri(url);
+
+ foreach (var sourceLinkDocument in sourceLinkMappings)
+ {
+ string key = sourceLinkDocument.Key;
+
+ if (Path.GetFileName(key) != "*")
+ {
+ continue;
+ }
+
+ var keyTrim = key.TrimEnd('*');
+
+ if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase))
+ {
+ var docUrlPart = document.Replace(keyTrim, "");
+ return new Uri(sourceLinkDocument.Value.TrimEnd('*') + docUrlPart);
+ }
+ }
+
+ return null;
+ }
+
+ public IEnumerable<SourceFile> Sources => this.sources;
+
+ public Dictionary<string, TypeInfo> TypesByName => this.typesByName;
+ public int Id => id;
+ public string Name => image.Name;
+
+ public SourceFile GetDocById(int document)
+ {
+ return sources.FirstOrDefault(s => s.SourceId.Document == document);
+ }
+
+ public MethodInfo GetMethodByToken(uint token)
+ {
+ methods.TryGetValue(token, out var value);
+ return value;
+ }
+
+ public TypeInfo GetTypeByName(string name)
+ {
+ typesByName.TryGetValue(name, out var res);
+ return res;
+ }
+ }
+
+ internal class SourceFile
+ {
+ Dictionary<uint, MethodInfo> methods;
+ AssemblyInfo assembly;
+ int id;
+ Document doc;
+
+ internal SourceFile(AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
+ {
+ this.methods = new Dictionary<uint, MethodInfo>();
+ this.SourceLinkUri = sourceLinkUri;
+ this.assembly = assembly;
+ this.id = id;
+ this.doc = doc;
+ this.DebuggerFileName = doc.Url.Replace("\\", "/").Replace(":", "");
+
+ this.SourceUri = new Uri((Path.IsPathRooted(doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
+ if (SourceUri.IsFile && File.Exists(SourceUri.LocalPath))
+ {
+ this.Url = this.SourceUri.ToString();
+ }
+ else
+ {
+ this.Url = DotNetUrl;
+ }
+ }
+
+ internal void AddMethod(MethodInfo mi)
+ {
+ if (!this.methods.ContainsKey(mi.Token))
+ this.methods[mi.Token] = mi;
+ }
+
+ public string DebuggerFileName { get; }
+ public string Url { get; }
+ public string AssemblyName => assembly.Name;
+ public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
+
+ public SourceId SourceId => new SourceId(assembly.Id, this.id);
+ public Uri SourceLinkUri { get; }
+ public Uri SourceUri { get; }
+
+ public IEnumerable<MethodInfo> Methods => this.methods.Values;
+
+ public string DocUrl => doc.Url;
+
+ public (int startLine, int startColumn, int endLine, int endColumn) GetExtents()
+ {
+ var start = Methods.OrderBy(m => m.StartLocation.Line).ThenBy(m => m.StartLocation.Column).First();
+ var end = Methods.OrderByDescending(m => m.EndLocation.Line).ThenByDescending(m => m.EndLocation.Column).First();
+ return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
+ }
+
+ async Task<MemoryStream> GetDataAsync(Uri uri, CancellationToken token)
+ {
+ var mem = new MemoryStream();
+ try
+ {
+ if (uri.IsFile && File.Exists(uri.LocalPath))
+ {
+ using (var file = File.Open(SourceUri.LocalPath, FileMode.Open))
+ {
+ await file.CopyToAsync(mem, token).ConfigureAwait(false);
+ mem.Position = 0;
+ }
+ }
+ else if (uri.Scheme == "http" || uri.Scheme == "https")
+ {
+ var client = new HttpClient();
+ using (var stream = await client.GetStreamAsync(uri))
+ {
+ await stream.CopyToAsync(mem, token).ConfigureAwait(false);
+ mem.Position = 0;
+ }
+ }
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ return mem;
+ }
+
+ static HashAlgorithm GetHashAlgorithm(DocumentHashAlgorithm algorithm)
+ {
+ switch (algorithm)
+ {
+ case DocumentHashAlgorithm.SHA1:
+ return SHA1.Create();
+ case DocumentHashAlgorithm.SHA256:
+ return SHA256.Create();
+ case DocumentHashAlgorithm.MD5:
+ return MD5.Create();
+ }
+ return null;
+ }
+
+ bool CheckPdbHash(byte[] computedHash)
+ {
+ if (computedHash.Length != doc.Hash.Length)
+ return false;
+
+ for (var i = 0; i < computedHash.Length; i++)
+ if (computedHash[i] != doc.Hash[i])
+ return false;
+
+ return true;
+ }
+
+ byte[] ComputePdbHash(Stream sourceStream)
+ {
+ var algorithm = GetHashAlgorithm(doc.HashAlgorithm);
+ if (algorithm != null)
+ using (algorithm)
+ return algorithm.ComputeHash(sourceStream);
+
+ return Array.Empty<byte>();
+ }
+
+ public async Task<Stream> GetSourceAsync(bool checkHash, CancellationToken token = default(CancellationToken))
+ {
+ if (doc.EmbeddedSource.Length > 0)
+ return new MemoryStream(doc.EmbeddedSource, false);
+
+ foreach (var url in new[] { SourceUri, SourceLinkUri })
+ {
+ var mem = await GetDataAsync(url, token).ConfigureAwait(false);
+ if (mem != null && (!checkHash || CheckPdbHash(ComputePdbHash(mem))))
+ {
+ mem.Position = 0;
+ return mem;
+ }
+ }
+
+ return MemoryStream.Null;
+ }
+
+ public object ToScriptSource(int executionContextId, object executionContextAuxData)
+ {
+ return new
+ {
+ scriptId = SourceId.ToString(),
+ url = Url,
+ executionContextId,
+ executionContextAuxData,
+ //hash: should be the v8 hash algo, managed implementation is pending
+ dotNetUrl = DotNetUrl,
+ };
+ }
+ }
+
+ internal class DebugStore
+ {
+ List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
+ readonly HttpClient client;
+ readonly ILogger logger;
+
+ public DebugStore(ILogger logger, HttpClient client)
+ {
+ this.client = client;
+ this.logger = logger;
+ }
+
+ public DebugStore(ILogger logger) : this(logger, new HttpClient())
+ { }
+
+ class DebugItem
+ {
+ public string Url { get; set; }
+ public Task<byte[][]> Data { get; set; }
+ }
+
+ public async IAsyncEnumerable<SourceFile> Load(SessionId sessionId, string[] loaded_files, [EnumeratorCancellation] CancellationToken token)
+ {
+ static bool MatchPdb(string asm, string pdb) => Path.ChangeExtension(asm, "pdb") == pdb;
+
+ var asm_files = new List<string>();
+ var pdb_files = new List<string>();
+ foreach (var file_name in loaded_files)
+ {
+ if (file_name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
+ pdb_files.Add(file_name);
+ else
+ asm_files.Add(file_name);
+ }
+
+ List<DebugItem> steps = new List<DebugItem>();
+ foreach (var url in asm_files)
+ {
+ try
+ {
+ var pdb = pdb_files.FirstOrDefault(n => MatchPdb(url, n));
+ steps.Add(
+ new DebugItem
+ {
+ Url = url,
+ Data = Task.WhenAll(client.GetByteArrayAsync(url), pdb != null ? client.GetByteArrayAsync(pdb) : Task.FromResult<byte[]>(null))
+ });
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug($"Failed to read {url} ({e.Message})");
+ }
+ }
+
+ var resolver = new DefaultAssemblyResolver();
+ foreach (var step in steps)
+ {
+ AssemblyInfo assembly = null;
+ try
+ {
+ var bytes = await step.Data.ConfigureAwait(false);
+ assembly = new AssemblyInfo(resolver, step.Url, bytes[0], bytes[1]);
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug($"Failed to load {step.Url} ({e.Message})");
+ }
+ if (assembly == null)
+ continue;
+
+ assemblies.Add(assembly);
+ foreach (var source in assembly.Sources)
+ yield return source;
+ }
+ }
+
+ public IEnumerable<SourceFile> AllSources() => assemblies.SelectMany(a => a.Sources);
+
+ public SourceFile GetFileById(SourceId id) => AllSources().SingleOrDefault(f => f.SourceId.Equals(id));
+
+ public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
+
+ /*
+ V8 uses zero based indexing for both line and column.
+ PPDBs uses one based indexing for both line and column.
+ */
+ static bool Match(SequencePoint sp, SourceLocation start, SourceLocation end)
+ {
+ var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
+ var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
+
+ if (start.Line > spEnd.Line)
+ return false;
+
+ if (start.Column > spEnd.Column && start.Line == spEnd.Line)
+ return false;
+
+ if (end.Line < spStart.Line)
+ return false;
+
+ if (end.Column < spStart.Column && end.Line == spStart.Line)
+ return false;
+
+ return true;
+ }
+
+ public List<SourceLocation> FindPossibleBreakpoints(SourceLocation start, SourceLocation end)
+ {
+ //XXX FIXME no idea what todo with locations on different files
+ if (start.Id != end.Id)
+ {
+ logger.LogDebug($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
+ return null;
+ }
+
+ var sourceId = start.Id;
+
+ var doc = GetFileById(sourceId);
+
+ var res = new List<SourceLocation>();
+ if (doc == null)
+ {
+ logger.LogDebug($"Could not find document {sourceId}");
+ return res;
+ }
+
+ foreach (var method in doc.Methods)
+ {
+ foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+ {
+ if (!sequencePoint.IsHidden && Match(sequencePoint, start, end))
+ res.Add(new SourceLocation(method, sequencePoint));
+ }
+ }
+ return res;
+ }
+
+ /*
+ V8 uses zero based indexing for both line and column.
+ PPDBs uses one based indexing for both line and column.
+ */
+ static bool Match(SequencePoint sp, int line, int column)
+ {
+ var bp = (line: line + 1, column: column + 1);
+
+ if (sp.StartLine > bp.line || sp.EndLine < bp.line)
+ return false;
+
+ //Chrome sends a zero column even if getPossibleBreakpoints say something else
+ if (column == 0)
+ return true;
+
+ if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
+ return false;
+
+ if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
+ return false;
+
+ return true;
+ }
+
+ public IEnumerable<SourceLocation> FindBreakpointLocations(BreakpointRequest request)
+ {
+ request.TryResolve(this);
+
+ var asm = assemblies.FirstOrDefault(a => a.Name.Equals(request.Assembly, StringComparison.OrdinalIgnoreCase));
+ var sourceFile = asm?.Sources?.SingleOrDefault(s => s.DebuggerFileName.Equals(request.File, StringComparison.OrdinalIgnoreCase));
+
+ if (sourceFile == null)
+ yield break;
+
+ foreach (var method in sourceFile.Methods)
+ {
+ foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+ {
+ if (!sequencePoint.IsHidden && Match(sequencePoint, request.Line, request.Column))
+ yield return new SourceLocation(method, sequencePoint);
+ }
+ }
+ }
+
+ public string ToUrl(SourceLocation location) => location != null ? GetFileById(location.Id).Url : "";
+ }
}