3 using System
.Threading
.Tasks
;
4 using Newtonsoft
.Json
.Linq
;
6 using System
.Net
.WebSockets
;
7 using System
.Threading
;
10 using System
.Collections
.Generic
;
15 internal class MonoCommands
{
16 public const string GET_CALL_STACK
= "MONO.mono_wasm_get_call_stack()";
17 public const string IS_RUNTIME_READY_VAR
= "MONO.mono_wasm_runtime_is_ready";
18 public const string START_SINGLE_STEPPING
= "MONO.mono_wasm_start_single_stepping({0})";
19 public const string GET_SCOPE_VARIABLES
= "MONO.mono_wasm_get_variables({0}, [ {1} ])";
20 public const string SET_BREAK_POINT
= "MONO.mono_wasm_set_breakpoint(\"{0}\", {1}, {2})";
21 public const string REMOVE_BREAK_POINT
= "MONO.mono_wasm_remove_breakpoint({0})";
22 public const string GET_LOADED_FILES
= "MONO.mono_wasm_get_loaded_files()";
23 public const string CLEAR_ALL_BREAKPOINTS
= "MONO.mono_wasm_clear_all_breakpoints()";
24 public const string GET_OBJECT_PROPERTIES
= "MONO.mono_wasm_get_object_properties({0})";
25 public const string GET_ARRAY_VALUES
= "MONO.mono_wasm_get_array_values({0})";
28 public enum MonoErrorCodes
{
33 internal class MonoConstants
{
34 public const string RUNTIME_IS_READY
= "mono_wasm_runtime_ready";
37 public Frame (MethodInfo method
, SourceLocation location
, int id
)
40 this.Location
= location
;
44 public MethodInfo Method { get; private set; }
45 public SourceLocation Location { get; private set; }
46 public int Id { get; private set; }
51 public SourceLocation Location { get; private set; }
52 public int LocalId { get; private set; }
53 public int RemoteId { get; set; }
54 public BreakPointState State { get; set; }
56 public Breakpoint (SourceLocation loc
, int localId
, BreakPointState state
)
59 this.LocalId
= localId
;
64 enum BreakPointState
{
76 public class MonoProxy
: WsProxy
{
78 List
<Breakpoint
> breakpoints
= new List
<Breakpoint
> ();
79 List
<Frame
> current_callstack
;
81 int local_breakpoint_id
;
85 public MonoProxy () { }
87 protected override async Task
<bool> AcceptEvent (string method
, JObject args
, CancellationToken token
)
90 case "Runtime.executionContextCreated": {
91 var ctx
= args
? ["context"];
92 var aux_data
= ctx
? ["auxData"] as JObject
;
93 if (aux_data
!= null) {
94 var is_default
= aux_data
["isDefault"]?.Value
<bool> ();
95 if (is_default
== true) {
96 var ctx_id
= ctx
["id"].Value
<int> ();
97 await OnDefaultContext (ctx_id
, aux_data
, token
);
102 case "Debugger.paused": {
103 //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack
104 var top_func
= args
? ["callFrames"]? [0]? ["functionName"]?.Value
<string> ();
105 if (top_func
== "mono_wasm_fire_bp" || top_func
== "_mono_wasm_fire_bp") {
106 await OnBreakPointHit (args
, token
);
109 if (top_func
== MonoConstants
.RUNTIME_IS_READY
) {
110 await OnRuntimeReady (token
);
115 case "Debugger.scriptParsed":{
116 if (args
?["url"]?.Value
<string> ()?.StartsWith ("wasm://") == true) {
117 // Console.WriteLine ("ignoring wasm event");
128 protected override async Task
<bool> AcceptCommand (int id
, string method
, JObject args
, CancellationToken token
)
131 case "Debugger.getScriptSource": {
132 var script_id
= args
? ["scriptId"]?.Value
<string> ();
133 if (script_id
.StartsWith ("dotnet://", StringComparison
.InvariantCultureIgnoreCase
)) {
134 await OnGetScriptSource (id
, script_id
, token
);
140 case "Runtime.compileScript": {
141 var exp
= args
? ["expression"]?.Value
<string> ();
142 if (exp
.StartsWith ("//dotnet:", StringComparison
.InvariantCultureIgnoreCase
)) {
143 OnCompileDotnetScript (id
, token
);
149 case "Debugger.getPossibleBreakpoints": {
150 var start
= SourceLocation
.Parse (args
? ["start"] as JObject
);
151 //FIXME support variant where restrictToFunction=true and end is omitted
152 var end
= SourceLocation
.Parse (args
? ["end"] as JObject
);
153 if (start
!= null && end
!= null)
154 return GetPossibleBreakpoints (id
, start
, end
, token
);
158 case "Debugger.setBreakpointByUrl": {
159 Info ($"BP req {args}");
160 var bp_req
= BreakPointRequest
.Parse (args
, store
);
161 if (bp_req
!= null) {
162 await SetBreakPoint (id
, bp_req
, token
);
167 case "Debugger.removeBreakpoint": {
168 return await RemoveBreakpoint (id
, args
, token
);
171 case "Debugger.resume": {
172 await OnResume (token
);
176 case "Debugger.stepInto": {
177 if (this.current_callstack
!= null) {
178 await Step (id
, StepKind
.Into
, token
);
184 case "Debugger.stepOut": {
185 if (this.current_callstack
!= null) {
186 await Step (id
, StepKind
.Out
, token
);
192 case "Debugger.stepOver": {
193 if (this.current_callstack
!= null) {
194 await Step (id
, StepKind
.Over
, token
);
200 case "Runtime.getProperties": {
201 var objId
= args
? ["objectId"]?.Value
<string> ();
202 if (objId
.StartsWith ("dotnet:scope:", StringComparison
.InvariantCulture
)) {
203 await GetScopeProperties (id
, int.Parse (objId
.Substring ("dotnet:scope:".Length
)), token
);
206 if (objId
.StartsWith("dotnet:", StringComparison
.InvariantCulture
))
208 if (objId
.StartsWith("dotnet:object:", StringComparison
.InvariantCulture
))
209 await GetDetails(id
, int.Parse(objId
.Substring("dotnet:object:".Length
)), token
, MonoCommands
.GET_OBJECT_PROPERTIES
);
210 if (objId
.StartsWith("dotnet:array:", StringComparison
.InvariantCulture
))
211 await GetDetails(id
, int.Parse(objId
.Substring("dotnet:array:".Length
)), token
, MonoCommands
.GET_ARRAY_VALUES
);
221 async Task
OnRuntimeReady (CancellationToken token
)
223 Info ("RUNTIME READY, PARTY TIME");
224 await RuntimeReady (token
);
225 await SendCommand ("Debugger.resume", new JObject (), token
);
226 SendEvent ("Mono.runtimeReady", new JObject (), token
);
229 async Task
OnBreakPointHit (JObject args
, CancellationToken token
)
231 //FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
232 var o
= JObject
.FromObject (new {
233 expression
= MonoCommands
.GET_CALL_STACK
,
234 objectGroup
= "mono_debugger",
235 includeCommandLineAPI
= false,
240 var orig_callframes
= args
? ["callFrames"]?.Values
<JObject
> ();
241 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
244 //Give up and send the original call stack
245 SendEvent ("Debugger.paused", args
, token
);
249 //step one, figure out where did we hit
250 var res_value
= res
.Value
? ["result"]? ["value"];
251 if (res_value
== null || res_value
is JValue
) {
252 //Give up and send the original call stack
253 SendEvent ("Debugger.paused", args
, token
);
257 Debug ($"call stack (err is {res.Error} value is:\n{res.Value}");
258 var bp_id
= res_value
? ["breakpoint_id"]?.Value
<int> ();
259 Debug ($"We just hit bp {bp_id}");
260 if (!bp_id
.HasValue
) {
261 //Give up and send the original call stack
262 SendEvent ("Debugger.paused", args
, token
);
265 var bp
= this.breakpoints
.FirstOrDefault (b
=> b
.RemoteId
== bp_id
.Value
);
267 var src
= bp
== null ? null : store
.GetFileById (bp
.Location
.Id
);
269 var callFrames
= new List
<JObject
> ();
270 foreach (var frame
in orig_callframes
) {
271 var function_name
= frame
["functionName"]?.Value
<string> ();
272 var url
= frame
["url"]?.Value
<string> ();
273 if ("mono_wasm_fire_bp" == function_name
|| "_mono_wasm_fire_bp" == function_name
) {
274 var frames
= new List
<Frame
> ();
276 var the_mono_frames
= res
.Value
? ["result"]? ["value"]? ["frames"]?.Values
<JObject
> ();
278 foreach (var mono_frame
in the_mono_frames
) {
279 var il_pos
= mono_frame
["il_pos"].Value
<int> ();
280 var method_token
= mono_frame
["method_token"].Value
<int> ();
281 var assembly_name
= mono_frame
["assembly_name"].Value
<string> ();
283 var asm
= store
.GetAssemblyByName (assembly_name
);
285 Info ($"Unable to find assembly: {assembly_name}");
289 var method
= asm
.GetMethodByToken (method_token
);
291 if (method
== null) {
292 Info ($"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}");
296 var location
= method
?.GetLocationByIl (il_pos
);
298 // When hitting a breakpoint on the "IncrementCount" method in the standard
299 // Blazor project template, one of the stack frames is inside mscorlib.dll
300 // and we get location==null for it. It will trigger a NullReferenceException
301 // if we don't skip over that stack frame.
302 if (location
== null) {
306 Info ($"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
307 Info ($"\tmethod {method.Name} location: {location}");
308 frames
.Add (new Frame (method
, location
, frame_id
));
310 callFrames
.Add (JObject
.FromObject (new {
311 functionName
= method
.Name
,
312 callFrameId
= $"dotnet:scope:{frame_id}",
313 functionLocation
= method
.StartLocation
.ToJObject (),
315 location
= location
.ToJObject (),
317 url
= store
.ToUrl (location
),
319 scopeChain
= new [] {
324 className
= "Object",
325 description
= "Object",
326 objectId
= $"dotnet:scope:{frame_id}",
329 startLocation
= method
.StartLocation
.ToJObject (),
330 endLocation
= method
.EndLocation
.ToJObject (),
335 this.current_callstack
= frames
;
338 } else if (!(function_name
.StartsWith ("wasm-function", StringComparison
.InvariantCulture
)
339 || url
.StartsWith ("wasm://wasm/", StringComparison
.InvariantCulture
))) {
340 callFrames
.Add (frame
);
344 var bp_list
= new string [bp
== null ? 0 : 1];
346 bp_list
[0] = $"dotnet:{bp.LocalId}";
348 o
= JObject
.FromObject (new {
349 callFrames
= callFrames
,
350 reason
= "other", //other means breakpoint
351 hitBreakpoints
= bp_list
,
354 SendEvent ("Debugger.paused", o
, token
);
357 async Task
OnDefaultContext (int ctx_id
, JObject aux_data
, CancellationToken token
)
359 Debug ("Default context created, clearing state and sending events");
362 foreach (var b
in this.breakpoints
){
363 b
.State
= BreakPointState
.Pending
;
365 this.runtime_ready
= false;
367 var o
= JObject
.FromObject (new {
368 expression
= MonoCommands
.IS_RUNTIME_READY_VAR
,
369 objectGroup
= "mono_debugger",
370 includeCommandLineAPI
= false,
374 this.ctx_id
= ctx_id
;
375 this.aux_ctx_data
= aux_data
;
377 Debug ("checking if the runtime is ready");
378 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
379 var is_ready
= res
.Value
? ["result"]? ["value"]?.Value
<bool> ();
380 //Debug ($"\t{is_ready}");
381 if (is_ready
.HasValue
&& is_ready
.Value
== true) {
382 Debug ("RUNTIME LOOK READY. GO TIME!");
383 await OnRuntimeReady (token
);
388 async Task
OnResume (CancellationToken token
)
391 this.current_callstack
= null;
392 await Task
.CompletedTask
;
395 async Task
Step (int msg_id
, StepKind kind
, CancellationToken token
)
398 var o
= JObject
.FromObject (new {
399 expression
= string.Format (MonoCommands
.START_SINGLE_STEPPING
, (int)kind
),
400 objectGroup
= "mono_debugger",
401 includeCommandLineAPI
= false,
403 returnByValue
= true,
406 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
408 SendResponse (msg_id
, Result
.Ok (new JObject ()), token
);
410 this.current_callstack
= null;
412 await SendCommand ("Debugger.resume", new JObject (), token
);
415 async Task
GetDetails(int msg_id
, int object_id
, CancellationToken token
, string command
)
417 var o
= JObject
.FromObject(new
419 expression
= string.Format(command
, object_id
),
420 objectGroup
= "mono_debugger",
421 includeCommandLineAPI
= false,
423 returnByValue
= true,
426 var res
= await SendCommand("Runtime.evaluate", o
, token
);
428 //if we fail we just buble that to the IDE (and let it panic over it)
431 SendResponse(msg_id
, res
, token
);
436 var values
= res
.Value
?["result"]?["value"]?.Values
<JObject
>().ToArray() ?? Array
.Empty
<JObject
>();
437 var var_list
= new List
<JObject
>();
439 // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
440 // results in a "Memory access out of bounds", causing 'values' to be null,
441 // so skip returning variable values in that case.
442 for (int i
= 0; i
< values
.Length
; i
+=2)
444 string fieldName
= (string)values
[i
]["name"];
445 if (fieldName
.Contains("k__BackingField")){
446 fieldName
= fieldName
.Replace("k__BackingField", "");
447 fieldName
= fieldName
.Replace("<", "");
448 fieldName
= fieldName
.Replace(">", "");
450 var value = values
[i
+ 1]? ["value"];
451 if (((string)value ["description"]) == null)
452 value ["description"] = value ["value"]?.ToString ();
454 var_list
.Add(JObject
.FromObject(new {
460 o
= JObject
.FromObject(new
465 Debug ($"failed to parse {res.Value}");
467 SendResponse(msg_id
, Result
.Ok(o
), token
);
471 async Task
GetScopeProperties (int msg_id
, int scope_id
, CancellationToken token
)
473 var scope
= this.current_callstack
.FirstOrDefault (s
=> s
.Id
== scope_id
);
474 var vars
= scope
.Method
.GetLiveVarsAt (scope
.Location
.CliLocation
.Offset
);
477 var var_ids
= string.Join (",", vars
.Select (v
=> v
.Index
));
479 var o
= JObject
.FromObject (new {
480 expression
= string.Format (MonoCommands
.GET_SCOPE_VARIABLES
, scope
.Id
, var_ids
),
481 objectGroup
= "mono_debugger",
482 includeCommandLineAPI
= false,
484 returnByValue
= true,
487 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
489 //if we fail we just buble that to the IDE (and let it panic over it)
491 SendResponse (msg_id
, res
, token
);
496 var values
= res
.Value
? ["result"]? ["value"]?.Values
<JObject
> ().ToArray ();
498 var var_list
= new List
<JObject
> ();
500 // Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
501 // results in a "Memory access out of bounds", causing 'values' to be null,
502 // so skip returning variable values in that case.
503 while (values
!= null && i
< vars
.Length
&& i
< values
.Length
) {
504 var value = values
[i
] ["value"];
505 if (((string)value ["description"]) == null)
506 value ["description"] = value ["value"]?.ToString ();
508 var_list
.Add (JObject
.FromObject (new {
509 name
= vars
[i
].Name
,
514 //Async methods are special in the way that local variables can be lifted to generated class fields
515 //value of "this" comes here either
516 while (i
< values
.Length
) {
517 String name
= values
[i
] ["name"].ToString ();
519 if (name
.IndexOf (">", StringComparison
.Ordinal
) > 0)
520 name
= name
.Substring (1, name
.IndexOf (">", StringComparison
.Ordinal
) - 1);
522 var value = values
[i
+ 1] ["value"];
523 if (((string)value ["description"]) == null)
524 value ["description"] = value ["value"]?.ToString ();
526 var_list
.Add (JObject
.FromObject (new {
532 o
= JObject
.FromObject (new {
535 SendResponse (msg_id
, Result
.Ok (o
), token
);
538 SendResponse (msg_id
, res
, token
);
542 async Task
<Result
> EnableBreakPoint (Breakpoint bp
, CancellationToken token
)
544 var asm_name
= bp
.Location
.CliLocation
.Method
.Assembly
.Name
;
545 var method_token
= bp
.Location
.CliLocation
.Method
.Token
;
546 var il_offset
= bp
.Location
.CliLocation
.Offset
;
548 var o
= JObject
.FromObject (new {
549 expression
= string.Format (MonoCommands
.SET_BREAK_POINT
, asm_name
, method_token
, il_offset
),
550 objectGroup
= "mono_debugger",
551 includeCommandLineAPI
= false,
553 returnByValue
= true,
556 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
557 var ret_code
= res
.Value
? ["result"]? ["value"]?.Value
<int> ();
559 if (ret_code
.HasValue
) {
560 bp
.RemoteId
= ret_code
.Value
;
561 bp
.State
= BreakPointState
.Active
;
562 //Debug ($"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
568 async Task
RuntimeReady (CancellationToken token
)
571 var o
= JObject
.FromObject (new {
572 expression
= MonoCommands
.GET_LOADED_FILES
,
573 objectGroup
= "mono_debugger",
574 includeCommandLineAPI
= false,
576 returnByValue
= true,
578 var loaded_pdbs
= await SendCommand ("Runtime.evaluate", o
, token
);
579 var the_value
= loaded_pdbs
.Value
? ["result"]? ["value"];
580 var the_pdbs
= the_value
?.ToObject
<string[]> ();
581 this.store
= new DebugStore (the_pdbs
);
583 foreach (var s
in store
.AllSources ()) {
584 var ok
= JObject
.FromObject (new {
585 scriptId
= s
.SourceId
.ToString (),
587 executionContextId
= this.ctx_id
,
588 hash
= s
.DocHashCode
,
589 executionContextAuxData
= this.aux_ctx_data
,
590 dotNetUrl
= s
.DotNetUrl
592 //Debug ($"\tsending {s.Url}");
593 SendEvent ("Debugger.scriptParsed", ok
, token
);
596 o
= JObject
.FromObject (new {
597 expression
= MonoCommands
.CLEAR_ALL_BREAKPOINTS
,
598 objectGroup
= "mono_debugger",
599 includeCommandLineAPI
= false,
601 returnByValue
= true,
604 var clear_result
= await SendCommand ("Runtime.evaluate", o
, token
);
605 if (clear_result
.IsErr
) {
606 Debug ($"Failed to clear breakpoints due to {clear_result}");
610 runtime_ready
= true;
612 foreach (var bp
in breakpoints
) {
613 if (bp
.State
!= BreakPointState
.Pending
)
615 var res
= await EnableBreakPoint (bp
, token
);
616 var ret_code
= res
.Value
? ["result"]? ["value"]?.Value
<int> ();
618 //if we fail we just buble that to the IDE (and let it panic over it)
619 if (!ret_code
.HasValue
) {
620 //FIXME figure out how to inform the IDE of that.
621 Info ($"FAILED TO ENABLE BP {bp.LocalId}");
622 bp
.State
= BreakPointState
.Disabled
;
627 async Task
<bool> RemoveBreakpoint(int msg_id
, JObject args
, CancellationToken token
) {
628 var bpid
= args
? ["breakpointId"]?.Value
<string> ();
629 if (bpid
?.StartsWith ("dotnet:") != true)
632 var the_id
= int.Parse (bpid
.Substring ("dotnet:".Length
));
634 var bp
= breakpoints
.FirstOrDefault (b
=> b
.LocalId
== the_id
);
636 Info ($"Could not find dotnet bp with id {the_id}");
640 breakpoints
.Remove (bp
);
641 //FIXME verify result (and log?)
642 var res
= await RemoveBreakPoint (bp
, token
);
648 async Task
<Result
> RemoveBreakPoint (Breakpoint bp
, CancellationToken token
)
650 var o
= JObject
.FromObject (new {
651 expression
= string.Format (MonoCommands
.REMOVE_BREAK_POINT
, bp
.RemoteId
),
652 objectGroup
= "mono_debugger",
653 includeCommandLineAPI
= false,
655 returnByValue
= true,
658 var res
= await SendCommand ("Runtime.evaluate", o
, token
);
659 var ret_code
= res
.Value
? ["result"]? ["value"]?.Value
<int> ();
661 if (ret_code
.HasValue
) {
663 bp
.State
= BreakPointState
.Disabled
;
669 async Task
SetBreakPoint (int msg_id
, BreakPointRequest req
, CancellationToken token
)
671 var bp_loc
= store
.FindBestBreakpoint (req
);
672 Info ($"BP request for '{req}' runtime ready {runtime_ready} location '{bp_loc}'");
673 if (bp_loc
== null) {
675 Info ($"Could not resolve breakpoint request: {req}");
676 SendResponse (msg_id
, Result
.Err(JObject
.FromObject (new {
677 code
= (int)MonoErrorCodes
.BpNotFound
,
678 message
= $"C# Breakpoint at {req} not found."
683 Breakpoint bp
= null;
684 if (!runtime_ready
) {
685 bp
= new Breakpoint (bp_loc
, local_breakpoint_id
++, BreakPointState
.Pending
);
687 bp
= new Breakpoint (bp_loc
, local_breakpoint_id
++, BreakPointState
.Disabled
);
689 var res
= await EnableBreakPoint (bp
, token
);
690 var ret_code
= res
.Value
? ["result"]? ["value"]?.Value
<int> ();
692 //if we fail we just buble that to the IDE (and let it panic over it)
693 if (!ret_code
.HasValue
) {
694 SendResponse (msg_id
, res
, token
);
699 var locations
= new List
<JObject
> ();
701 locations
.Add (JObject
.FromObject (new {
702 scriptId
= bp_loc
.Id
.ToString (),
703 lineNumber
= bp_loc
.Line
,
704 columnNumber
= bp_loc
.Column
707 breakpoints
.Add (bp
);
709 var ok
= JObject
.FromObject (new {
710 breakpointId
= $"dotnet:{bp.LocalId}",
711 locations
= locations
,
714 SendResponse (msg_id
, Result
.Ok (ok
), token
);
717 bool GetPossibleBreakpoints (int msg_id
, SourceLocation start
, SourceLocation end
, CancellationToken token
)
719 var bps
= store
.FindPossibleBreakpoints (start
, end
);
723 var loc
= new List
<JObject
> ();
724 foreach (var b
in bps
) {
725 loc
.Add (b
.ToJObject ());
728 var o
= JObject
.FromObject (new {
732 SendResponse (msg_id
, Result
.Ok (o
), token
);
737 void OnCompileDotnetScript (int msg_id
, CancellationToken token
)
739 var o
= JObject
.FromObject (new { }
);
741 SendResponse (msg_id
, Result
.Ok (o
), token
);
745 async Task
OnGetScriptSource (int msg_id
, string script_id
, CancellationToken token
)
747 var id
= new SourceId (script_id
);
748 var src_file
= store
.GetFileById (id
);
750 var res
= new StringWriter ();
751 //res.WriteLine ($"//{id}");
754 var uri
= new Uri (src_file
.Url
);
755 if (uri
.IsFile
&& File
.Exists(uri
.LocalPath
)) {
756 using (var f
= new StreamReader (File
.Open (src_file
.SourceUri
.LocalPath
, FileMode
.Open
))) {
757 await res
.WriteAsync (await f
.ReadToEndAsync ());
760 var o
= JObject
.FromObject (new {
761 scriptSource
= res
.ToString ()
764 SendResponse (msg_id
, Result
.Ok (o
), token
);
765 } else if(src_file
.SourceLinkUri
!= null) {
766 var doc
= await new WebClient ().DownloadStringTaskAsync (src_file
.SourceLinkUri
);
767 await res
.WriteAsync (doc
);
769 var o
= JObject
.FromObject (new {
770 scriptSource
= res
.ToString ()
773 SendResponse (msg_id
, Result
.Ok (o
), token
);
775 var o
= JObject
.FromObject (new {
776 scriptSource
= $"// Unable to find document {src_file.SourceUri}"
779 SendResponse (msg_id
, Result
.Ok (o
), token
);
781 } catch (Exception e
) {
782 var o
= JObject
.FromObject (new {
783 scriptSource
= $"// Unable to read document ({e.Message})\n" +
784 $"Local path: {src_file?.SourceUri}\n" +
785 $"SourceLink path: {src_file?.SourceLinkUri}\n"
788 SendResponse (msg_id
, Result
.Ok (o
), token
);