3 using System
.Collections
.Generic
;
7 using Newtonsoft
.Json
.Linq
;
10 using Newtonsoft
.Json
;
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
)
28 var url
= args
? ["url"]?.Value
<string> ();
29 if (!url
.StartsWith ("dotnet://", StringComparison
.InvariantCulture
))
32 var parts
= ParseDocumentUrl(url
);
33 if (parts
.Assembly
== null)
36 var line
= args
? ["lineNumber"]?.Value
<int> ();
37 var column
= args
? ["columnNumber"]?.Value
<int> ();
38 if (line
== null || column
== null)
41 return new BreakPointRequest () {
42 Assembly
= parts
.Assembly
,
43 File
= parts
.DocumentPath
,
49 static (string Assembly
, string DocumentPath
) ParseDocumentUrl(string url
)
51 if (Uri
.TryCreate(url
, UriKind
.Absolute
, out var docUri
) && docUri
.Scheme
== "dotnet")
55 docUri
.PathAndQuery
.Substring(1)
66 internal class VarInfo
{
67 public VarInfo (VariableDebugInformation v
)
73 public VarInfo (ParameterDefinition p
)
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
;
94 public CliLocation (MethodInfo method
, int offset
)
100 public MethodInfo Method { get => method; }
101 public int Offset { get => offset; }
105 internal class SourceLocation
{
111 public SourceLocation (SourceId id
, int line
, int column
)
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
)
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)
147 return new SourceLocation (id
, line
.Value
, column
.Value
);
150 internal JObject
ToJObject ()
152 return JObject
.FromObject (new {
153 scriptId
= id
.ToString (),
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
))
186 return new SourceId (id
);
189 public override string ToString ()
191 return $"dotnet://{assembly}_{document}";
194 public override bool Equals (object obj
)
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;
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
;
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
) {
258 return new SourceLocation (this, prev
);
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
{
283 ModuleDefinition image
;
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
)) {
295 ReaderParameters rp
= new ReaderParameters(/*ReadingMode.Immediate*/);
297 rp
.ReadSymbols
= true;
298 rp
.SymbolReaderProvider
= new PortablePdbReaderProvider();
299 rp
.SymbolStream
= new MemoryStream(pdb
);
302 rp
.ReadingMode
= ReadingMode
.Immediate
;
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*/);
314 rp
.ReadSymbols
= true;
315 rp
.SymbolReaderProvider
= new NativePdbReaderProvider();
316 rp
.SymbolStream
= new MemoryStream(pdb
);
319 rp
.ReadingMode
= ReadingMode
.Immediate
;
322 this.image
= ModuleDefinition
.ReadModule(new MemoryStream(assembly
), rp
);
328 public AssemblyInfo ()
336 var d2s
= new Dictionary
<Document
, SourceFile
> ();
338 Func
<Document
, SourceFile
> get_src
= (doc
) => {
341 if (d2s
.ContainsKey (doc
))
343 var src
= new SourceFile (this, sources
.Count
, doc
, GetSourceLinkUrl(doc
.Url
));
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
;
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
)) {
395 foreach (var sourceLinkDocument
in _sourceLinkMappings
) {
396 string key
= sourceLinkDocument
.Key
;
397 if (Path
.GetFileName(key
) != "*") {
401 var keyTrim
= key
.TrimEnd('*');
402 var docUrlPart
= document
.Replace(keyTrim
, "");
404 return new Uri(sourceLinkDocument
.Value
.TrimEnd('*') + docUrlPart
);
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 }";
419 public IEnumerable
<SourceFile
> Sources
{
420 get { return this.sources; }
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);
438 internal class SourceFile
{
439 HashSet
<MethodInfo
> methods
;
441 AssemblyInfo assembly
;
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
;
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
) {
482 if (file_name
.EndsWith(".pdb", StringComparison
.OrdinalIgnoreCase
))
483 pdb_files
.Add(file_name
);
485 asm_files
.Add(file_name
);
488 //FIXME make this parallel
489 foreach (var p
in asm_files
) {
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;
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
)
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.
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
)
539 if ((start
.Column
+ 1) > sp
.StartColumn
&& start
.Line
== sp
.StartLine
)
542 if (end
.Line
< sp
.EndLine
)
545 if ((end
.Column
+ 1) < sp
.EndColumn
&& end
.Line
== sp
.EndLine
)
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
)
556 var src_id
= start
.Id
;
558 var doc
= GetFileById (src_id
);
560 var res
= new List
<SourceLocation
> ();
562 //FIXME we need to write up logging here
563 Console
.WriteLine ($"Could not find document {src_id}");
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
));
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.
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
)
591 //Chrome sends a zero column even if getPossibleBreakpoints say something else
595 if (sp
.StartColumn
> (column
+ 1) && sp
.StartLine
== line
)
598 if (sp
.EndColumn
< (column
+ 1) && sp
.EndLine
== line
)
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
);
620 public string ToUrl(SourceLocation location
)
621 => location
!= null ? GetFileById(location
.Id
).Url
: "";