Remove the logic that prepends a line in source references
[mono-project.git] / sdks / wasm / Mono.WebAssembly.DebuggerProxy / DebugStore.cs
blobe1e9b7392ba854634cce785eb6fe9e7329a5af04
1 using System;
2 using System.IO;
3 using System.Collections.Generic;
4 using Mono.Cecil;
5 using Mono.Cecil.Cil;
6 using System.Linq;
7 using Newtonsoft.Json.Linq;
8 using System.Net.Http;
9 using Mono.Cecil.Pdb;
10 using Newtonsoft.Json;
11 using System.Text.RegularExpressions;
13 namespace WsProxy {
14 internal class BreakPointRequest {
15 public string Assembly { get; private set; }
16 public string File { get; private set; }
17 public int Line { get; private set; }
18 public int Column { get; private set; }
20 public override string ToString () {
21 return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
24 public static BreakPointRequest Parse (JObject args, DebugStore store)
26 if (args == null)
27 return null;
29 var url = args? ["url"]?.Value<string> ();
30 if (url == null) {
31 var urlRegex = args?["urlRegex"].Value<string>();
32 var sourceFile = store.GetFileByUrlRegex (urlRegex);
34 url = sourceFile?.DotNetUrl;
37 if (url != null && !url.StartsWith ("dotnet://", StringComparison.InvariantCulture)) {
38 var sourceFile = store.GetFileByUrl (url);
39 url = sourceFile?.DotNetUrl;
42 if (url == null)
43 return null;
45 var parts = ParseDocumentUrl (url);
46 if (parts.Assembly == null)
47 return null;
49 var line = args? ["lineNumber"]?.Value<int> ();
50 var column = args? ["columnNumber"]?.Value<int> ();
51 if (line == null || column == null)
52 return null;
54 return new BreakPointRequest () {
55 Assembly = parts.Assembly,
56 File = parts.DocumentPath,
57 Line = line.Value,
58 Column = column.Value
62 static (string Assembly, string DocumentPath) ParseDocumentUrl (string url)
64 if (Uri.TryCreate (url, UriKind.Absolute, out var docUri) && docUri.Scheme == "dotnet") {
65 return (
66 docUri.Host,
67 docUri.PathAndQuery.Substring (1)
69 } else {
70 return (null, null);
76 internal class VarInfo {
77 public VarInfo (VariableDebugInformation v)
79 this.Name = v.Name;
80 this.Index = v.Index;
83 public VarInfo (ParameterDefinition p)
85 this.Name = p.Name;
86 this.Index = (p.Index + 1) * -1;
88 public string Name { get; private set; }
89 public int Index { get; private set; }
92 public override string ToString ()
94 return $"(var-info [{Index}] '{Name}')";
99 internal class CliLocation {
101 private MethodInfo method;
102 private int offset;
104 public CliLocation (MethodInfo method, int offset)
106 this.method = method;
107 this.offset = offset;
110 public MethodInfo Method { get => method; }
111 public int Offset { get => offset; }
115 internal class SourceLocation {
116 SourceId id;
117 int line;
118 int column;
119 CliLocation cliLoc;
121 public SourceLocation (SourceId id, int line, int column)
123 this.id = id;
124 this.line = line;
125 this.column = column;
128 public SourceLocation (MethodInfo mi, SequencePoint sp)
130 this.id = mi.SourceId;
131 this.line = sp.StartLine - 1;
132 this.column = sp.StartColumn - 1;
133 this.cliLoc = new CliLocation (mi, sp.Offset);
136 public SourceId Id { get => id; }
137 public int Line { get => line; }
138 public int Column { get => column; }
139 public CliLocation CliLocation => this.cliLoc;
141 public override string ToString ()
143 return $"{id}:{Line}:{Column}";
146 public static SourceLocation Parse (JObject obj)
148 if (obj == null)
149 return null;
151 var id = SourceId.TryParse (obj ["scriptId"]?.Value<string> ());
152 var line = obj ["lineNumber"]?.Value<int> ();
153 var column = obj ["columnNumber"]?.Value<int> ();
154 if (id == null || line == null || column == null)
155 return null;
157 return new SourceLocation (id, line.Value, column.Value);
160 internal JObject ToJObject ()
162 return JObject.FromObject (new {
163 scriptId = id.ToString (),
164 lineNumber = line,
165 columnNumber = column
171 internal class SourceId {
172 readonly int assembly, document;
174 public int Assembly => assembly;
175 public int Document => document;
177 internal SourceId (int assembly, int document)
179 this.assembly = assembly;
180 this.document = document;
184 public SourceId (string id)
186 id = id.Substring ("dotnet://".Length);
187 var sp = id.Split ('_');
188 this.assembly = int.Parse (sp [0]);
189 this.document = int.Parse (sp [1]);
192 public static SourceId TryParse (string id)
194 if (!id.StartsWith ("dotnet://", StringComparison.InvariantCulture))
195 return null;
196 return new SourceId (id);
199 public override string ToString ()
201 return $"dotnet://{assembly}_{document}";
204 public override bool Equals (object obj)
206 if (obj == null)
207 return false;
208 SourceId that = obj as SourceId;
209 return that.assembly == this.assembly && that.document == this.document;
212 public override int GetHashCode ()
214 return this.assembly.GetHashCode () ^ this.document.GetHashCode ();
217 public static bool operator == (SourceId a, SourceId b)
219 if ((object)a == null)
220 return (object)b == null;
221 return a.Equals (b);
224 public static bool operator != (SourceId a, SourceId b)
226 return !a.Equals (b);
230 internal class MethodInfo {
231 AssemblyInfo assembly;
232 internal MethodDefinition methodDef;
233 SourceFile source;
235 public SourceId SourceId => source.SourceId;
237 public string Name => methodDef.Name;
239 public SourceLocation StartLocation { get; private set; }
240 public SourceLocation EndLocation { get; private set; }
241 public AssemblyInfo Assembly => assembly;
242 public int Token => (int)methodDef.MetadataToken.RID;
244 public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
246 this.assembly = assembly;
247 this.methodDef = methodDef;
248 this.source = source;
250 var sps = methodDef.DebugInformation.SequencePoints;
251 if (sps != null && sps.Count > 0) {
252 StartLocation = new SourceLocation (this, sps [0]);
253 EndLocation = new SourceLocation (this, sps [sps.Count - 1]);
258 public SourceLocation GetLocationByIl (int pos)
260 SequencePoint prev = null;
261 foreach (var sp in methodDef.DebugInformation.SequencePoints) {
262 if (sp.Offset > pos)
263 break;
264 prev = sp;
267 if (prev != null)
268 return new SourceLocation (this, prev);
270 return null;
273 public VarInfo [] GetLiveVarsAt (int offset)
275 var res = new List<VarInfo> ();
277 res.AddRange (methodDef.Parameters.Select (p => new VarInfo (p)));
279 res.AddRange (methodDef.DebugInformation.GetScopes ()
280 .Where (s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
281 .SelectMany (s => s.Variables)
282 .Where (v => !v.IsDebuggerHidden)
283 .Select (v => new VarInfo (v)));
286 return res.ToArray ();
290 internal class AssemblyInfo {
291 static int next_id;
292 ModuleDefinition image;
293 readonly int id;
294 Dictionary<int, MethodInfo> methods = new Dictionary<int, MethodInfo> ();
295 Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
296 readonly List<SourceFile> sources = new List<SourceFile>();
298 public AssemblyInfo (byte[] assembly, byte[] pdb)
300 lock (typeof (AssemblyInfo)) {
301 this.id = ++next_id;
304 try {
305 ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
306 if (pdb != null) {
307 rp.ReadSymbols = true;
308 rp.SymbolReaderProvider = new PortablePdbReaderProvider ();
309 rp.SymbolStream = new MemoryStream (pdb);
312 rp.ReadingMode = ReadingMode.Immediate;
313 rp.InMemory = true;
315 this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
316 } catch (BadImageFormatException ex) {
317 Console.WriteLine ($"Failed to read assembly as portable PDB: {ex.Message}");
320 if (this.image == null) {
321 ReaderParameters rp = new ReaderParameters (/*ReadingMode.Immediate*/);
322 if (pdb != null) {
323 rp.ReadSymbols = true;
324 rp.SymbolReaderProvider = new NativePdbReaderProvider ();
325 rp.SymbolStream = new MemoryStream (pdb);
328 rp.ReadingMode = ReadingMode.Immediate;
329 rp.InMemory = true;
331 this.image = ModuleDefinition.ReadModule (new MemoryStream (assembly), rp);
334 Populate ();
337 public AssemblyInfo ()
341 void Populate ()
343 ProcessSourceLink();
345 var d2s = new Dictionary<Document, SourceFile> ();
347 Func<Document, SourceFile> get_src = (doc) => {
348 if (doc == null)
349 return null;
350 if (d2s.ContainsKey (doc))
351 return d2s [doc];
352 var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl (doc.Url));
353 sources.Add (src);
354 d2s [doc] = src;
355 return src;
358 foreach (var m in image.GetTypes().SelectMany(t => t.Methods)) {
359 Document first_doc = null;
360 foreach (var sp in m.DebugInformation.SequencePoints) {
361 if (first_doc == null && !sp.Document.Url.EndsWith (".g.cs")) {
362 first_doc = sp.Document;
364 // else if (first_doc != sp.Document) {
365 // //FIXME this is needed for (c)ctors in corlib
366 // throw new Exception ($"Cant handle multi-doc methods in {m}");
370 if (first_doc == null) {
371 // all generated files
372 first_doc = m.DebugInformation.SequencePoints.FirstOrDefault ()?.Document;
375 if (first_doc != null) {
376 var src = get_src (first_doc);
377 var mi = new MethodInfo (this, m, src);
378 int mt = (int)m.MetadataToken.RID;
379 this.methods [mt] = mi;
380 if (src != null)
381 src.AddMethod (mi);
386 private void ProcessSourceLink ()
388 var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault (i => i.Kind == CustomDebugInformationKind.SourceLink);
390 if (sourceLinkDebugInfo != null) {
391 var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
393 if (sourceLinkContent != null) {
394 var jObject = JObject.Parse (sourceLinkContent) ["documents"];
395 sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>> (jObject.ToString ());
400 private Uri GetSourceLinkUrl (string document)
402 if (sourceLinkMappings.TryGetValue (document, out string url)) {
403 return new Uri (url);
406 foreach (var sourceLinkDocument in sourceLinkMappings) {
407 string key = sourceLinkDocument.Key;
409 if (Path.GetFileName (key) != "*") {
410 continue;
413 var keyTrim = key.TrimEnd ('*');
415 if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase)) {
416 var docUrlPart = document.Replace (keyTrim, "");
417 return new Uri (sourceLinkDocument.Value.TrimEnd ('*') + docUrlPart);
421 return null;
424 private string GetRelativePath (string relativeTo, string path)
426 var uri = new Uri (relativeTo, UriKind.RelativeOrAbsolute);
427 var rel = Uri.UnescapeDataString (uri.MakeRelativeUri (new Uri (path, UriKind.RelativeOrAbsolute)).ToString ()).Replace (Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
428 if (rel.Contains (Path.DirectorySeparatorChar.ToString ()) == false) {
429 rel = $".{ Path.DirectorySeparatorChar }{ rel }";
431 return rel;
434 public IEnumerable<SourceFile> Sources {
435 get { return this.sources; }
438 public int Id => id;
439 public string Name => image.Name;
441 public SourceFile GetDocById (int document)
443 return sources.FirstOrDefault (s => s.SourceId.Document == document);
446 public MethodInfo GetMethodByToken (int token)
448 methods.TryGetValue (token, out var value);
449 return value;
453 internal class SourceFile {
454 HashSet<MethodInfo> methods;
455 AssemblyInfo assembly;
456 int id;
457 Document doc;
459 internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
461 this.methods = new HashSet<MethodInfo> ();
462 this.SourceLinkUri = sourceLinkUri;
463 this.assembly = assembly;
464 this.id = id;
465 this.doc = doc;
466 this.DebuggerFileName = doc.Url.Replace ("\\", "/").Replace (":", "");
468 this.SourceUri = new Uri ((Path.IsPathRooted (doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
469 if (SourceUri.IsFile && File.Exists (SourceUri.LocalPath)) {
470 this.Url = this.SourceUri.ToString ();
471 } else {
472 this.Url = DotNetUrl;
477 internal void AddMethod (MethodInfo mi)
479 this.methods.Add (mi);
481 public string DebuggerFileName { get; }
482 public string Url { get; }
483 public string AssemblyName => assembly.Name;
484 public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
485 public string DocHashCode => "abcdee" + id;
486 public SourceId SourceId => new SourceId (assembly.Id, this.id);
487 public Uri SourceLinkUri { get; }
488 public Uri SourceUri { get; }
490 public IEnumerable<MethodInfo> Methods => this.methods;
493 internal class DebugStore {
494 List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
496 public DebugStore (string [] loaded_files)
498 bool MatchPdb (string asm, string pdb)
500 return Path.ChangeExtension (asm, "pdb") == pdb;
503 var asm_files = new List<string> ();
504 var pdb_files = new List<string> ();
505 foreach (var f in loaded_files) {
506 var file_name = f;
507 if (file_name.EndsWith (".pdb", StringComparison.OrdinalIgnoreCase))
508 pdb_files.Add (file_name);
509 else
510 asm_files.Add (file_name);
513 //FIXME make this parallel
514 foreach (var p in asm_files) {
515 try {
516 var pdb = pdb_files.FirstOrDefault (n => MatchPdb (p, n));
517 HttpClient h = new HttpClient ();
518 var assembly_bytes = h.GetByteArrayAsync (p).Result;
519 byte [] pdb_bytes = null;
520 if (pdb != null)
521 pdb_bytes = h.GetByteArrayAsync (pdb).Result;
523 this.assemblies.Add (new AssemblyInfo (assembly_bytes, pdb_bytes));
524 } catch (Exception e) {
525 Console.WriteLine ($"Failed to read {p} ({e.Message})");
530 public IEnumerable<SourceFile> AllSources ()
532 foreach (var a in assemblies) {
533 foreach (var s in a.Sources)
534 yield return s;
538 public SourceFile GetFileById (SourceId id)
540 return AllSources ().FirstOrDefault (f => f.SourceId.Equals (id));
543 public AssemblyInfo GetAssemblyByName (string name)
545 return assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
549 V8 uses zero based indexing for both line and column.
550 PPDBs uses one based indexing for both line and column.
552 static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
554 var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
555 var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
557 if (start.Line > spStart.Line)
558 return false;
559 if (start.Column > spStart.Column && start.Line == sp.StartLine)
560 return false;
562 if (end.Line < spEnd.Line)
563 return false;
565 if (end.Column < spEnd.Column && end.Line == spEnd.Line)
566 return false;
568 return true;
571 public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end)
573 //XXX FIXME no idea what todo with locations on different files
574 if (start.Id != end.Id)
575 return null;
576 var src_id = start.Id;
578 var doc = GetFileById (src_id);
580 var res = new List<SourceLocation> ();
581 if (doc == null) {
582 //FIXME we need to write up logging here
583 Console.WriteLine ($"Could not find document {src_id}");
584 return res;
587 foreach (var m in doc.Methods) {
588 foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
589 if (Match (sp, start, end))
590 res.Add (new SourceLocation (m, sp));
593 return res;
597 V8 uses zero based indexing for both line and column.
598 PPDBs uses one based indexing for both line and column.
600 static bool Match (SequencePoint sp, int line, int column)
602 var bp = (line: line + 1, column: column + 1);
604 if (sp.StartLine > bp.line || sp.EndLine < bp.line)
605 return false;
607 //Chrome sends a zero column even if getPossibleBreakpoints say something else
608 if (column == 0)
609 return true;
611 if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
612 return false;
614 if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
615 return false;
617 return true;
620 public SourceLocation FindBestBreakpoint (BreakPointRequest req)
622 var asm = assemblies.FirstOrDefault (a => a.Name.Equals (req.Assembly, StringComparison.OrdinalIgnoreCase));
623 var src = asm?.Sources?.FirstOrDefault (s => s.DebuggerFileName.Equals (req.File, StringComparison.OrdinalIgnoreCase));
625 if (src == null)
626 return null;
628 foreach (var m in src.Methods) {
629 foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
630 //FIXME handle multi doc methods
631 if (Match (sp, req.Line, req.Column))
632 return new SourceLocation (m, sp);
636 return null;
639 public string ToUrl (SourceLocation location)
640 => location != null ? GetFileById (location.Id).Url : "";
642 public SourceFile GetFileByUrlRegex (string urlRegex)
644 var regex = new Regex (urlRegex);
645 return AllSources ().FirstOrDefault (file => regex.IsMatch (file.Url.ToString()));
648 public SourceFile GetFileByUrl (string url)
649 => AllSources ().FirstOrDefault (file => file.Url.ToString() == url);