3 using System
.Collections
.Generic
;
7 using Newtonsoft
.Json
.Linq
;
10 using Newtonsoft
.Json
;
11 using System
.Text
.RegularExpressions
;
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
)
29 var url
= args
? ["url"]?.Value
<string> ();
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
;
45 var parts
= ParseDocumentUrl (url
);
46 if (parts
.Assembly
== null)
49 var line
= args
? ["lineNumber"]?.Value
<int> ();
50 var column
= args
? ["columnNumber"]?.Value
<int> ();
51 if (line
== null || column
== null)
54 return new BreakPointRequest () {
55 Assembly
= parts
.Assembly
,
56 File
= parts
.DocumentPath
,
62 static (string Assembly
, string DocumentPath
) ParseDocumentUrl (string url
)
64 if (Uri
.TryCreate (url
, UriKind
.Absolute
, out var docUri
) && docUri
.Scheme
== "dotnet") {
67 docUri
.PathAndQuery
.Substring (1)
76 internal class VarInfo
{
77 public VarInfo (VariableDebugInformation v
)
83 public VarInfo (ParameterDefinition p
)
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
;
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
{
121 public SourceLocation (SourceId id
, int line
, int column
)
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
)
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)
157 return new SourceLocation (id
, line
.Value
, column
.Value
);
160 internal JObject
ToJObject ()
162 return JObject
.FromObject (new {
163 scriptId
= id
.ToString (),
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
))
196 return new SourceId (id
);
199 public override string ToString ()
201 return $"dotnet://{assembly}_{document}";
204 public override bool Equals (object obj
)
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;
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
;
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
) {
268 return new SourceLocation (this, prev
);
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
{
292 ModuleDefinition image
;
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
)) {
305 ReaderParameters rp
= new ReaderParameters (/*ReadingMode.Immediate*/);
307 rp
.ReadSymbols
= true;
308 rp
.SymbolReaderProvider
= new PortablePdbReaderProvider ();
309 rp
.SymbolStream
= new MemoryStream (pdb
);
312 rp
.ReadingMode
= ReadingMode
.Immediate
;
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*/);
323 rp
.ReadSymbols
= true;
324 rp
.SymbolReaderProvider
= new NativePdbReaderProvider ();
325 rp
.SymbolStream
= new MemoryStream (pdb
);
328 rp
.ReadingMode
= ReadingMode
.Immediate
;
331 this.image
= ModuleDefinition
.ReadModule (new MemoryStream (assembly
), rp
);
337 public AssemblyInfo ()
345 var d2s
= new Dictionary
<Document
, SourceFile
> ();
347 Func
<Document
, SourceFile
> get_src
= (doc
) => {
350 if (d2s
.ContainsKey (doc
))
352 var src
= new SourceFile (this, sources
.Count
, doc
, GetSourceLinkUrl (doc
.Url
));
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
;
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
) != "*") {
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
);
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 }";
434 public IEnumerable
<SourceFile
> Sources
{
435 get { return this.sources; }
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);
453 internal class SourceFile
{
454 HashSet
<MethodInfo
> methods
;
455 AssemblyInfo assembly
;
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
;
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 ();
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
) {
507 if (file_name
.EndsWith (".pdb", StringComparison
.OrdinalIgnoreCase
))
508 pdb_files
.Add (file_name
);
510 asm_files
.Add (file_name
);
513 //FIXME make this parallel
514 foreach (var p
in asm_files
) {
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;
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
)
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
)
559 if (start
.Column
> spStart
.Column
&& start
.Line
== sp
.StartLine
)
562 if (end
.Line
< spEnd
.Line
)
565 if (end
.Column
< spEnd
.Column
&& end
.Line
== spEnd
.Line
)
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
)
576 var src_id
= start
.Id
;
578 var doc
= GetFileById (src_id
);
580 var res
= new List
<SourceLocation
> ();
582 //FIXME we need to write up logging here
583 Console
.WriteLine ($"Could not find document {src_id}");
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
));
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
)
607 //Chrome sends a zero column even if getPossibleBreakpoints say something else
611 if (sp
.StartColumn
> bp
.column
&& sp
.StartLine
== bp
.line
)
614 if (sp
.EndColumn
< bp
.column
&& sp
.EndLine
== bp
.line
)
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
));
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
);
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
);