Merge pull request #41 from wintersteiger/christoph/more-error-handlers

Add more error handlers and exception catchers
This commit is contained in:
Simon Cruanes 2024-07-31 12:34:30 -04:00 committed by GitHub
commit f09e36523f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 147 additions and 61 deletions

View file

@ -10,7 +10,9 @@ let ( let* ) x f = f x
let ( and+ ) a b = a, b let ( and+ ) a b = a, b
let return x = x let return x = x
let failwith = failwith let failwith = failwith
let fail = raise
let fail = Printexc.raise_with_backtrace
let stdin = fun () -> stdin let stdin = fun () -> stdin
let stdout = fun () -> stdout let stdout = fun () -> stdout
@ -25,7 +27,12 @@ let default_spawn f =
in in
ignore (Thread.create run ()) ignore (Thread.create run ())
let catch f g = try f () with e -> g e let catch f g =
try f ()
with e ->
let bt = Printexc.get_raw_backtrace () in
g e bt
let n_bytes_written = Atomic.make 0 let n_bytes_written = Atomic.make 0
let n_bytes_read = Atomic.make 0 let n_bytes_read = Atomic.make 0

View file

@ -1,3 +1,4 @@
module Trace = Trace_core module Trace = Trace_core
let ( let@ ) = ( @@ ) let ( let@ ) = ( @@ )
let spf = Printf.sprintf

View file

@ -14,7 +14,10 @@ module IO_eio :
let return x = x let return x = x
let failwith = failwith let failwith = failwith
let fail = raise let fail = raise
let catch f handler = try f () with exn -> handler exn
let catch f handler = try f () with exn ->
let bt = Printexc.get_raw_backtrace () in
handler exn bt
let stdin env = Eio.Buf_read.of_flow ~max_size:1_000_000 (Eio.Stdenv.stdin env) let stdin env = Eio.Buf_read.of_flow ~max_size:1_000_000 (Eio.Stdenv.stdin env)
let stdout = Eio.Stdenv.stdout let stdout = Eio.Stdenv.stdout

View file

@ -42,8 +42,6 @@ module Make (IO : IO) : S with module IO = IO = struct
type json = J.t type json = J.t
let spf = Printf.sprintf
module ErrorCode = Jsonrpc.Response.Error.Code module ErrorCode = Jsonrpc.Response.Error.Code
(* (*
module Err = struct module Err = struct
@ -138,7 +136,7 @@ module Make (IO : IO) : S with module IO = IO = struct
(fun () -> (fun () ->
let+ x = f () in let+ x = f () in
Ok x) Ok x)
(fun e -> IO.return (Error e)) (fun e bt -> IO.return (Error (e, bt)))
(** Sends a server notification to the LSP client. *) (** Sends a server notification to the LSP client. *)
let send_server_notification (self : t) (n : Lsp.Server_notification.t) : let send_server_notification (self : t) (n : Lsp.Server_notification.t) :
@ -166,10 +164,15 @@ module Make (IO : IO) : S with module IO = IO = struct
(** Wraps some action and, in case the [IO.t] request has failed, logs the (** Wraps some action and, in case the [IO.t] request has failed, logs the
failure to the LSP client. *) failure to the LSP client. *)
let with_error_handler (self : t) (action : unit -> unit IO.t) : unit IO.t = let with_error_handler (self : t) (action : unit -> unit IO.t) : unit IO.t =
IO.catch action (fun e -> IO.catch action (fun exn bt ->
let message =
spf "LSP handler failed with %s\n%s" (Printexc.to_string exn)
(Printexc.raw_backtrace_to_string bt)
in
Log.err (fun k -> k "%s" message);
let msg = let msg =
Lsp.Types.LogMessageParams.create ~type_:Lsp.Types.MessageType.Error Lsp.Types.LogMessageParams.create ~type_:Lsp.Types.MessageType.Error
~message:(Printexc.to_string e) ~message
in in
let msg = let msg =
Lsp.Server_notification.LogMessage msg Lsp.Server_notification.LogMessage msg
@ -191,11 +194,12 @@ module Make (IO : IO) : S with module IO = IO = struct
let handle_request (self : t) (r : Jsonrpc.Request.t) : unit IO.t = let handle_request (self : t) (r : Jsonrpc.Request.t) : unit IO.t =
let protect ~id f = let protect ~id f =
IO.catch f (fun e -> IO.catch f (fun e bt ->
let message = let message =
spf "%s\n%s" (Printexc.to_string e) (Printexc.get_backtrace ()) spf "%s\n%s" (Printexc.to_string e)
(Printexc.raw_backtrace_to_string bt)
in in
Log.err (fun k -> k "error: %s" message); Log.err (fun k -> k "error in request handler: %s" message);
let r = let r =
Jsonrpc.Response.error id Jsonrpc.Response.error id
(Jsonrpc.Response.Error.make (Jsonrpc.Response.Error.make
@ -215,13 +219,23 @@ module Make (IO : IO) : S with module IO = IO = struct
~notify_back:(send_server_notification self) ~notify_back:(send_server_notification self)
~server_request:(server_request self) ~server_request:(server_request self)
in in
let response =
match reply with
| Ok reply ->
let reply_json = Lsp.Client_request.yojson_of_result r reply in let reply_json = Lsp.Client_request.yojson_of_result r reply in
let response = Jsonrpc.Response.ok id reply_json in Jsonrpc.Response.ok id reply_json
| Error message ->
Jsonrpc.Response.error id
(Jsonrpc.Response.Error.make
~code:Jsonrpc.Response.Error.Code.InternalError ~message ())
in
send_response self response send_response self response
| Error e -> IO.failwith (spf "cannot decode request: %s" e)) | Error e -> IO.failwith (spf "cannot decode request: %s" e))
(fun e -> (fun e bt ->
let message = let message =
spf "%s\n%s" (Printexc.to_string e) (Printexc.get_backtrace ()) spf "%s\n%s" (Printexc.to_string e)
(Printexc.raw_backtrace_to_string bt)
in in
Log.err (fun k -> k "error: %s" message); Log.err (fun k -> k "error: %s" message);
let r = let r =
@ -273,7 +287,8 @@ module Make (IO : IO) : S with module IO = IO = struct
| _ -> j | _ -> j
(* read a full message *) (* read a full message *)
let read_msg (self : t) : (Jsonrpc.Packet.t, exn) result IO.t = let read_msg (self : t) :
(Jsonrpc.Packet.t, exn * Printexc.raw_backtrace) result IO.t =
let rec read_headers acc = let rec read_headers acc =
let*? line = try_ @@ fun () -> IO.read_line self.ic in let*? line = try_ @@ fun () -> IO.read_line self.ic in
match String.trim line with match String.trim line with
@ -292,8 +307,9 @@ module Make (IO : IO) : S with module IO = IO = struct
with with
| pair -> read_headers (pair :: acc) | pair -> read_headers (pair :: acc)
| exception _ -> | exception _ ->
IO.return let bt = Printexc.get_raw_backtrace () in
(Error (E (ErrorCode.ParseError, spf "invalid header: %S" line)))) let exn = E (ErrorCode.ParseError, spf "invalid header: %S" line) in
IO.return (Error (exn, bt)))
in in
let*? headers = read_headers [] in let*? headers = read_headers [] in
Log.debug (fun k -> Log.debug (fun k ->
@ -323,15 +339,21 @@ module Make (IO : IO) : S with module IO = IO = struct
(match Jsonrpc.Packet.t_of_yojson @@ fix_null_in_params j with (match Jsonrpc.Packet.t_of_yojson @@ fix_null_in_params j with
| m -> IO.return @@ Ok m | m -> IO.return @@ Ok m
| exception exn -> | exception exn ->
let bt = Printexc.get_raw_backtrace () in
Log.err (fun k -> Log.err (fun k ->
k "cannot decode json message: %s" (Printexc.to_string exn)); k "cannot decode json message: %s\n%s" (Printexc.to_string exn)
IO.return (Error (E (ErrorCode.ParseError, "cannot decode json")))) (Printexc.raw_backtrace_to_string bt));
let exn = E (ErrorCode.ParseError, "cannot decode json") in
IO.return (Error (exn, bt)))
| exception _ -> | exception _ ->
let bt = Printexc.get_raw_backtrace () in
IO.return IO.return
@@ Error (E (ErrorCode.ParseError, "missing content-length' header")) @@ Error (E (ErrorCode.ParseError, "missing content-length' header"), bt)
) else ) else (
let bt = Printexc.get_callstack 10 in
IO.return IO.return
@@ Error (E (ErrorCode.InvalidRequest, "content-type must be 'utf-8'")) @@ Error (E (ErrorCode.InvalidRequest, "content-type must be 'utf-8'"), bt)
)
let send_server_request (self : t) (req : 'from_server Lsp.Server_request.t) let send_server_request (self : t) (req : 'from_server Lsp.Server_request.t)
(cb : ('from_server, Jsonrpc.Response.Error.t) result -> unit IO.t) : (cb : ('from_server, Jsonrpc.Response.Error.t) result -> unit IO.t) :
@ -369,7 +391,7 @@ module Make (IO : IO) : S with module IO = IO = struct
| Ok r -> | Ok r ->
let* () = process_msg r in let* () = process_msg r in
loop () loop ()
| Error e -> IO.fail e | Error (e, bt) -> IO.fail e bt
in in
loop () loop ()
end end

View file

@ -29,8 +29,12 @@ module IO_lwt :
let write = Lwt_io.write_from_exactly let write = Lwt_io.write_from_exactly
let read = Lwt_io.read_into_exactly let read = Lwt_io.read_into_exactly
let read_line = Lwt_io.read_line let read_line = Lwt_io.read_line
let catch = Lwt.catch
let fail = Lwt.fail let catch f g =
let bt = Printexc.get_callstack 10 in
Lwt.catch f (fun exn -> g exn bt)
let fail e _bt = Lwt.fail e
end end
(** Spawn function. (** Spawn function.

View file

@ -60,7 +60,7 @@ module Make (IO : IO) = struct
server_request:send_request -> server_request:send_request ->
id:Req_id.t -> id:Req_id.t ->
'a Lsp.Client_request.t -> 'a Lsp.Client_request.t ->
'a IO.t ('a, string) result IO.t
(** Method called to handle client requests. (** Method called to handle client requests.
@param notify_back an object used to reply to the client, send progress @param notify_back an object used to reply to the client, send progress
messages, diagnostics, etc. messages, diagnostics, etc.
@ -76,7 +76,14 @@ module Make (IO : IO) = struct
end end
let async (self : #base_server) f : unit IO.t = let async (self : #base_server) f : unit IO.t =
self#spawn_query_handler f; self#spawn_query_handler (fun () ->
IO.catch f (fun exn bt ->
let msg =
spf "LSP async notification handler failed with %s\n%s"
(Printexc.to_string exn)
(Printexc.raw_backtrace_to_string bt)
in
IO.return @@ Log.err (fun k -> k "%s" msg)));
IO.return () IO.return ()
(** A wrapper to more easily reply to notifications *) (** A wrapper to more easily reply to notifications *)
@ -150,6 +157,11 @@ module Make (IO : IO) = struct
} }
(** Current state of a document. *) (** Current state of a document. *)
let[@inline] lift_ok x =
let open IO in
let+ x = x in
Ok x
(** An easily overloadable class. Pick the methods you want to support. (** An easily overloadable class. Pick the methods you want to support.
The user must provide at least the callbacks for document lifecycle: The user must provide at least the callbacks for document lifecycle:
open, close, update. The most basic LSP server should check documents open, close, update. The most basic LSP server should check documents
@ -337,10 +349,24 @@ module Make (IO : IO) = struct
server_request:_ -> server_request:_ ->
id:Req_id.t -> id:Req_id.t ->
r Lsp.Client_request.t -> r Lsp.Client_request.t ->
r IO.t = (r, string) result IO.t =
fun ~notify_back ~server_request ~id (r : _ Lsp.Client_request.t) -> fun ~notify_back ~server_request ~id (r : _ Lsp.Client_request.t) ->
Trace.with_span ~__FILE__ ~__LINE__ "linol.on-request" Trace.with_span ~__FILE__ ~__LINE__ "linol.on-request"
@@ fun _sp : r IO.t -> @@ fun _sp : (r, string) result IO.t ->
(* handler to catch all errors *)
let try_catch : (unit -> (r, _) result IO.t) -> (r, _) result IO.t =
fun f ->
IO.catch f (fun exn bt ->
let msg =
spf "LSP request handler failed with %s\n%s"
(Printexc.to_string exn)
(Printexc.raw_backtrace_to_string bt)
in
Log.err (fun k -> k "%s" msg);
IO.return @@ Error msg)
in
try_catch @@ fun () ->
Log.debug (fun k -> Log.debug (fun k ->
k "handle request[id=%s] <opaque>" (Req_id.to_string id)); k "handle request[id=%s] <opaque>" (Req_id.to_string id));
@ -353,7 +379,7 @@ module Make (IO : IO) = struct
~server_request () ~server_request ()
in in
status <- `ReceivedShutdown; status <- `ReceivedShutdown;
self#on_req_shutdown ~notify_back ~id lift_ok @@ self#on_req_shutdown ~notify_back ~id
| Lsp.Client_request.Initialize i -> | Lsp.Client_request.Initialize i ->
Log.debug (fun k -> k "req: initialize"); Log.debug (fun k -> k "req: initialize");
let notify_back = let notify_back =
@ -361,21 +387,22 @@ module Make (IO : IO) = struct
~partialResultToken:None ~workDoneToken:i.workDoneToken ~partialResultToken:None ~workDoneToken:i.workDoneToken
~notify_back ~server_request () ~notify_back ~server_request ()
in in
self#on_req_initialize ~notify_back i lift_ok @@ self#on_req_initialize ~notify_back i
| Lsp.Client_request.TextDocumentHover | Lsp.Client_request.TextDocumentHover
{ textDocument; position; workDoneToken } -> { textDocument; position; workDoneToken } ->
let uri = textDocument.uri in let uri = textDocument.uri in
Log.debug (fun k -> k "req: hover '%s'" (DocumentUri.to_path uri)); Log.debug (fun k -> k "req: hover '%s'" (DocumentUri.to_path uri));
(match Hashtbl.find_opt docs uri with (match Hashtbl.find_opt docs uri with
| None -> IO.return None | None -> IO.return @@ Ok None
| Some doc_st -> | Some doc_st ->
let notify_back = let notify_back =
new notify_back new notify_back
~workDoneToken ~partialResultToken:None ~uri ~notify_back ~workDoneToken ~partialResultToken:None ~uri ~notify_back
~server_request () ~server_request ()
in in
self#on_req_hover ~notify_back ~id ~uri ~pos:position lift_ok
@@ self#on_req_hover ~notify_back ~id ~uri ~pos:position
~workDoneToken doc_st) ~workDoneToken doc_st)
| Lsp.Client_request.TextDocumentCompletion | Lsp.Client_request.TextDocumentCompletion
{ {
@ -389,14 +416,15 @@ module Make (IO : IO) = struct
Log.debug (fun k -> Log.debug (fun k ->
k "req: complete '%s'" (DocumentUri.to_path uri)); k "req: complete '%s'" (DocumentUri.to_path uri));
(match Hashtbl.find_opt docs uri with (match Hashtbl.find_opt docs uri with
| None -> IO.return None | None -> IO.return @@ Ok None
| Some doc_st -> | Some doc_st ->
let notify_back = let notify_back =
new notify_back new notify_back
~partialResultToken ~workDoneToken ~uri ~notify_back ~partialResultToken ~workDoneToken ~uri ~notify_back
~server_request () ~server_request ()
in in
self#on_req_completion ~notify_back ~id ~uri ~workDoneToken lift_ok
@@ self#on_req_completion ~notify_back ~id ~uri ~workDoneToken
~partialResultToken ~pos:position ~ctx:context doc_st) ~partialResultToken ~pos:position ~ctx:context doc_st)
| Lsp.Client_request.TextDocumentDefinition | Lsp.Client_request.TextDocumentDefinition
{ textDocument; position; workDoneToken; partialResultToken } -> { textDocument; position; workDoneToken; partialResultToken } ->
@ -410,9 +438,10 @@ module Make (IO : IO) = struct
in in
(match Hashtbl.find_opt docs uri with (match Hashtbl.find_opt docs uri with
| None -> IO.return None | None -> IO.return @@ Ok None
| Some doc_st -> | Some doc_st ->
self#on_req_definition ~notify_back ~id ~workDoneToken lift_ok
@@ self#on_req_definition ~notify_back ~id ~workDoneToken
~partialResultToken ~uri ~pos:position doc_st) ~partialResultToken ~uri ~pos:position doc_st)
| Lsp.Client_request.TextDocumentCodeLens | Lsp.Client_request.TextDocumentCodeLens
{ textDocument; workDoneToken; partialResultToken } -> { textDocument; workDoneToken; partialResultToken } ->
@ -426,9 +455,10 @@ module Make (IO : IO) = struct
in in
(match Hashtbl.find_opt docs uri with (match Hashtbl.find_opt docs uri with
| None -> IO.return [] | None -> IO.return @@ Ok []
| Some doc_st -> | Some doc_st ->
self#on_req_code_lens ~notify_back ~id ~uri ~workDoneToken lift_ok
@@ self#on_req_code_lens ~notify_back ~id ~uri ~workDoneToken
~partialResultToken doc_st) ~partialResultToken doc_st)
| Lsp.Client_request.TextDocumentCodeLensResolve cl -> | Lsp.Client_request.TextDocumentCodeLensResolve cl ->
Log.debug (fun k -> k "req: codelens resolve"); Log.debug (fun k -> k "req: codelens resolve");
@ -437,7 +467,7 @@ module Make (IO : IO) = struct
~workDoneToken:None ~partialResultToken:None ~notify_back ~workDoneToken:None ~partialResultToken:None ~notify_back
~server_request () ~server_request ()
in in
self#on_req_code_lens_resolve ~notify_back ~id cl lift_ok @@ self#on_req_code_lens_resolve ~notify_back ~id cl
| Lsp.Client_request.ExecuteCommand | Lsp.Client_request.ExecuteCommand
{ command; arguments; workDoneToken } -> { command; arguments; workDoneToken } ->
Log.debug (fun k -> k "req: execute command '%s'" command); Log.debug (fun k -> k "req: execute command '%s'" command);
@ -446,8 +476,9 @@ module Make (IO : IO) = struct
~workDoneToken ~partialResultToken:None ~notify_back ~workDoneToken ~partialResultToken:None ~notify_back
~server_request () ~server_request ()
in in
self#on_req_execute_command ~notify_back ~id ~workDoneToken command lift_ok
arguments @@ self#on_req_execute_command ~notify_back ~id ~workDoneToken
command arguments
| Lsp.Client_request.DocumentSymbol | Lsp.Client_request.DocumentSymbol
{ textDocument = d; workDoneToken; partialResultToken } -> { textDocument = d; workDoneToken; partialResultToken } ->
let notify_back = let notify_back =
@ -455,7 +486,8 @@ module Make (IO : IO) = struct
~workDoneToken ~partialResultToken ~notify_back ~server_request ~workDoneToken ~partialResultToken ~notify_back ~server_request
() ()
in in
self#on_req_symbol ~notify_back ~id ~uri:d.uri ~workDoneToken lift_ok
@@ self#on_req_symbol ~notify_back ~id ~uri:d.uri ~workDoneToken
~partialResultToken () ~partialResultToken ()
| Lsp.Client_request.CodeAction a -> | Lsp.Client_request.CodeAction a ->
let notify_back = let notify_back =
@ -464,14 +496,15 @@ module Make (IO : IO) = struct
~partialResultToken:a.partialResultToken ~notify_back ~partialResultToken:a.partialResultToken ~notify_back
~server_request () ~server_request ()
in in
self#on_req_code_action ~notify_back ~id a lift_ok @@ self#on_req_code_action ~notify_back ~id a
| Lsp.Client_request.InlayHint p -> | Lsp.Client_request.InlayHint p ->
let notify_back : notify_back = let notify_back : notify_back =
new notify_back new notify_back
~workDoneToken:p.workDoneToken ~partialResultToken:None ~workDoneToken:p.workDoneToken ~partialResultToken:None
~notify_back ~server_request () ~notify_back ~server_request ()
in in
self#on_req_inlay_hint ~notify_back ~id ~uri:p.textDocument.uri lift_ok
@@ self#on_req_inlay_hint ~notify_back ~id ~uri:p.textDocument.uri
~range:p.range () ~range:p.range ()
| Lsp.Client_request.CodeActionResolve _ | Lsp.Client_request.CodeActionResolve _
| Lsp.Client_request.LinkedEditingRange _ | Lsp.Client_request.LinkedEditingRange _
@ -512,14 +545,15 @@ module Make (IO : IO) = struct
~workDoneToken:None ~partialResultToken:None ~notify_back ~workDoneToken:None ~partialResultToken:None ~notify_back
~server_request () ~server_request ()
in in
self#on_request_unhandled ~notify_back ~id r lift_ok @@ self#on_request_unhandled ~notify_back ~id r
| Lsp.Client_request.UnknownRequest r -> | Lsp.Client_request.UnknownRequest r ->
let notify_back = let notify_back =
new notify_back new notify_back
~workDoneToken:None ~partialResultToken:None ~notify_back ~workDoneToken:None ~partialResultToken:None ~notify_back
~server_request () ~server_request ()
in in
self#on_unknown_request ~notify_back ~server_request ~id r.meth lift_ok
@@ self#on_unknown_request ~notify_back ~server_request ~id r.meth
r.params r.params
method virtual on_notif_doc_did_open method virtual on_notif_doc_did_open
@ -559,6 +593,21 @@ module Make (IO : IO) = struct
let@ _sp = let@ _sp =
Trace.with_span ~__FILE__ ~__LINE__ "linol.on-notification" Trace.with_span ~__FILE__ ~__LINE__ "linol.on-notification"
in in
(* handler to catch all errors *)
let try_catch : (unit -> unit IO.t) -> unit IO.t =
fun f ->
IO.catch f (fun exn bt ->
let msg =
spf "LSP notification handler failed with %s\n%s"
(Printexc.to_string exn)
(Printexc.raw_backtrace_to_string bt)
in
Log.err (fun k -> k "%s" msg);
IO.return ())
in
try_catch @@ fun () ->
let open Lsp.Types in let open Lsp.Types in
match n with match n with
| Lsp.Client_notification.TextDocumentDidOpen | Lsp.Client_notification.TextDocumentDidOpen
@ -654,8 +703,8 @@ module Make (IO : IO) = struct
| Lsp.Client_notification.DidSaveTextDocument params -> | Lsp.Client_notification.DidSaveTextDocument params ->
let notify_back = let notify_back =
new notify_back new notify_back
~workDoneToken:None ~partialResultToken:None ~uri:params.textDocument.uri ~workDoneToken:None ~partialResultToken:None
~notify_back ~server_request () ~uri:params.textDocument.uri ~notify_back ~server_request ()
in in
async self (fun () -> async self (fun () ->

View file

@ -18,6 +18,6 @@ module type IO = sig
val read_line : in_channel -> string t val read_line : in_channel -> string t
val write : out_channel -> bytes -> int -> int -> unit t val write : out_channel -> bytes -> int -> int -> unit t
val write_string : out_channel -> string -> unit t val write_string : out_channel -> string -> unit t
val fail : exn -> unit t val fail : exn -> Printexc.raw_backtrace -> unit t
val catch : (unit -> 'a t) -> (exn -> 'a t) -> 'a t val catch : (unit -> 'a t) -> (exn -> Printexc.raw_backtrace -> 'a t) -> 'a t
end end