Adjust formatting, add .editorconfig
[mono-project.git] / sdks / wasm / Mono.WebAssembly.DebuggerProxy / DebugStore.cs
blob32799b6f6c5265def2046b9acbd66c15ec66265a
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;
12 namespace WsProxy {
13 internal class BreakPointRequest {
14 public string Assembly { get; private set; }
15 public string File { get; private set; }
16 public int Line { get; private set; }
17 public int Column { get; private set; }
19 public override string ToString () {
20 return $"BreakPointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
23 public static BreakPointRequest Parse (JObject args)
25 if (args == null)
26 return null;
28 var url = args? ["url"]?.Value<string> ();
29 if (!url.StartsWith ("dotnet://", StringComparison.InvariantCulture))
30 return null;
32 var parts = ParseDocumentUrl(url);
33 if (parts.Assembly == null)
34 return null;
36 var line = args? ["lineNumber"]?.Value<int> ();
37 var column = args? ["columnNumber"]?.Value<int> ();
38 if (line == null || column == null)
39 return null;
41 return new BreakPointRequest () {
42 Assembly = parts.Assembly,
43 File = parts.DocumentPath,
44 Line = line.Value,
45 Column = column.Value
49 static (string Assembly, string DocumentPath) ParseDocumentUrl(string url)
51 if (Uri.TryCreate(url, UriKind.Absolute, out var docUri) && docUri.Scheme == "dotnet")
53 return (
54 docUri.Host,
55 docUri.PathAndQuery.Substring(1)
58 else
60 return (null, null);
66 internal class VarInfo {
67 public VarInfo (VariableDebugInformation v)
69 this.Name = v.Name;
70 this.Index = v.Index;
73 public VarInfo (ParameterDefinition p)
75 this.Name = p.Name;
76 this.Index = (p.Index + 1) * -1;
78 public string Name { get; private set; }
79 public int Index { get; private set; }
82 public override string ToString ()
84 return $"(var-info [{Index}] '{Name}')";
89 internal class CliLocation {
91 private MethodInfo method;
92 private int offset;
94 public CliLocation (MethodInfo method, int offset)
96 this.method = method;
97 this.offset = offset;
100 public MethodInfo Method { get => method; }
101 public int Offset { get => offset; }
105 internal class SourceLocation {
106 SourceId id;
107 int line;
108 int column;
109 CliLocation cliLoc;
111 public SourceLocation (SourceId id, int line, int column)
113 this.id = id;
114 this.line = line;
115 this.column = column;
118 public SourceLocation (MethodInfo mi, SequencePoint sp)
120 this.id = mi.SourceId;
121 this.line = sp.StartLine;
122 this.column = sp.StartColumn - 1;
123 this.cliLoc = new CliLocation (mi, sp.Offset);
126 public SourceId Id { get => id; }
127 public int Line { get => line; }
128 public int Column { get => column; }
129 public CliLocation CliLocation => this.cliLoc;
131 public override string ToString ()
133 return $"{id}:{Line}:{Column}";
136 public static SourceLocation Parse (JObject obj)
138 if (obj == null)
139 return null;
141 var id = SourceId.TryParse (obj ["scriptId"]?.Value<string> ());
142 var line = obj ["lineNumber"]?.Value<int> ();
143 var column = obj ["columnNumber"]?.Value<int> ();
144 if (id == null || line == null || column == null)
145 return null;
147 return new SourceLocation (id, line.Value, column.Value);
150 internal JObject ToJObject ()
152 return JObject.FromObject (new {
153 scriptId = id.ToString (),
154 lineNumber = line,
155 columnNumber = column
161 internal class SourceId {
162 readonly int assembly, document;
164 public int Assembly => assembly;
165 public int Document => document;
167 internal SourceId (int assembly, int document)
169 this.assembly = assembly;
170 this.document = document;
174 public SourceId (string id)
176 id = id.Substring ("dotnet://".Length);
177 var sp = id.Split ('_');
178 this.assembly = int.Parse (sp [0]);
179 this.document = int.Parse (sp [1]);
182 public static SourceId TryParse (string id)
184 if (!id.StartsWith ("dotnet://", StringComparison.InvariantCulture))
185 return null;
186 return new SourceId (id);
189 public override string ToString ()
191 return $"dotnet://{assembly}_{document}";
194 public override bool Equals (object obj)
196 if (obj == null)
197 return false;
198 SourceId that = obj as SourceId;
199 return that.assembly == this.assembly && that.document == this.document;
202 public override int GetHashCode ()
204 return this.assembly.GetHashCode () ^ this.document.GetHashCode ();
207 public static bool operator == (SourceId a, SourceId b)
209 if ((object)a == null)
210 return (object)b == null;
211 return a.Equals (b);
214 public static bool operator != (SourceId a, SourceId b)
216 return !a.Equals (b);
220 internal class MethodInfo {
221 AssemblyInfo assembly;
222 internal MethodDefinition methodDef;
223 SourceFile source;
225 public SourceId SourceId => source.SourceId;
227 public string Name => methodDef.Name;
229 public SourceLocation StartLocation { get; private set; }
230 public SourceLocation EndLocation { get; private set; }
231 public AssemblyInfo Assembly => assembly;
232 public int Token => (int)methodDef.MetadataToken.RID;
234 public MethodInfo (AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
236 this.assembly = assembly;
237 this.methodDef = methodDef;
238 this.source = source;
240 var sps = methodDef.DebugInformation.SequencePoints;
241 if (sps != null && sps.Count > 0) {
242 StartLocation = new SourceLocation (this, sps [0]);
243 EndLocation = new SourceLocation (this, sps [sps.Count - 1]);
248 public SourceLocation GetLocationByIl (int pos)
250 SequencePoint prev = null;
251 foreach (var sp in methodDef.DebugInformation.SequencePoints) {
252 if (sp.Offset > pos)
253 break;
254 prev = sp;
257 if (prev != null)
258 return new SourceLocation (this, prev);
260 return null;
263 public VarInfo [] GetLiveVarsAt (int offset)
265 var res = new List<VarInfo> ();
267 res.AddRange (methodDef.Parameters.Select (p => new VarInfo (p)));
269 res.AddRange (methodDef.DebugInformation.GetScopes ()
270 .Where (s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
271 .SelectMany (s => s.Variables)
272 .Where (v => !v.IsDebuggerHidden)
273 .Select (v => new VarInfo (v)));
276 return res.ToArray ();
281 internal class AssemblyInfo {
282 static int next_id;
283 ModuleDefinition image;
284 readonly int id;
285 Dictionary<int, MethodInfo> methods = new Dictionary<int, MethodInfo> ();
286 private Dictionary<string, string> _sourceLinkMappings = new Dictionary<string, string>();
287 readonly List<SourceFile> sources = new List<SourceFile>();
289 public AssemblyInfo(byte[] assembly, byte[] pdb) {
290 lock (typeof(AssemblyInfo)) {
291 this.id = ++next_id;
294 try {
295 ReaderParameters rp = new ReaderParameters(/*ReadingMode.Immediate*/);
296 if (pdb != null) {
297 rp.ReadSymbols = true;
298 rp.SymbolReaderProvider = new PortablePdbReaderProvider();
299 rp.SymbolStream = new MemoryStream(pdb);
302 rp.ReadingMode = ReadingMode.Immediate;
303 rp.InMemory = true;
305 this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
307 catch (BadImageFormatException ex) {
308 Console.WriteLine("Failed to read assembly as portable PDB");
311 if (this.image == null) {
312 ReaderParameters rp = new ReaderParameters(/*ReadingMode.Immediate*/);
313 if (pdb != null) {
314 rp.ReadSymbols = true;
315 rp.SymbolReaderProvider = new NativePdbReaderProvider();
316 rp.SymbolStream = new MemoryStream(pdb);
319 rp.ReadingMode = ReadingMode.Immediate;
320 rp.InMemory = true;
322 this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
325 Populate();
328 public AssemblyInfo ()
332 void Populate ()
334 ProcessSourceLink();
336 var d2s = new Dictionary<Document, SourceFile> ();
338 Func<Document, SourceFile> get_src = (doc) => {
339 if (doc == null)
340 return null;
341 if (d2s.ContainsKey (doc))
342 return d2s [doc];
343 var src = new SourceFile (this, sources.Count, doc, GetSourceLinkUrl(doc.Url));
344 sources.Add (src);
345 d2s [doc] = src;
346 return src;
349 foreach (var m in image.GetTypes ().SelectMany (t => t.Methods)) {
350 Document first_doc = null;
351 foreach (var sp in m.DebugInformation.SequencePoints) {
352 if (first_doc == null && !sp.Document.Url.EndsWith(".g.cs")) {
353 first_doc = sp.Document;
355 // else if (first_doc != sp.Document) {
356 // //FIXME this is needed for (c)ctors in corlib
357 // throw new Exception ($"Cant handle multi-doc methods in {m}");
361 if (first_doc == null) {
362 // all generated files
363 first_doc = m.DebugInformation.SequencePoints.FirstOrDefault()?.Document;
366 if (first_doc != null) {
367 var src = get_src(first_doc);
368 var mi = new MethodInfo(this, m, src);
369 int mt = (int)m.MetadataToken.RID;
370 this.methods[mt] = mi;
371 if (src != null)
372 src.AddMethod(mi);
377 private void ProcessSourceLink() {
378 var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault(i => i.Kind == CustomDebugInformationKind.SourceLink);
380 if (sourceLinkDebugInfo != null) {
381 var sourceLinkContent = ((SourceLinkDebugInformation)sourceLinkDebugInfo).Content;
383 if (sourceLinkContent != null) {
384 var jObject = JObject.Parse(sourceLinkContent)["documents"];
385 _sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
390 private Uri GetSourceLinkUrl(string document) {
391 if (_sourceLinkMappings.TryGetValue(document, out string url)) {
392 return new Uri(url);
395 foreach (var sourceLinkDocument in _sourceLinkMappings) {
396 string key = sourceLinkDocument.Key;
397 if (Path.GetFileName(key) != "*") {
398 continue;
401 var keyTrim = key.TrimEnd('*');
402 var docUrlPart = document.Replace(keyTrim, "");
404 return new Uri(sourceLinkDocument.Value.TrimEnd('*') + docUrlPart);
407 return null;
410 public string GetRelativePath(string relativeTo, string path) {
411 var uri = new Uri(relativeTo, UriKind.RelativeOrAbsolute);
412 var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path, UriKind.RelativeOrAbsolute)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
413 if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false) {
414 rel = $".{ Path.DirectorySeparatorChar }{ rel }";
416 return rel;
419 public IEnumerable<SourceFile> Sources {
420 get { return this.sources; }
423 public int Id => id;
424 public string Name => image.Name;
426 public SourceFile GetDocById (int document)
428 return sources.FirstOrDefault (s => s.SourceId.Document == document);
431 public MethodInfo GetMethodByToken(int token)
433 methods.TryGetValue(token, out var value);
434 return value;
438 internal class SourceFile {
439 HashSet<MethodInfo> methods;
440 Uri sourceLinkUri;
441 AssemblyInfo assembly;
442 int id;
443 Document doc;
445 internal SourceFile (AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
447 this.methods = new HashSet<MethodInfo> ();
448 this.sourceLinkUri = sourceLinkUri;
449 this.assembly = assembly;
450 this.id = id;
451 this.doc = doc;
452 this.FileName = doc.Url.Replace("\\", "/").Replace(":", "");
455 internal void AddMethod (MethodInfo mi)
457 this.methods.Add (mi);
459 public string FileName { get; }
460 public string Url => $"dotnet://{assembly.Name}/{FileName}";
461 public string DocHashCode => "abcdee" + id;
462 public SourceId SourceId => new SourceId (assembly.Id, this.id);
463 public Uri OriginalSourcePath
464 => sourceLinkUri ?? new Uri(doc.Url.StartsWith("/") ? "file://" : "" + doc.Url, UriKind.RelativeOrAbsolute);
466 public IEnumerable<MethodInfo> Methods => this.methods;
469 internal class DebugStore {
470 List<AssemblyInfo> assemblies = new List<AssemblyInfo> ();
472 public DebugStore (string[] loaded_files)
474 bool MatchPdb (string asm, string pdb) {
475 return Path.ChangeExtension (asm, "pdb") == pdb;
478 var asm_files = new List<string> ();
479 var pdb_files = new List<string> ();
480 foreach (var f in loaded_files) {
481 var file_name = f;
482 if (file_name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
483 pdb_files.Add(file_name);
484 else
485 asm_files.Add(file_name);
488 //FIXME make this parallel
489 foreach (var p in asm_files) {
490 try {
491 var pdb = pdb_files.FirstOrDefault(n => MatchPdb(p, n));
492 HttpClient h = new HttpClient();
493 var assembly_bytes = h.GetByteArrayAsync(p).Result;
494 byte[] pdb_bytes = null;
495 if (pdb != null)
496 pdb_bytes = h.GetByteArrayAsync(pdb).Result;
498 this.assemblies.Add(new AssemblyInfo(assembly_bytes, pdb_bytes));
500 catch (Exception e) {
501 Console.WriteLine($"Failed to read {p} ({e.Message})");
506 public IEnumerable<SourceFile> AllSources ()
508 foreach (var a in assemblies) {
509 foreach (var s in a.Sources)
510 yield return s;
515 public SourceFile GetFileById (SourceId id)
517 return AllSources ().FirstOrDefault (f => f.SourceId.Equals (id));
520 public AssemblyInfo GetAssemblyByName (string name)
522 return assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
526 Matching logic here is hilarious and it goes like this:
527 We inject one line at the top of all sources to make it easy to identify them [1].
528 V8 uses zero based indexing for both line and column.
529 PPDBs uses one based indexing for both line and column.
530 Which means that:
531 - for lines, values are already adjusted (v8 numbers come +1 due to the injected line)
532 - for columns, we need to +1 the v8 numbers
533 [1] It's so we can deal with the Runtime.compileScript ide cmd
535 static bool Match (SequencePoint sp, SourceLocation start, SourceLocation end)
537 if (start.Line > sp.StartLine)
538 return false;
539 if ((start.Column + 1) > sp.StartColumn && start.Line == sp.StartLine)
540 return false;
542 if (end.Line < sp.EndLine)
543 return false;
545 if ((end.Column + 1) < sp.EndColumn && end.Line == sp.EndLine)
546 return false;
548 return true;
551 public List<SourceLocation> FindPossibleBreakpoints (SourceLocation start, SourceLocation end)
553 //XXX FIXME no idea what todo with locations on different files
554 if (start.Id != end.Id)
555 return null;
556 var src_id = start.Id;
558 var doc = GetFileById (src_id);
560 var res = new List<SourceLocation> ();
561 if (doc == null) {
562 //FIXME we need to write up logging here
563 Console.WriteLine ($"Could not find document {src_id}");
564 return res;
567 foreach (var m in doc.Methods) {
568 foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
569 if (Match (sp, start, end))
570 res.Add (new SourceLocation (m, sp));
573 return res;
577 Matching logic here is hilarious and it goes like this:
578 We inject one line at the top of all sources to make it easy to identify them [1].
579 V8 uses zero based indexing for both line and column.
580 PPDBs uses one based indexing for both line and column.
581 Which means that:
582 - for lines, values are already adjusted (v8 numbers come + 1 due to the injected line)
583 - for columns, we need to +1 the v8 numbers
584 [1] It's so we can deal with the Runtime.compileScript ide cmd
586 static bool Match (SequencePoint sp, int line, int column)
588 if (sp.StartLine > line || sp.EndLine < line)
589 return false;
591 //Chrome sends a zero column even if getPossibleBreakpoints say something else
592 if (column == 0)
593 return true;
595 if (sp.StartColumn > (column + 1) && sp.StartLine == line)
596 return false;
598 if (sp.EndColumn < (column + 1) && sp.EndLine == line)
599 return false;
601 return true;
604 public SourceLocation FindBestBreakpoint (BreakPointRequest req)
606 var asm = this.assemblies.FirstOrDefault (a => a.Name.Equals(req.Assembly, StringComparison.OrdinalIgnoreCase));
607 var src = asm.Sources.FirstOrDefault (s => s.FileName.Equals(req.File, StringComparison.OrdinalIgnoreCase));
609 foreach (var m in src.Methods) {
610 foreach (var sp in m.methodDef.DebugInformation.SequencePoints) {
611 //FIXME handle multi doc methods
612 if (Match (sp, req.Line, req.Column))
613 return new SourceLocation (m, sp);
617 return null;
620 public string ToUrl(SourceLocation location)
621 => location != null ? GetFileById(location.Id).Url : "";