diff --git a/.Makefile.lint b/.Makefile.lint index ecae11caedaf5711a2d0241b5a76796b47daffe0..7d18f22eed17986328f25354182e7a0ccd4fb06a 100644 --- a/.Makefile.lint +++ b/.Makefile.lint @@ -81,17 +81,6 @@ ML_LINT_KO+=src/kernel_services/ast_data/ast.mli ML_LINT_KO+=src/kernel_services/ast_data/kernel_function.ml ML_LINT_KO+=src/kernel_services/ast_data/kernel_function.mli ML_LINT_KO+=src/kernel_services/ast_data/property_status.mli -ML_LINT_KO+=src/kernel_services/ast_printing/cabs_debug.ml -ML_LINT_KO+=src/kernel_services/ast_printing/cil_descriptive_printer.ml -ML_LINT_KO+=src/kernel_services/ast_printing/cil_printer.mli -ML_LINT_KO+=src/kernel_services/ast_printing/cil_types_debug.mli -ML_LINT_KO+=src/kernel_services/ast_printing/cprint.ml -ML_LINT_KO+=src/kernel_services/ast_printing/cprint.mli -ML_LINT_KO+=src/kernel_services/ast_printing/logic_print.ml -ML_LINT_KO+=src/kernel_services/ast_printing/printer.ml -ML_LINT_KO+=src/kernel_services/ast_printing/printer_api.mli -ML_LINT_KO+=src/kernel_services/ast_printing/printer_builder.ml -ML_LINT_KO+=src/kernel_services/ast_printing/printer_builder.mli ML_LINT_KO+=src/kernel_services/ast_queries/ast_info.ml ML_LINT_KO+=src/kernel_services/ast_queries/ast_info.mli ML_LINT_KO+=src/kernel_services/ast_queries/cil.mli diff --git a/.gitignore b/.gitignore index 633a7d72b4f6a96980eec0d1d508467a17ddfa92..0e333030f4d2f99767fd4ca245b5c91ccbfd7f55 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ autom4te.cache /share/manuals/ #doc + /doc/manuals/ /doc/*/*.dot /doc/*/*.aux @@ -137,6 +138,8 @@ autom4te.cache /doc/pdg/pdg00*.png /doc/pdg/previous_motif.gif +/doc/server/ + #lib /lib/fc/ /lib/plugins/*.mli diff --git a/Makefile b/Makefile index a2f6ecef224d5acb8fb13fb8f3a5a9abda75da13..43c11ae3d2657d51b590cd0944d0d2d696344025 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,7 @@ PLUGIN_BIN_DOC_LIST:= PLUGIN_DIST_EXTERNAL_LIST:= PLUGIN_DIST_TESTS_LIST:= PLUGIN_DISTRIBUTED_NAME_LIST:= +MERLIN_PACKAGES:= PLUGIN_HEADER_SPEC_LIST := PLUGIN_HEADER_DIRS_LIST := @@ -451,6 +452,7 @@ LIB_CMO =\ src/libraries/utils/leftistheap \ src/libraries/stdlib/integer \ src/libraries/utils/json \ + src/libraries/utils/markdown \ src/libraries/utils/rich_text \ src/libraries/utils/dotgraph @@ -1511,6 +1513,28 @@ plugins-doc: $(addsuffix _DOC,$(PLUGIN_DISTRIBUTED_NAME_LIST)),\ $(PLUGIN_DOC_LIST))) +.PHONY: server-doc-md server-doc-html server-doc + +server-doc-md: byte + $(PRINT) 'Generating Markdown server documentation' + @rm -fr doc/server + @mkdir -p doc/server + ./bin/frama-c.byte -server-doc doc/server + +server-doc-html: server-doc-md + $(PRINT) 'Generating HTML server documentation' + @find doc/server -name "*.md" -print -exec pandoc \ + --standalone --toc --toc-depth=2 --to html \ + --template doc/pandoc/template.html \ + --metadata-file {}.json \ + --lua-filter doc/pandoc/href.lua \ + {} -o {}.html \; + @cp -f doc/pandoc/style.css doc/server/ + $(PRINT) 'HTML server documentation ready:' + $(PRINT) ' open doc/server/readme.md.html' + +server-doc: server-doc-html + # to make the documentation for one plugin only, # the name of the plugin should begin with a capital letter : # Example for the pdg doc : make Pdg_DOC diff --git a/doc/code/.gitignore b/doc/code/.gitignore index b7720446464e2b86180a469acd76669aa411012f..d215811ad788d24a80392ce042fe246711ae33ff 100644 --- a/doc/code/.gitignore +++ b/doc/code/.gitignore @@ -33,3 +33,4 @@ /value/ /variadic/ /wp/ +/server/ diff --git a/doc/pandoc/href.lua b/doc/pandoc/href.lua new file mode 100644 index 0000000000000000000000000000000000000000..080635d1533c0aee541b0c4b85bb8917b80bcd74 --- /dev/null +++ b/doc/pandoc/href.lua @@ -0,0 +1,4 @@ +function Link(el) + el.target = string.gsub(el.target, "%.md", ".md.html") + return el +end diff --git a/doc/pandoc/style.css b/doc/pandoc/style.css new file mode 100644 index 0000000000000000000000000000000000000000..ba609d558f7bdb1b4936cfd8fcf9520199602f46 --- /dev/null +++ b/doc/pandoc/style.css @@ -0,0 +1,224 @@ +/**************************************************************************/ +/* */ +/* This file is part of Frama-C. */ +/* */ +/* Copyright (C) 2007-2018 */ +/* CEA (Commissariat à l'énergie atomique et aux énergies */ +/* alternatives) */ +/* */ +/* you can redistribute it and/or modify it under the terms of the GNU */ +/* Lesser General Public License as published by the Free Software */ +/* Foundation, version 2.1. */ +/* */ +/* It is distributed in the hope that it will be useful, */ +/* but WITHOUT ANY WARRANTY; without even the implied warranty of */ +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ +/* GNU Lesser General Public License for more details. */ +/* */ +/* See the GNU Lesser General Public License version 2.1 */ +/* for more details (enclosed in the file licenses/LGPLv2.1). */ +/* */ +/**************************************************************************/ + +* { margin: 0; padding: 0 } + +html +{ + background-color: #fff; +} + +* { margin: 0; padding: 0 } + +body { + position: absolute; + color: #222; + width: 100%; + height: 100%; + overflow: hidden; + font-family: "Verdana", sans; + font-size: 12pt; +} + +/* -------------------------------------------------------------------------- */ +/* --- Headers --- */ +/* -------------------------------------------------------------------------- */ + +#BODY { + display: flex ; + flex-flow: row nowrap ; + width: 100% ; + height: 100% ; + overflow: hidden ; +} + +#NAVIGATION { + display: block ; + overflow: auto ; + flex: 0 1 auto ; + order: 1 ; + box-sizing: border-box ; + min-width: 3cm ; + max-width: 8cm ; + height: 100% ; + padding: 0cm 0.5cm 0cm 0.5cm ; + background: #888 ; +} + +#CONTENT { + display: block ; + overflow: auto ; + flex: 1 1 auto ; + order: 2 ; + justify-content: left ; + align-content: center ; + box-sizing: border-box ; + width: 18cm ; + height: 100% ; + padding: 0cm 1cm 1cm 1cm ; +} + +/* -------------------------------------------------------------------------- */ +/* --- Navigation --- */ +/* -------------------------------------------------------------------------- */ + +#TOC { + font-size: smaller ; +} + +#NAVIGATION a.root { + display: block; + font-family: "Optima", "Verdana", "Arial", sans; + font-size: 16pt; + margin-top: 1cm; + margin-bottom: 6mm; +} + +#NAVIGATION a.chapter { + display: block ; + margin-left: 0px; + margin-top: 10px; + margin-bottom: 10px; +} + +#NAVIGATION a:hover { + background-color: darkorange ; +} + +#NAVIGATION a { + color: white ; +} + +#NAVIGATION code { + color: black ; +} + +#NAVIGATION ul { + width: 6cm ; +} + +#NAVIGATION ul > ul { + margin-left: 0px ; + padding-top: 2px ; + padding-bottom: 2px ; + background-color: darkgrey ; +} + +/* -------------------------------------------------------------------------- */ +/* --- Headers --- */ +/* -------------------------------------------------------------------------- */ + +h1 { + width: 18cm; + font-family: "Optima", "Verdana", "Arial", sans; + text-align: left; + margin-top: 1cm; + margin-bottom: 6mm; + padding-left: 3mm; + border-left: 20px solid red; +} + +h2 { + width: 18cm; + font-family: "Optima", "Verdana", "Arial", sans; + margin-top: 5mm; + margin-bottom: 2mm; + border-bottom: solid thin darkred ; + color: darkred; +} + +h3 { + width: 17cm; + font-family: "Optima", "Verdana", "Arial", sans; + color: black; + margin-top: 5mm; + margin-bottom: 3mm; + border-bottom: thin solid #404040; +} + +h4,h5,h6 { + margin-left: 4mm; + margin-top: 4mm; + margin-bottom: 1mm; + font-family: "Optima", "Verdana", "Arial", sans; + font-size: 10pt; + font-style: italic; + font-weight: bold; + color: darkred; +} + +p { margin: 6px 0px 6px 0px; width: 15cm; } + +ul,ol,blockquote { + margin-left: 24px ; + width: 13cm; +} + +li { + padding-left: 6px ; + padding-right : 6px ; +} + +pre { + width : 15cm ; + background-color: #eef ; +} + +pre,code { font-size: smaller ; color: #106000 } + +hr { + border: none ; + border-top: 1px solid #404040 ; + margin-top: 4mm ; + margin-bottom: 4mm ; +} + +table { + border-collapse: collapse ; + margin-left: 6mm ; +} + +th,td { + padding: 2px 16px 2px 12px ; +} + +thead { + border-bottom: thin solid gray ; +} + +tr.odd { + background-color: rgba(178, 222, 236, 0.3) ; +} + +:target { background-color: darkorange; } + +a:visited { color: darkred; text-decoration: none } +a:link { color: darkred; text-decoration: none } +a:hover { background-color: lightgray; color: firebrick } +a:active { background-color: lightgray; color: darkgreen } + +a:visited em { color: darkgreen; text-decoration: none } +a:link em { color: darkgreen; text-decoration: none } +a:hover em { background-color: lightgray; color: firebrick } +a:active em { background-color: lightgray; color: darkgreen } + +/* -------------------------------------------------------------------------- */ diff --git a/doc/pandoc/template.html b/doc/pandoc/template.html new file mode 100644 index 0000000000000000000000000000000000000000..e6871d568e832f49fc1bcaadb8171df7a7f47b0b --- /dev/null +++ b/doc/pandoc/template.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$> +<head> + <meta charset="utf-8" /> + <meta name="generator" content="pandoc" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> + <title>$if(document)$$document$ – $endif$$title$</title> + <link rel="stylesheet" href="$root$/style.css" /> + <!--[if lt IE 9]> + <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script> + <![endif]--> +</head> +<body> + <div id="BODY"> + <div id="NAVIGATION"> + <nav id="TOC"> + <a class="root" href="$root$/readme.md.html">$document$</a> + $if(chapter)$ + <a class="chapter" href="$page$">$chapter$</a> + $endif$ + $if(link)$ + <ul> + $if(toc)$ + $for(link)$ + $if(link.toc)$ + $table-of-contents$ + $else$ + <li><a href="$link.href$.html">$link.title$</a></li> + $endif$ + $endfor$ + $else$ + $for(link)$ + <li><a href="$link.href$.html">$link.title$</a></li> + $endfor$ + $endif$ + </ul> + $else$ + $table-of-contents$ + $endif$ + </nav> + </div> + <div id="CONTENT"> + $body$ + </div> + </div> +</body> +</html> diff --git a/headers/header_spec.txt b/headers/header_spec.txt index eddf3001da28a29bd80934dc1b513c19c84a1c73..ec689685fd2bca4b2d4bbccc5c6a0a52487960c1 100644 --- a/headers/header_spec.txt +++ b/headers/header_spec.txt @@ -667,6 +667,8 @@ src/libraries/utils/indexer.ml: CEA_LGPL src/libraries/utils/indexer.mli: CEA_LGPL src/libraries/utils/leftistheap.ml: JCF_LGPL src/libraries/utils/leftistheap.mli: JCF_LGPL +src/libraries/utils/markdown.ml: CEA_LGPL +src/libraries/utils/markdown.mli: CEA_LGPL src/libraries/utils/pretty_utils.ml: CEA_LGPL src/libraries/utils/pretty_utils.mli: CEA_LGPL src/libraries/utils/qstack.ml: CEA_LGPL @@ -1025,6 +1027,32 @@ src/plugins/rte/rte.ml: CEA_LGPL_OR_PROPRIETARY src/plugins/rte/rte.mli: CEA_LGPL_OR_PROPRIETARY src/plugins/rte/visit.ml: CEA_LGPL_OR_PROPRIETARY src/plugins/rte/visit.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/.gitignore: .ignore +src/plugins/server/Makefile.in: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/Server.mli: .ignore +src/plugins/server/configure.ac: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/data.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/data.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/doc.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/doc.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/jbuffer.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/jbuffer.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_ast.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_ast.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_main.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_main.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_project.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/kernel_project.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/main.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/main.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/request.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/request.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/server_parameters.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/server_parameters.mli: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/server_batch.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/server_zmq.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/syntax.ml: CEA_LGPL_OR_PROPRIETARY +src/plugins/server/syntax.mli: CEA_LGPL_OR_PROPRIETARY src/plugins/scope/Scope.mli: CEA_LGPL_OR_PROPRIETARY src/plugins/scope/datascope.ml: CEA_LGPL_OR_PROPRIETARY src/plugins/scope/datascope.mli: CEA_LGPL_OR_PROPRIETARY diff --git a/src/libraries/utils/markdown.ml b/src/libraries/utils/markdown.ml new file mode 100644 index 0000000000000000000000000000000000000000..0295d8007b6588eac586be4811bc619ef65947d2 --- /dev/null +++ b/src/libraries/utils/markdown.ml @@ -0,0 +1,333 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Markdown Documentation Generation Utility --- *) +(* -------------------------------------------------------------------------- *) + +type md = Format.formatter -> unit +type text = md +type block = md +type section = md + +let pretty fmt w = w fmt +let pp_text = pretty +let pp_block = pretty +let pp_section = pretty + +(* -------------------------------------------------------------------------- *) +(* --- Context --- *) +(* -------------------------------------------------------------------------- *) + +type toc = level:int -> name:string -> title:string -> unit + +type context = { + page: string ; + path: string list ; + names: bool ; + level: int ; + toc: toc option ; +} + +let context = ref { + page = "" ; + path = [] ; + names = false ; + level = 0 ; + toc = None ; + } + +let local ctxt job data = + let current = !context in + try context := ctxt ; job data ; context := current + with err -> context := current ; raise err + +(* -------------------------------------------------------------------------- *) +(* --- Combinators --- *) +(* -------------------------------------------------------------------------- *) + +let nil _fmt = () +let empty= nil +let space fmt = Format.pp_print_space fmt () +let newline fmt = Format.pp_print_newline fmt () + +let merge sep ds fmt = + match List.filter (fun d -> d != nil) ds with + | [] -> () + | d::ds -> d fmt ; List.iter (fun d -> sep fmt ; d fmt) ds + +let glue ?sep ds fmt = + match sep with + | None -> List.iter (fun d -> d fmt) ds + | Some s -> merge s ds fmt + +let (<@>) a b = + if a == empty then b else + if b == empty then a else + fun fmt -> a fmt ; b fmt + +let (<+>) a b = + if a == empty then b else + if b == empty then a else + fun fmt -> a fmt ; space fmt ; b fmt + +let (</>) a b = + if a == empty then b else + if b == empty then a else + fun fmt -> a fmt ; newline fmt ; b fmt + +let fmt_text k fmt = Format.fprintf fmt "@[<h 0>%t@]" k +let fmt_block k fmt = Format.fprintf fmt "@[<v 0>%t@]@\n" k + +(* -------------------------------------------------------------------------- *) +(* --- Elementary Text --- *) +(* -------------------------------------------------------------------------- *) + +let raw s fmt = Format.pp_print_string fmt s +let rm s fmt = Format.pp_print_string fmt s +let it s fmt = Format.fprintf fmt "_%s_" s +let bf s fmt = Format.fprintf fmt "**%s**" s +let tt s fmt = Format.fprintf fmt "`%s`" s +let text = merge space +let praw s = fmt_block (raw s) + +(* -------------------------------------------------------------------------- *) +(* --- Links --- *) +(* -------------------------------------------------------------------------- *) + +type href = [ + | `URL of string + | `Page of string + | `Name of string + | `Section of string * string +] + +let filepath m = String.split_on_char '/' m + +let rec relative source target = + match source , target with + | p::ps , q::qs when p = q -> relative ps qs + | [] , _ -> target + | _::d , _ -> List.map (fun _ -> "..") d @ target + +let lnk target = + String.concat "/" (relative !context.path (filepath target)) + +let id m = + let buffer = Buffer.create (String.length m) in + let lowercase = Char.lowercase_ascii in + let dash = ref false in + let emit c = + if !dash then (Buffer.add_char buffer '-' ; dash := false) ; + Buffer.add_char buffer c in + String.iter + (function + | '0'..'9' as c -> emit c + | 'a'..'z' as c -> emit c + | 'A'..'Z' as c -> emit (lowercase c) + | '.' | '_' as c -> emit c + | ' ' | '\t' | '\n' | '-' -> dash := (Buffer.length buffer > 0) + | _ -> ()) m ; + Buffer.contents buffer + +let href ?title (h : href) fmt = + match title , h with + | None , `URL url -> Format.fprintf fmt "%s" url + | Some w , `URL url -> Format.fprintf fmt "[%s](%s)" w url + | None , `Page p -> Format.fprintf fmt "[%s](%s)" p (lnk p) + | Some w , `Page p -> Format.fprintf fmt "[%s](%s)" w (lnk p) + | None , `Section(p,s) -> Format.fprintf fmt "[%s](%s#%s)" s (lnk p) (id s) + | Some w , `Section(p,s) -> Format.fprintf fmt "[%s](%s#%s)" w (lnk p) (id s) + | None , `Name a -> Format.fprintf fmt "[%s](#%s)" a (id a) + | Some w , `Name a -> Format.fprintf fmt "[%s](#%s)" w (id a) + +(* -------------------------------------------------------------------------- *) +(* --- Blocks --- *) +(* -------------------------------------------------------------------------- *) + +let aname anchor fmt = + Format.fprintf fmt "<a name=\"%s\"></a>@\n" (id anchor) + +let title h ?name title fmt = + begin + let { level ; names ; toc } = !context in + let level = max 0 (min 5 (level + h - 1)) in + Format.fprintf fmt "%s %s" (String.make level '#') title ; + if names || name <> None || toc <> None then + begin + let anchor = match name with None -> title | Some a -> a in + Format.fprintf fmt " {#%s}" (id anchor) ; + (match toc with + | None -> () + | Some callback -> + callback ~level ~name:anchor ~title) ; + end ; + Format.pp_print_newline fmt () ; + end + +let h1 = title 1 +let h2 = title 2 +let h3 = title 3 +let h4 = title 4 + +let indent h w fmt = local { !context with level = !context.level + h } w fmt + +let in_h1 = indent 1 +let in_h2 = indent 2 +let in_h3 = indent 3 +let in_h4 = indent 4 + +let hrule fmt = Format.pp_print_string fmt "-------------------------@." + +let par w fmt = Format.fprintf fmt "@[<hov 0>%t@]@." w + +let list ws fmt = + List.iter + (fun w -> Format.fprintf fmt "@[<hov 2>- %t@]@." w) ws + +let enum ws fmt = + let k = ref 0 in + List.iter + (fun w -> incr k ; Format.fprintf fmt "@[<hov 3>%d. %t@]@." !k w) ws + +let description items fmt = + List.iter + (fun (a,w) -> Format.fprintf fmt "@[<hov 2>- **%s** %t@]@." a w) items + +let code ?(lang="") pp fmt = + begin + Format.fprintf fmt "@[<v 0>```%s" lang ; + let buffer = Buffer.create 80 in + let bfmt = Format.formatter_of_buffer buffer in + pp bfmt ; Format.pp_print_flush bfmt () ; + let content = Buffer.contents buffer in + let lines = String.split_on_char '\n' content in + let rec clean = function [] -> [] | ""::w -> clean w | w -> w in + List.iter + (fun l -> Format.fprintf fmt "@\n%s" l) + (List.rev (clean (List.rev (clean lines)))) ; + Format.fprintf fmt "@\n```@]@." + end + +type column = [ + | `Left of string + | `Right of string + | `Center of string +] + +let table columns rows fmt = + begin + Format.fprintf fmt "@[<v 0>" ; + List.iter + (function `Left h | `Right h | `Center h -> Format.fprintf fmt "| %s " h) + columns ; + Format.fprintf fmt "|@\n" ; + List.iter (fun column -> + let dash h k = String.make (max 3 (String.length h + k)) '-' in + match column with + | `Left h -> Format.fprintf fmt "|:%s" (dash h 1) + | `Right h -> Format.fprintf fmt "|%s:" (dash h 1) + | `Center h -> Format.fprintf fmt "|:%s:" (dash h 0) + ) columns ; + Format.fprintf fmt "|@\n" ; + List.iter (fun row -> + List.iter (fun col -> Format.fprintf fmt "| @[<h 0>%t@] " col) row ; + Format.fprintf fmt "|@\n" ; + ) rows ; + Format.fprintf fmt "@]@." ; + end + +let concat ps = merge newline (List.filter (fun p -> p != empty) ps) + +(* -------------------------------------------------------------------------- *) +(* --- Refs --- *) +(* -------------------------------------------------------------------------- *) + +let mk f fmt = (f ()) fmt +let mk_text = mk +let mk_block = mk + +(* -------------------------------------------------------------------------- *) +(* --- Sections --- *) +(* -------------------------------------------------------------------------- *) + +let document s = s + +let subsections section subsections = section </> in_h1 (merge newline subsections) + +let section ?name ~title content subsections = + h1 ?name title </> content </> in_h1 (merge newline subsections) + +(* -------------------------------------------------------------------------- *) +(* --- Include File --- *) +(* -------------------------------------------------------------------------- *) + +let from_file path fmt = + let inc = open_in path in + try + while true do + let line = input_line inc in + Format.pp_print_string fmt line ; + Format.pp_print_newline fmt () ; + done + with + | End_of_file -> close_in inc + | exn -> close_in inc ; raise exn + +let read_block = from_file +let read_section = from_file +let read_text path fmt = Format.fprintf fmt "@[<h 0>%t@]" (from_file path) + +(* -------------------------------------------------------------------------- *) +(* --- Dump to File --- *) +(* -------------------------------------------------------------------------- *) + +let rec mkdir root page = + let dir = Filename.dirname page in + if dir <> "." && dir <> ".." then + let path = Printf.sprintf "%s/%s" root dir in + if not (Sys.file_exists path) then + begin + mkdir root dir ; + try Unix.mkdir path 0o755 + with Unix.Unix_error _ -> + failwith (Printf.sprintf "Can not create direcoty '%s'" dir) + end + +let dump ~root ~page ?(names=false) ?toc doc = + local + { page ; path = filepath page ; level = 1 ; toc ; names = names } + begin fun () -> + mkdir root page ; + let out = open_out (Printf.sprintf "%s/%s" root page) in + let fmt = Format.formatter_of_out_channel out in + try + doc fmt ; + Format.pp_print_newline fmt () ; + close_out out ; + with err -> + Format.pp_print_newline fmt () ; + close_out out ; + raise err + end () + +(* -------------------------------------------------------------------------- *) diff --git a/src/libraries/utils/markdown.mli b/src/libraries/utils/markdown.mli new file mode 100644 index 0000000000000000000000000000000000000000..34b955919fcc8696722172e66ad43680b689335c --- /dev/null +++ b/src/libraries/utils/markdown.mli @@ -0,0 +1,183 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Markdown Documentation Generation Utility --- *) +(* -------------------------------------------------------------------------- *) + +(** {2 Markdown} + + A lightweight helper for generating Markdown documentation. + Two levels of formatters are defined to help managing indentation and + spaces: [text] for inline markdown, and [block] for markdown paragraphs. + +*) + +type text +type block +type section + +val (<@>) : text -> text -> text (** Infix operator for [glue] *) +val (<+>) : text -> text -> text (** Infix operator for [text] *) +val (</>) : block -> block -> block (** Infix operator for [concat] *) + +(** {2 Text Constructors} *) + +val nil : text (** Empty *) +val raw : string -> text (** inlined markdown format *) +val rm : string -> text (** roman (normal) style *) +val it : string -> text (** italic style *) +val bf : string -> text (** bold style *) +val tt : string -> text (** typewriter style *) + +val glue : ?sep:text -> text list -> text (** Glue text fragments *) +val text : text list -> text (** Glue text fragments with spaces *) + +(** {2 Block Constructors} *) + +val empty : block (** Empty *) +val hrule : block (** Horizontal rule *) + +val h1 : ?name:string -> string -> block (** Title level 1 *) +val h2 : ?name:string -> string -> block (** Title level 2 *) +val h3 : ?name:string -> string -> block (** Title level 3 *) +val h4 : ?name:string -> string -> block (** Title level 4 *) + +val in_h1 : block -> block (** Increment title levels by 1 *) +val in_h2 : block -> block (** Increment title levels by 2 *) +val in_h3 : block -> block (** Increment title levels by 3 *) +val in_h4 : block -> block (** Increment title levels by 4 *) + +val par : text -> block (** Simple text paragraph *) +val praw : string -> block (** Simple raw paragraph *) +val list : text list -> block (** Itemized list *) +val enum : text list -> block (** Enumerated list *) +val description : (string * text) list -> block (** Description list *) + +(** Formatted code. + + The code content is pretty-printed in a vertical [<v0>] box + with default [Format] formatter. + Leading and trailing empty lines are removed and indentation is + preserved. *) +val code : ?lang:string -> (Format.formatter -> unit) -> block + +val concat : block list -> block (** Glue paragraphs with empty lines *) + +(** {2 Hyperlinks} + + [`Page], [`Name] and [`Section] links refers to the current document, + see [dump] function below. + + In [`Section(p,t)], [p] is the page path relative to the + document {i root}, and [t] is the section title {i or} name. + + For [`Name a], the links refers to name or title [a] + in the {i current} page. + + Hence, everywhere throughout a self-content document directory [~root], + local page [~page] inside [~root] can be referenced + by [`Page page], and its sections can by [`Section(page,title)] + or [`Section(page,name)]. + +*) + +type href = [ + | `URL of string + | `Page of string + | `Name of string + | `Section of string * string +] + +(** Default [~title] is taken from [href]. When printed, + actual link will be relativized with respect to current page. *) +val href : ?title:string -> href -> text + +(** Define a local anchor *) +val aname : string -> block + +(** {2 Tables} *) + +type column = [ + | `Left of string + | `Right of string + | `Center of string +] + +val table : column list -> text list list -> block + +(** {2 Markdown Generator} + Generating function are called each time the markdown + fragment is printed. *) + +val mk_text : (unit -> text) -> text +val mk_block : (unit -> block) -> block + +(** {2 Sections} + + Sections are an alternative to [h1-h4] constructors to build + properly nested sub-sections. Deep sections at depth 5 and more are + flattened. +*) + +val section : ?name:string -> title:string -> block -> section list -> section +val subsections : section -> section list -> section +val document : section -> block + +(** {2 Dump to file} + + Generate the markdown [~page] in directory [~root] with the given content. + The [~root] directory shall be absolute or relative to the current working + directory. The [~page] file-path shall be relative to the [~root] directory + and will be used to relocate hyperlinks to other [`Page] and [`Section] + properly. + + Hence, everywhere throughout the document, [dump ~root ~page doc] + is referenced by [`Page page], and its sections are referenced by + [`Section(page,title)]. + +*) + +(** Callback to listen for actual sections when printing a page. *) +type toc = level:int -> name:string -> title:string -> unit + +(** Create a markdown page. + - [~root] document directory (relocatable) + - [~page] relative file-path of the page in [~root] (non relocatable) + - [~names] generate explicit [<a name=...>] tags for all titles + - [~toc] optional callback to register table of contents +*) +val dump : root:string -> page:string -> ?names:bool -> ?toc:toc -> block -> unit + +(** {2 Miscellaneous} *) + +val read_text : string -> text +val read_block : string -> block +val read_section : string -> section + +val fmt_text : (Format.formatter -> unit) -> text +val fmt_block : (Format.formatter -> unit) -> block +val pp_text : Format.formatter -> text -> unit +val pp_block : Format.formatter -> block -> unit +val pp_section : Format.formatter -> section -> unit + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/.gitignore b/src/plugins/server/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f1d2f8cc5ec41ae8f688b80afeb51e4084f3c60f --- /dev/null +++ b/src/plugins/server/.gitignore @@ -0,0 +1,4 @@ +/Makefile +/Server.mli +/tests/ptests_config +/tests/report/result diff --git a/src/plugins/server/Makefile.in b/src/plugins/server/Makefile.in new file mode 100644 index 0000000000000000000000000000000000000000..f70395724dae585cc902a00f8ca0dcc190d9ace0 --- /dev/null +++ b/src/plugins/server/Makefile.in @@ -0,0 +1,117 @@ +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2019 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +# Do not use ?= to initialize both below variables +# (fixed efficiency issue, see GNU Make manual, Section 8.11) +ifndef FRAMAC_SHARE +FRAMAC_SHARE :=$(shell frama-c-config -print-share-path) +endif +ifndef FRAMAC_LIBDIR +FRAMAC_LIBDIR :=$(shell frama-c-config -print-libpath) +endif + +include $(FRAMAC_SHARE)/Makefile.config +# -------------------------------------------------------------------------- +# --- Plugin Setting +# -------------------------------------------------------------------------- + +PLUGIN_ENABLE:=@ENABLE_SERVER@ +PLUGIN_NAME:=Server +PLUGIN_DISTRIBUTED:=$(PLUGIN_ENABLE) +PLUGIN_DISTRIB_EXTERNAL:= Makefile.in configure.ac configure + +PLUGIN_REQUIRES:= yojson +PLUGIN_CMO:= \ + server_parameters \ + jbuffer \ + doc syntax data main request \ + server_batch \ + kernel_main \ + kernel_project \ + kernel_ast + +PLUGIN_UNDOC:= server_batch.ml server_zmq.ml + +PLUGIN_GENERATED:= $(PLUGIN_DIR)/Server.mli +PLUGIN_TESTS := + +# -------------------------------------------------------------------------- +# --- ZeroMQ Support +# -------------------------------------------------------------------------- + +ifeq (@SERVER_ZMQ@,yes) +PLUGIN_REQUIRES+= zmq +PLUGIN_CMO+= server_zmq +endif + +# -------------------------------------------------------------------------- +# --- Frama-C Dynamic Plug-in +# -------------------------------------------------------------------------- + +include $(FRAMAC_SHARE)/Makefile.dynamic + +# -------------------------------------------------------------------------- +# --- Server API +# -------------------------------------------------------------------------- + +SERVER_API= \ + doc.mli syntax.mli data.mli request.mli + +define Capitalize +$(shell printf "%s%s" \ + $$($(ECHO) $(1) | cut -c 1 | tr '[:lower:]' '[:upper:]') + $$($(ECHO) $(1) | cut -c 2-)) +endef + +define ExportModule +$(ECHO) "module $(call Capitalize, $(basename $(notdir $(1)))) : sig" >> $(2); +$(ECHO) '# 1 "$(1)"' >> $(2); +$(CAT) $(1) >> $(2); +$(ECHO) "end" >> $(2); +endef + +SERVER_MLI=$(addprefix $(Server_DIR)/, $(SERVER_API)) + +$(Server_DIR)/Server.mli: $(Server_DIR)/Makefile $(SERVER_MLI) + $(PRINT_MAKING) $@ "from" $(SERVER_MLI) + $(RM) $@ $@.tmp + $(ECHO) "(* This file is generated. Do not edit. *)" >> $@.tmp + $(ECHO) "(** {b Server Public API} *)" >> $@.tmp + $(foreach file,$(SERVER_MLI),$(call ExportModule,$(file),$@.tmp)) + $(CHMOD_RO) $@.tmp + $(MV) $@.tmp $@ + +# -------------------------------------------------------------------------- +# --- Configure +# -------------------------------------------------------------------------- + +ifeq ("$(FRAMAC_INTERNAL)","yes") +CONFIG_STATUS_DIR=$(FRAMAC_SRC) +else +CONFIG_STATUS_DIR=. +endif + +$(Server_DIR)/Makefile: $(Server_DIR)/Makefile.in \ + $(CONFIG_STATUS_DIR)/config.status + cd $(CONFIG_STATUS_DIR) && ./config.status --file $@ + +# -------------------------------------------------------------------------- diff --git a/src/plugins/server/configure.ac b/src/plugins/server/configure.ac new file mode 100644 index 0000000000000000000000000000000000000000..4ab9e3f73f11ed3c98fdf85d9531e12b51368e10 --- /dev/null +++ b/src/plugins/server/configure.ac @@ -0,0 +1,84 @@ +########################################################################## +# # +# This file is part of Frama-C. # +# # +# Copyright (C) 2007-2019 # +# CEA (Commissariat à l'énergie atomique et aux énergies # +# alternatives) # +# # +# you can redistribute it and/or modify it under the terms of the GNU # +# Lesser General Public License as published by the Free Software # +# Foundation, version 2.1. # +# # +# It is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU Lesser General Public License for more details. # +# # +# See the GNU Lesser General Public License version 2.1 # +# for more details (enclosed in the file licenses/LGPLv2.1). # +# # +########################################################################## + +######################################## +# Server as a standard Frama-C plug-in # +######################################## + +m4_define([plugin_file],Makefile.in) + +m4_define([FRAMAC_SHARE_ENV], + [m4_normalize(m4_esyscmd([echo $FRAMAC_SHARE]))]) + +m4_define([FRAMAC_SHARE], + [m4_ifval(FRAMAC_SHARE_ENV,[FRAMAC_SHARE_ENV], + [m4_esyscmd(frama-c -print-path)])]) + +m4_ifndef([FRAMAC_M4_MACROS], [m4_include(FRAMAC_SHARE/configure.ac)]) + +check_plugin(server,PLUGIN_RELATIVE_PATH(plugin_file),[Server plug-in],yes) + +######################################## +# Server Main Configuration # +######################################## + +AC_MSG_CHECKING(for Yojson) +SERVER_LIB_YOJSON=$($OCAMLFIND query yojson -format %v) +if test -z "$SERVER_LIB_YOJSON" +then + AC_MSG_RESULT(Server disabled (use 'opam install yojson')) + plugin_disable(server) +else + AC_MSG_RESULT(yes) +fi + +######################################## +# Server ZMQ Configuration # +######################################## + +AC_ARG_ENABLE( + server-zmq, + [ --enable-server-zmq Server ZeroMQ support (default: yes)], + SERVER_ZMQ=$enableval, + SERVER_ZMQ=yes +) + +if test "$SERVER_ZMQ" = "yes" ; +then + AC_MSG_CHECKING(for ZeroMQ) + SERVER_LIB_ZMQ=$($OCAMLFIND query zmq -format %v) + if test -z "$SERVER_LIB_ZMQ" + then + AC_MSG_RESULT(Server support for ZeroMQ disabled (use 'opam install zmq').) + SERVER_ZMQ=no + else + AC_MSG_RESULT(yes); + fi +fi + +AC_SUBST(SERVER_ZMQ) + +####################### +# Generating Makefile # +####################### + +write_plugin_config(Makefile) diff --git a/src/plugins/server/data.ml b/src/plugins/server/data.ml new file mode 100644 index 0000000000000000000000000000000000000000..46c204d72366ee3e90f6160d5dbc61a078989eeb --- /dev/null +++ b/src/plugins/server/data.ml @@ -0,0 +1,533 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Data Encoding --- *) +(* -------------------------------------------------------------------------- *) + +module Js = Yojson.Basic +module Ju = Yojson.Basic.Util + +type json = Js.t +let pretty = Js.pretty_print ~std:false + +module type S = +sig + type t + val syntax : Syntax.t + val of_json : json -> t + val to_json : t -> json +end + +module type Info = +sig + val page : Doc.page + val name : string + val descr : Markdown.text +end + +type 'a data = (module S with type t = 'a) + +let failure js msg = + Pretty_utils.ksfprintf (fun msg -> raise(Ju.Type_error(msg,js))) msg + +(* -------------------------------------------------------------------------- *) +(* --- Option --- *) +(* -------------------------------------------------------------------------- *) + +module Joption(A : S) : S with type t = A.t option = +struct + type t = A.t option + + let nullable = try ignore (A.of_json `Null) ; true with _ -> false + let syntax = + Syntax.option (if nullable then A.syntax else Syntax.tuple [A.syntax]) + + let to_json = function + | None -> `Null + | Some v -> if nullable then `List [A.to_json v] else A.to_json v + + let of_json = function + | `Null -> None + | `List [js] when nullable -> Some (A.of_json js) + | js -> Some (A.of_json js) + +end + +(* -------------------------------------------------------------------------- *) +(* --- Tuples --- *) +(* -------------------------------------------------------------------------- *) + +module Jpair(A : S)(B : S) : S with type t = A.t * B.t = +struct + type t = A.t * B.t + let syntax = Syntax.tuple [A.syntax;B.syntax] + let to_json (x,y) = `List [ A.to_json x ; B.to_json y ] + let of_json = function + | `List [ ja ; jb ] -> A.of_json ja , B.of_json jb + | js -> failure js "Expected list with 2 elements" +end + +module Jtriple(A : S)(B : S)(C : S) : S with type t = A.t * B.t * C.t = +struct + type t = A.t * B.t * C.t + let syntax = Syntax.tuple [A.syntax;B.syntax;C.syntax] + let to_json (x,y,z) = `List [ A.to_json x ; B.to_json y ; C.to_json z ] + let of_json = function + | `List [ ja ; jb ; jc ] -> A.of_json ja , B.of_json jb , C.of_json jc + | js -> failure js "Expected list with 3 elements" +end + +(* -------------------------------------------------------------------------- *) +(* --- Lists --- *) +(* -------------------------------------------------------------------------- *) + +module Jlist(A : S) : S with type t = A.t list = +struct + type t = A.t list + let syntax = Syntax.array A.syntax + let to_json xs = `List (List.map A.to_json xs) + let of_json js = List.map A.of_json (Ju.to_list js) +end + +(* -------------------------------------------------------------------------- *) +(* --- Arrays --- *) +(* -------------------------------------------------------------------------- *) + +module Jarray(A : S) : S with type t = A.t array = +struct + type t = A.t array + let syntax = Syntax.array A.syntax + let to_json xs = `List (List.map A.to_json (Array.to_list xs)) + let of_json js = Array.of_list @@ List.map A.of_json (Ju.to_list js) +end + +(* -------------------------------------------------------------------------- *) +(* --- Collections --- *) +(* -------------------------------------------------------------------------- *) + +module type S_collection = +sig + include S + module Joption : S with type t = t option + module Jlist : S with type t = t list + module Jarray : S with type t = t array +end + +module Collection(A : S) : S_collection with type t = A.t = +struct + include A + module Joption = Joption(A) + module Jlist = Jlist(A) + module Jarray = Jarray(A) +end + +(* -------------------------------------------------------------------------- *) +(* --- Atomic Types --- *) +(* -------------------------------------------------------------------------- *) + +module Junit : S with type t = unit = +struct + type t = unit + let syntax = Syntax.unit + let of_json _js = () + let to_json () = `Null +end + +module Jany : S with type t = json = +struct + type t = json + let syntax = Syntax.any + let of_json js = js + let to_json js = js +end + +module Jbool : S_collection with type t = bool = + Collection + (struct + type t = bool + let syntax = Syntax.boolean + let of_json = Ju.to_bool + let to_json b = `Bool b + end) + +module Jint : S_collection with type t = int = + Collection + (struct + type t = int + let syntax = Syntax.int + let of_json = Ju.to_int + let to_json n = `Int n + end) + +module Jfloat : S_collection with type t = float = + Collection + (struct + type t = float + let syntax = Syntax.number + let of_json = Ju.to_number + let to_json v = `Float v + end) + +module Jstring : S_collection with type t = string = + Collection + (struct + type t = string + let syntax = Syntax.string + let of_json = Ju.to_string + let to_json s = `String s + end) + +module Jident : S_collection with type t = string = + Collection + (struct + type t = string + let syntax = Syntax.ident + let of_json = Ju.to_string + let to_json s = `String s + end) + +let text_page = Doc.page `Kernel ~title:"Rich Text Format" ~filename:"text.md" + +module Jtext = +struct + include Jany + let syntax = Syntax.publish ~page:text_page ~name:"text" + ~synopsis:Syntax.any ~descr:(Markdown.rm "Formatted text.") () +end + +(* -------------------------------------------------------------------------- *) +(* --- Records --- *) +(* -------------------------------------------------------------------------- *) + +module Fmap = Map.Make(String) + +module Record( R : Info ) = +struct + + type t = json Fmap.t + + type 'a field = { + member : t -> bool ; + getter : t -> 'a ; + setter : t -> 'a -> t ; + } + + (* Declared Fields in this Record *) + let fdocs = ref [] + let defaults = ref Fmap.empty + + let default () = !defaults + let has fd r = fd.member r + let get fd r = fd.getter r + let set fd v r = fd.setter r v + + let field (type a) name ~descr ?default (d : a data) : a field = + let module D = (val d) in + begin match default with + | None -> () + | Some v -> defaults := Fmap.add name (D.to_json v) !defaults + end ; + fdocs := Syntax.{ name ; syntax = D.syntax ; descr } :: !fdocs ; + let member r = Fmap.mem name r in + let getter r = D.of_json (Fmap.find name r) in + let setter r v = Fmap.add name (D.to_json v) r in + { member ; getter ; setter } + + let option (type a) name ~descr (d : a data) : a option field = + let module D = (val d) in + fdocs := Syntax.{ name ; syntax = option D.syntax ; descr } :: !fdocs ; + let member r = Fmap.mem name r in + let getter r = + try Some (D.of_json (Fmap.find name r)) with Not_found -> None in + let setter r = function + | None -> Fmap.remove name r + | Some v -> Fmap.add name (D.to_json v) r in + { member ; getter ; setter } + + let fields () = Syntax.fields ~title:"Field" !fdocs + + let syntax = + Syntax.publish ~page:R.page ~name:R.name + ~descr:R.descr + ~synopsis:(Syntax.record []) + ~details:(Markdown.mk_block fields) () + + let of_json js = + List.fold_left + (fun r (fd,js) -> Fmap.add fd js r) + (default ()) (Ju.to_assoc js) + + let to_json r : json = + `Assoc (Fmap.fold (fun fd js fds -> (fd,js) :: fds) r []) + +end + +(* -------------------------------------------------------------------------- *) +(* --- Index --- *) +(* -------------------------------------------------------------------------- *) + +(** Simplified [Map.S] *) +module type Map = +sig + type 'a t + type key + val empty : 'a t + val add : key -> 'a -> 'a t -> 'a t + val find : key -> 'a t -> 'a +end + +module type Index = +sig + include S_collection + val get : t -> int + val find : int -> t + val clear : unit -> unit +end + +let publish_id (module A : Info) = + Syntax.publish + ~page:A.page ~name:A.name ~synopsis:Syntax.int ~descr:A.descr () + +module INDEXER(M : Map)(I : Info) : +sig + type index + val create : unit -> index + val clear : index -> unit + val get : index -> M.key -> int + val find : index -> int -> M.key + val to_json : index -> M.key -> json + val of_json : index -> json -> M.key +end = +struct + + type index = { + mutable kid : int ; + mutable index : int M.t ; + lookup : (int,M.key) Hashtbl.t ; + } + + let create () = { + kid = 0 ; + index = M.empty ; + lookup = Hashtbl.create 0 ; + } + + let clear m = + begin + m.kid <- 0 ; + m.index <- M.empty ; + Hashtbl.clear m.lookup ; + end + + let get m a = + try M.find a m.index + with Not_found -> + let id = m.kid in + m.kid <- succ id ; + m.index <- M.add a id m.index ; + Hashtbl.add m.lookup id a ; id + + let find m id = Hashtbl.find m.lookup id + + let to_json m a = `Int (get m a) + let of_json m js = + let id = Ju.to_int js in + try find m id + with Not_found -> + failure js "[%s] No registered id #%d" I.name id + +end + +module Static(M : Map)(I : Info) : Index with type t = M.key = +struct + module INDEX = INDEXER(M)(I) + let index = INDEX.create () + let clear () = INDEX.clear index + let get = INDEX.get index + let find = INDEX.find index + include Collection + (struct + type t = M.key + let syntax = publish_id (module I) + let of_json = INDEX.of_json index + let to_json = INDEX.to_json index + end) +end + +module Index(M : Map)(I : Info) : Index with type t = M.key = +struct + + module INDEX = INDEXER(M)(I) + module TYPE : Datatype.S with type t = INDEX.index = + Datatype.Make + (struct + type t = INDEX.index + include Datatype.Undefined + let reprs = [INDEX.create()] + let name = "Server.Data.Index.Type." ^ I.name + let mem_project = Datatype.never_any_project + end) + module STATE = State_builder.Ref(TYPE) + (struct + let name = "Server.Data.Index.State." ^ I.name + let dependencies = [] + let default = INDEX.create + end) + + let index () = STATE.get () + let clear () = INDEX.clear (index()) + + let get a = INDEX.get (index()) a + let find id = INDEX.find (index()) id + + include Collection + (struct + type t = M.key + let syntax = publish_id (module I) + let of_json js = INDEX.of_json (index()) js + let to_json v = INDEX.to_json (index()) v + end) + +end + +module type IdentifiedType = +sig + type t + val id : t -> int + include Info +end + +module Identified(A : IdentifiedType) : Index with type t = A.t = +struct + + type index = (int,A.t) Hashtbl.t + + module TYPE : Datatype.S with type t = index = + Datatype.Make + (struct + type t = index + include Datatype.Undefined + let reprs = [Hashtbl.create 0] + let name = "Server.Data.Identified.Type." ^ A.name + let mem_project = Datatype.never_any_project + end) + + module STATE = State_builder.Ref(TYPE) + (struct + let name = "Server.Data.Identified.State." ^ A.name + let dependencies = [] + let default () = Hashtbl.create 0 + end) + + let lookup () = STATE.get () + let clear () = Hashtbl.clear (lookup()) + + let get = A.id + let find id = Hashtbl.find (lookup()) id + + include Collection + (struct + type t = A.t + let syntax = publish_id (module A) + let to_json a = `Int (get a) + let of_json js = + let k = Ju.to_int js in + try find k + with Not_found -> failure js "[%s] No registered id #%d" A.name k + end) + +end + +(* -------------------------------------------------------------------------- *) +(* --- Dictionnary --- *) +(* -------------------------------------------------------------------------- *) + +module type Enum = +sig + type t + val values : (t * string * Markdown.text) list + include Info +end + +module Dictionary(E : Enum) = +struct + + let registered = ref false + let index = Hashtbl.create 0 + let lookup = Hashtbl.create 0 + + let register () = + if not !registered then + begin + registered := true ; + let invalid msg tag = + let msg = Printf.sprintf "Server.Data.Enum.%s: duplicate %s (%S)" + E.name msg tag in + raise (Invalid_argument msg) + in + List.iter + (fun (value,tag,_) -> + if Hashtbl.mem index value then invalid "value" tag ; + Hashtbl.add index value tag ; + if Hashtbl.mem lookup tag then invalid "tag" tag ; + Hashtbl.add lookup tag value ; + ) E.values + end + + let values () = + Markdown.table + [ `Left E.name ; `Left "Description" ] + (List.map + (fun (_,tag,descr) -> + [ Markdown.tt (Printf.sprintf "%S" tag) ; descr ] + ) E.values) + + include Collection + (struct + type t = E.t + + let syntax = Syntax.publish + ~page:E.page ~name:E.name + ~synopsis:Syntax.ident + ~descr:E.descr ~details:(Markdown.mk_block values) () + + let to_json value = + register () ; + try `String (Hashtbl.find index value) + with Not_found -> + raise (Invalid_argument + (Printf.sprintf "[%s] Unregistered value" E.name)) + + let of_json js = + register () ; + let tag = Ju.to_string js in + try Hashtbl.find lookup tag + with Not_found -> + let msg = Printf.sprintf "[%s] Unregistered tag %S" E.name tag in + raise (Ju.Type_error(msg,js)) + + end) + +end + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/data.mli b/src/plugins/server/data.mli new file mode 100644 index 0000000000000000000000000000000000000000..f0c993fd042ca28c5abc2b961071ac01c9a4cb14 --- /dev/null +++ b/src/plugins/server/data.mli @@ -0,0 +1,190 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** Data Encoding *) +(* -------------------------------------------------------------------------- *) + +type json = Json.t + +val pretty : Format.formatter -> json -> unit + +module type S = +sig + type t + val syntax : Syntax.t + val of_json : json -> t + val to_json : t -> json +end + + +(** Datatype registration. + + Name and page must be consistent with each other: + - The name must be lowercase, dash-separated list of identifiers + - Protocol data must start with ["<server>-*"] + - Plugin data must start with ["<plugin>-*"] +*) +module type Info = +sig + val page : Doc.page + val name : string + val descr : Markdown.text +end + +type 'a data = (module S with type t = 'a) + +(* -------------------------------------------------------------------------- *) +(** {2 Collections} *) +(* -------------------------------------------------------------------------- *) + +module type S_collection = +sig + include S + module Joption : S with type t = t option + module Jlist : S with type t = t list + module Jarray : S with type t = t array +end + +module Collection(A : S) : S_collection with type t = A.t + +(* -------------------------------------------------------------------------- *) +(** {2 Constructors} *) +(* -------------------------------------------------------------------------- *) + +module Joption(A : S) : S with type t = A.t option +module Jpair(A : S)(B : S) : S with type t = A.t * B.t +module Jtriple(A : S)(B : S)(C : S) : S with type t = A.t * B.t * C.t +module Jlist(A : S) : S with type t = A.t list +module Jarray(A : S) : S with type t = A.t array + +(* -------------------------------------------------------------------------- *) +(** {2 Atomic Data} *) +(* -------------------------------------------------------------------------- *) + +module Junit : S with type t = unit +module Jany : S with type t = json +module Jbool : S_collection with type t = bool +module Jint : S_collection with type t = int +module Jfloat : S_collection with type t = float +module Jstring : S_collection with type t = string +module Jident : S_collection with type t = string (** Syntax is {i ident}. *) +module Jtext : S with type t = json (** Rich text encoding, see [Jbuffer] *) + +(* -------------------------------------------------------------------------- *) +(** {2 Records} *) +(* -------------------------------------------------------------------------- *) + +module Record(R : Info) : +sig + (** A new type [t] is created for each application of the functor. *) + include S + + (** Parametric field. Can only be used with type [t]. *) + type 'a field + + (** Field constructor *) + val field : string -> descr:Markdown.text -> ?default:'a -> 'a data -> 'a field + + (** Optional field constructor *) + val option : string -> descr:Markdown.text -> 'a data -> 'a option field + + (** Field presence. If the field has a default value, it will be always + present. *) + val has : 'a field -> t -> bool + + (** Field accessor. + @raise Not_found if the field is optional and not present *) + val get : 'a field -> t -> 'a + + (** Field updator. *) + val set : 'a field -> 'a -> t -> t + + (** Contains only the default values. *) + val default : unit -> t + +end + +(* -------------------------------------------------------------------------- *) +(** {2 Indexed Values} *) +(* -------------------------------------------------------------------------- *) + +(** Simplified [Map.S] *) +module type Map = +sig + type 'a t + type key + val empty : 'a t + val add : key -> 'a -> 'a t -> 'a t + val find : key -> 'a t -> 'a +end + +module type Index = +sig + include S_collection + val get : t -> int + val find : int -> t (** @raise Not_found if not registered *) + val clear : unit -> unit + (** Clear index tables. Use with extreme care. *) +end + +(** Builds an indexer that {i does not} depend on current project. *) +module Static(M : Map)(I : Info) : Index with type t = M.key + +(** Builds a {i projectified} index. *) +module Index(M : Map)(I : Info) : Index with type t = M.key + +(* -------------------------------------------------------------------------- *) +(** {2 Identified Types} *) +(* -------------------------------------------------------------------------- *) + +module type IdentifiedType = +sig + type t + val id : t -> int + include Info +end + +(** Builds a {i projectified} index on types with {i unique} identifiers *) +module Identified(A : IdentifiedType) : Index with type t = A.t + +(* -------------------------------------------------------------------------- *) +(** {2 Dictionary} *) +(* -------------------------------------------------------------------------- *) + +module type Enum = +sig + type t + val values : (t * string * Markdown.text) list + include Info +end + +module Dictionary(E : Enum) : S_collection with type t = E.t + +(* -------------------------------------------------------------------------- *) +(** {2 Misc} *) +(* -------------------------------------------------------------------------- *) + +val failure : json -> ('a, Format.formatter, unit, 'b) format4 -> 'a +(** @raise Yojson.Basic.Util.Type_error with provided message *) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/doc.ml b/src/plugins/server/doc.ml new file mode 100644 index 0000000000000000000000000000000000000000..0d141b3f018ecdd7b1946c5fa5f0db5a76bfd16f --- /dev/null +++ b/src/plugins/server/doc.ml @@ -0,0 +1,204 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Server Documentation --- *) +(* -------------------------------------------------------------------------- *) + +type json = Yojson.Basic.t +module Senv = Server_parameters +module Pages = Map.Make(String) + +type chapter = [ `Protocol | `Kernel | `Plugin of string ] + +type page = { + path : string ; + rootdir : string ; (* path to document root *) + chapter : chapter ; + title : string ; + order : int ; + intro : Markdown.section ; + mutable sections : Markdown.section list ; +} + +let order = ref 0 +let pages : page Pages.t ref = ref Pages.empty +let plugins : string list ref = ref [] +let entries : (string * Markdown.href) list ref = ref [] +let path page = page.path +let href page name : Markdown.href = `Section( page.path , name ) + +(* -------------------------------------------------------------------------- *) +(* --- Page Collection --- *) +(* -------------------------------------------------------------------------- *) + +let chapter pg = pg.chapter + +let page chapter ~title ~filename = + let rootdir,path = match chapter with + | `Protocol -> "." , filename + | `Kernel -> ".." , Printf.sprintf "kernel/%s" filename + | `Plugin name -> "../.." , Printf.sprintf "plugins/%s/%s" name filename + in + try + let other = Pages.find path !pages in + Senv.failure "Duplicate page '%s' path@." path ; other + with Not_found -> + let intro = match chapter with + | `Protocol -> + Printf.sprintf "%s/server/protocol/%s" Config.datadir filename + | `Kernel -> + Printf.sprintf "%s/server/kernel/%s" Config.datadir filename + | `Plugin name -> + if not (List.mem name !plugins) then plugins := name :: !plugins ; + Printf.sprintf "%s/%s/server/%s" Config.datadir name filename in + let intro = + if Sys.file_exists intro + then Markdown.read_section intro + else Markdown.(section ~title empty []) in + let order = incr order ; !order in + let page = { order ; rootdir ; path ; + chapter ; title ; intro ; + sections=[] } in + pages := Pages.add path page !pages ; page + +let publish ~page ?name ?(index=[]) ~title content sections = + let id = match name with Some id -> id | None -> title in + let href = `Section( page.path , id ) in + let section = Markdown.section ?name ~title content sections in + List.iter (fun entry -> entries := (entry , href) :: !entries) index ; + page.sections <- section :: page.sections ; href + +let _ = page `Protocol ~title:"Architecture" ~filename:"server.md" + +(* -------------------------------------------------------------------------- *) +(* --- Tables of Content --- *) +(* -------------------------------------------------------------------------- *) + +let title_of_chapter = function + | `Protocol -> "Server Protocols" + | `Kernel -> "Kernel Services" + | `Plugin name -> "Plugin " ^ String.capitalize_ascii name + +let pages_of_chapter c = + let w = ref [] in + Pages.iter + (fun _ p -> if p.chapter = c then w := p :: !w) !pages ; + List.sort (fun p q -> p.order - q.order) !w + +let table_of_chapter c fmt = + begin + Format.fprintf fmt "## %s@\n@." (title_of_chapter c) ; + List.iter + (fun p -> Format.fprintf fmt " - [%s](%s)@." p.title p.path) + (pages_of_chapter c) ; + Format.pp_print_newline fmt () ; + end + +let table_of_contents fmt = + begin + table_of_chapter `Protocol fmt ; + table_of_chapter `Kernel fmt ; + List.iter + (fun p -> table_of_chapter (`Plugin p) fmt) + (List.sort String.compare !plugins) + end + +let index () = + List.map + (fun (title,entry) -> Markdown.href ~title entry) + (List.sort (fun (a,_) (b,_) -> String.compare a b) !entries) + +let link ~toc ~title ~href : json = + let link = [ "title" , `String title ; "href" , `String href ] in + `Assoc (if not toc then link else ( "toc" , `Bool true ) :: link) + +let link_page page : json list = + List.fold_right + (fun p links -> + if p.chapter = page.chapter then + let toc = (p.path = page.path) in + let href = Filename.basename p.path in + link ~toc ~title:p.title ~href :: links + else links + ) (pages_of_chapter page.chapter) [] + +let maindata : json = + `Assoc [ + "document", `String "Frama-C Server" ; + "title",`String "Documentation" ; + "root", `String "." ; + ] + +let metadata page : json = + `Assoc [ + "document", `String "Frama-C Server" ; + "chapter", `String (title_of_chapter page.chapter) ; + "title", `String page.title ; + "root", `String page.rootdir ; + "link",`List (link_page page) ; + ] + +(* -------------------------------------------------------------------------- *) +(* --- Dump Documentation --- *) +(* -------------------------------------------------------------------------- *) + +let dump ~root ?(meta=true) () = + begin + Pages.iter + (fun path page -> + Senv.feedback "[doc] Page: '%s'" path ; + let body = Markdown.subsections page.intro (List.rev page.sections) in + Markdown.dump ~root ~page:path (Markdown.document body) ; + if meta then + let path = Printf.sprintf "%s/%s.json" root path in + Yojson.Basic.to_file path (metadata page) ; + ) !pages ; + Senv.feedback "[doc] Page: 'readme.md'" ; + if meta then + let path = Printf.sprintf "%s/readme.md.json" root in + Yojson.Basic.to_file path maindata ; + Markdown.(dump ~root ~page:"readme.md" + begin + h1 "Documentation" </> + par (bf "Version" <+> rm Config.version) </> + fmt_block table_of_contents </> + h2 "Index" </> + list (index ()) + end) ; + end + +let () = + Db.Main.extend begin + fun () -> + let root = Senv.Doc.get () in + if root <> "" then + if Sys.file_exists root && Sys.is_directory root then + begin + Senv.feedback "[doc] Root: '%s'" root ; + dump ~root () ; + end + else + Senv.error "[doc] File '%s' is not a directory" root + end + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/doc.mli b/src/plugins/server/doc.mli new file mode 100644 index 0000000000000000000000000000000000000000..e204916f44b8eec01dcc173221c77821bc990e9b --- /dev/null +++ b/src/plugins/server/doc.mli @@ -0,0 +1,68 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** Server Documentation *) +(* -------------------------------------------------------------------------- *) + +open Markdown + +(** The main chapters of the documentation. *) +type chapter = [ `Protocol | `Kernel | `Plugin of string ] + +(** A page of the server documentation. *) +type page + +val path : page -> string +val href : page -> string -> href +val chapter : page -> chapter + +(** Obtain the given page in the server documentation. + + The page initially contains an introductory section + read from the share directory: + - [frama-c/share/protocol/<filename>] for protocol pages, + - [frama-c/share/server/kernel/<filename>] for kernel pages, + - [frama-c/share/<plugin>/server/<filename>] for plugin's pages. +*) +val page : chapter -> title:string -> filename:string -> page + +(** Adds a section in the corresponding page. + Returns an href to the published section. + If index items are provided, they are added + to the server documentation index. +*) +val publish : + page:page -> + ?name:string -> + ?index:string list -> + title:string -> + Markdown.block -> + Markdown.section list -> + href + +(** Dumps all published pages of documentations. Unless [~meta:false], + also generates METADATA for each page in + [<filename>.json] for each page. *) +val dump : root:string -> ?meta:bool -> unit -> unit + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/jbuffer.ml b/src/plugins/server/jbuffer.ml new file mode 100644 index 0000000000000000000000000000000000000000..70e5d66af55ac68ad3b4d464a644f168d18f6073 --- /dev/null +++ b/src/plugins/server/jbuffer.ml @@ -0,0 +1,137 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +type json = Yojson.Basic.t + +type buffer = { + text : FCBuffer.t ; + mutable rjson : json list ; (* Current op-codes in reverse order *) + mutable stack : ( string * json list ) list ; + mutable fmt : Format.formatter ; +} + +let append buffer s k n = + FCBuffer.add_substring buffer.text s k n + +let flush buffer () = + let t = buffer.text in + let n = FCBuffer.length t in + if n > 0 then + let js = `String (FCBuffer.contents t) in + buffer.rjson <- js :: buffer.rjson ; + FCBuffer.clear t + +let push_tag buffer tag = + flush buffer () ; + buffer.stack <- ( tag , buffer.rjson ) :: buffer.stack ; + buffer.rjson <- [] + +let pop_tag buffer _tag = + match buffer.stack with + | [] -> () + | (tag,rjson)::stack -> + flush buffer () ; + buffer.stack <- stack ; + let content = List.rev buffer.rjson in + buffer.rjson <- + if content = [] then rjson + else + let block = `List ( `String tag :: content ) in + block :: rjson + +let no_mark _tag = () +let mark_open_tag buffer tg = push_tag buffer tg ; "" +let mark_close_tag buffer tg = pop_tag buffer tg ; "" + +let create ?indent ?margin () = + let buffer = { + fmt = Format.err_formatter ; + text = FCBuffer.create 80 ; rjson = [] ; stack = [] + } in + let fmt = Format.make_formatter (append buffer) (flush buffer) in + buffer.fmt <- fmt ; + begin match indent , margin with + | None , None -> () + | Some k , None -> + let m = Format.pp_get_margin fmt () in + Format.pp_set_max_indent fmt (max 0 (min k m)) + | None , Some m -> + Format.pp_set_margin fmt (max 0 m) ; + let k = Format.pp_get_max_indent fmt () in + if k < m-10 then Format.pp_set_max_indent fmt (max 0 (m-10)) + | Some k , Some m -> + Format.pp_set_margin fmt (max 0 m) ; + Format.pp_set_max_indent fmt (max 0 (min k (m-10))) + end ; + begin + let open Format in + pp_set_formatter_tag_functions fmt { + print_open_tag = no_mark ; + print_close_tag = no_mark ; + mark_open_tag = mark_open_tag buffer ; + mark_close_tag = mark_close_tag buffer ; + } ; + pp_set_print_tags fmt false ; + pp_set_mark_tags fmt true ; + end ; + buffer + +let bprintf buffer msg = Format.fprintf buffer.fmt msg +let formatter buffer = buffer.fmt + +let contents buffer : json = + flush buffer () ; + while buffer.stack <> [] do + pop_tag buffer "" + done ; + match List.rev buffer.rjson with + | [] -> `Null + | [`String _ as text] -> text + | content -> `List ( `String "" :: content ) + +let format ?indent ?margin msg = + let buffer = create ?indent ?margin () in + Format.kfprintf + (fun fmt -> Format.pp_print_flush fmt () ; contents buffer) + buffer.fmt msg + +let to_json ?indent ?margin pp a = + let buffer = create ?indent ?margin () in + pp buffer.fmt a ; + Format.pp_print_flush buffer.fmt () ; + contents buffer + +let rec fprintf fmt = function + | `Null -> () + | `String text -> Format.pp_print_string fmt text + | `List ( `String tag :: content ) -> + if tag <> "" then + begin + Format.fprintf fmt "@{<%s>" tag ; + List.iter (fprintf fmt) content ; + Format.fprintf fmt "@}" ; + end + else + List.iter (fprintf fmt) content + | js -> raise (Yojson.Basic.Util.Type_error("Invalid rich-text format",js)) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/jbuffer.mli b/src/plugins/server/jbuffer.mli new file mode 100644 index 0000000000000000000000000000000000000000..3e1e02a1beae8615cf925e52b980b093749766c1 --- /dev/null +++ b/src/plugins/server/jbuffer.mli @@ -0,0 +1,60 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +type json = Json.t + +(** All-in-one formatter. Return the JSON encoding of formatted text. *) +val format : ?indent:int -> ?margin:int -> + ('a,Format.formatter,unit,json) format4 -> 'a + +(** All-in-one formatter. Return the JSON encoding of formatted text. *) +val to_json : ?indent:int -> ?margin:int -> + (Format.formatter -> 'a -> unit) -> 'a -> json + +(** Buffer for encoding formatted text. *) +type buffer + +(** Create a formatter with [~indent] maximum indentation and + [~margin] right-margin. Defaults are those of [Format.make_formatter], + which are [~indent:68] and [~margin:78] with OCaml 4.05. *) +val create : ?indent:int -> ?margin:int -> unit -> buffer + +(** The underlying formatter of a buffer. *) +val formatter : buffer -> Format.formatter + +(** Prints into the buffer's formatter. *) +val bprintf : buffer -> ('a,Format.formatter,unit) format -> 'a + +val append : buffer -> string -> int -> int -> unit +val flush : buffer -> unit -> unit +val push_tag : buffer -> Format.tag -> unit +val pop_tag : buffer -> Format.tag -> unit + +(** Flushes the buffer and returns its JSON enoding. This pops all pending + tags. *) +val contents : buffer -> json + +(** Prints back a JSON encoding onto the provided formatter. + @raise Yojson.Basic.Util.Type_error in case of ill formatted buffer. *) +val fprintf : Format.formatter -> json -> unit + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_ast.ml b/src/plugins/server/kernel_ast.ml new file mode 100644 index 0000000000000000000000000000000000000000..229c526ab6f75a73624235f3c09c42b5a4ed27e5 --- /dev/null +++ b/src/plugins/server/kernel_ast.ml @@ -0,0 +1,164 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + + +open Data +module Sy = Syntax +module Md = Markdown +module Js = Yojson.Basic.Util +open Cil_types + +let page = Doc.page `Kernel ~title:"Ast Services" ~filename:"ast.md" + +(* -------------------------------------------------------------------------- *) +(* --- Compute Ast --- *) +(* -------------------------------------------------------------------------- *) + +let () = Request.register ~page + ~kind:`EXEC ~name:"kernel.ast.compute" + ~descr:(Md.rm "Ensures that AST is computed") + ~input:(module Junit) ~output:(module Junit) Ast.compute + +(* -------------------------------------------------------------------------- *) +(* --- Printers --- *) +(* -------------------------------------------------------------------------- *) + +module Tag = +struct + + open Printer_tag + + type index = (string,localizable) Hashtbl.t + + let kid = ref 0 + + let index () = Hashtbl.create 0 + + module TYPE : Datatype.S with type t = index = + Datatype.Make + (struct + type t = index + include Datatype.Undefined + let reprs = [index()] + let name = "Server.Jprinter.Index" + let mem_project = Datatype.never_any_project + end) + + module STATE = State_builder.Ref(TYPE) + (struct + let name = "Server.Jprinter.State" + let dependencies = [] + let default = index + end) + + let of_stmt s = Printf.sprintf "#s%d" s.sid + let of_start s = Printf.sprintf "#k%d" s.sid + let of_varinfo v = Printf.sprintf "#v%d" v.vid + + let create_tag = function + | PStmt(_,st) -> of_stmt st + | PStmtStart(_,st) -> of_start st + | PVDecl(_,_,vi) -> of_varinfo vi + | PLval _ -> Printf.sprintf "#l%d" (incr kid ; !kid) + | PExp _ -> Printf.sprintf "#e%d" (incr kid ; !kid) + | PTermLval _ -> Printf.sprintf "#t%d" (incr kid ; !kid) + | PGlobal _ -> Printf.sprintf "#g%d" (incr kid ; !kid) + | PIP _ -> Printf.sprintf "#p%d" (incr kid ; !kid) + + let create item = + let tag = create_tag item in + let index = STATE.get () in + Hashtbl.add index tag item ; tag + + let lookup = Hashtbl.find (STATE.get()) + +end + +module PP = Printer_tag.Make(Tag) + +(* -------------------------------------------------------------------------- *) +(* --- Ast Data --- *) +(* -------------------------------------------------------------------------- *) + +module Stmt = Data.Collection + (struct + type t = stmt + let syntax = Sy.publish ~page ~name:"stmt" + ~synopsis:Sy.ident + ~descr:(Md.rm "Code statement identifier") () + let to_json st = `String (Tag.of_stmt st) + let of_json js = + try + let open Printer_tag in + match Tag.lookup (Js.to_string js) with + | PStmt(_,st) -> st + | _ -> raise Not_found + with Not_found -> + Data.failure js "Unknown stmt id" + end) + +module Ki = Data.Collection + (struct + type t = kinstr + let syntax = Sy.union [ Sy.tag "global" ; Stmt.syntax ] + let to_json = function + | Kglobal -> `String "global" + | Kstmt st -> `String (Tag.of_stmt st) + let of_json = function + | `String "global" -> Kglobal + | js -> Kstmt (Stmt.of_json js) + end) + +module Kf = Data.Collection + (struct + type t = kernel_function + let syntax = Sy.publish ~page ~name:"fct-id" + ~synopsis:Sy.ident + ~descr:(Md.rm "Function identified by its global name.") () + let to_json kf = + `String (Kernel_function.get_name kf) + let of_json js = + try Js.to_string js |> Globals.Functions.find_by_name + with Not_found -> Data.failure js "Undefined function" + end) + +(* -------------------------------------------------------------------------- *) +(* --- Functions --- *) +(* -------------------------------------------------------------------------- *) + +let () = Request.register ~page + ~kind:`GET ~name:"kernel.ast.getFunctions" + ~descr:(Md.rm "Collect all functions in the AST") + ~input:(module Junit) ~output:(module Kf.Jlist) + begin fun () -> + let pool = ref [] in + Globals.Functions.iter (fun kf -> pool := kf :: !pool) ; + List.rev !pool + end + +let () = Request.register ~page + ~kind:`GET ~name:"kernel.ast.printFunction" + ~descr:(Md.rm "Print the AST of a function") + ~input:(module Kf) ~output:(module Jtext) + (fun kf -> Jbuffer.to_json PP.pp_global (Kernel_function.get_global kf)) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_ast.mli b/src/plugins/server/kernel_ast.mli new file mode 100644 index 0000000000000000000000000000000000000000..0b448e0e0bb89b4bd6ff46c94080022a91893f94 --- /dev/null +++ b/src/plugins/server/kernel_ast.mli @@ -0,0 +1,34 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +open Cil_types + +(* -------------------------------------------------------------------------- *) +(** Ast Data *) +(* -------------------------------------------------------------------------- *) + +module PP : Printer_tag.S_pp +module Kf : Data.S_collection with type t = kernel_function +module Ki : Data.S_collection with type t = kinstr +module Stmt : Data.S_collection with type t = stmt + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_main.ml b/src/plugins/server/kernel_main.ml new file mode 100644 index 0000000000000000000000000000000000000000..4cfd33596d14017a5ea0e0584435616387d44b84 --- /dev/null +++ b/src/plugins/server/kernel_main.ml @@ -0,0 +1,215 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +open Data +module Sy = Syntax +module Md = Markdown +module Senv = Server_parameters + +(* -------------------------------------------------------------------------- *) +(* --- Frama-C Kernel Services --- *) +(* -------------------------------------------------------------------------- *) + +let page = Doc.page `Kernel ~title:"Kernel Services" ~filename:"kernel.md" + +(* -------------------------------------------------------------------------- *) +(* --- Config --- *) +(* -------------------------------------------------------------------------- *) + +let () = + let get_config = Request.signature + ~page ~kind:`GET ~name:"kernel.getConfig" + ~descr:(Md.rm "Frama-C Kernel configuration") + ~input:(module Junit) () in + let result name descr = + Request.result get_config ~name ~descr:(Md.rm descr) (module Jstring) in + let set_version = result "version" "Frama-C version" in + let set_datadir = result "datadir" "Shared directory (FRAMAC_SHARE)" in + let set_libdir = result "libdir" "Lib directory (FRAMAC_LIB)" in + let set_pluginpath = Request.result get_config + ~name:"pluginpath" ~descr:(Md.rm "Plugin directories (FRAMAC_PLUGIN)") + (module Jstring.Jlist) in + Request.register_sig get_config + begin fun rq () -> + set_version rq Config.version ; + set_datadir rq Config.datadir ; + set_libdir rq Config.libdir ; + set_pluginpath rq Config.plugin_dir ; + end + +(* -------------------------------------------------------------------------- *) +(* --- File Positions --- *) +(* -------------------------------------------------------------------------- *) + +module RawSource = +struct + type t = Filepath.position + let syntax = Sy.publish ~page ~name:"source" + ~synopsis:(Sy.record [ "file" , Sy.string ; "line" , Sy.int ]) + ~descr:(Md.rm "Source file positions.") + ~details:(Md.praw "The file path is normalized, \ + and the line number starts at one.") () + + let to_json p = `Assoc [ + "file" , `String (p.Filepath.pos_path :> string) ; + "line" , `Int p.Filepath.pos_lnum ; + ] + + let of_json = function + | `Assoc [ "file" , `String path ; "line" , `Int line ] + | `Assoc [ "line" , `Int line ; "file" , `String path ] + -> Log.source ~file:(Filepath.Normalized.of_string path) ~line + | js -> failure js "Invalid source format" + +end + +module LogSource = Collection(RawSource) + +(* -------------------------------------------------------------------------- *) +(* --- Log Lind --- *) +(* -------------------------------------------------------------------------- *) + +module RawKind = +struct + type t = Log.kind + let page = page + let name = "kind" + let descr = Md.rm "Frama-C message category." + let values = [ + Log.Error, "ERROR", Md.rm "User Error" ; + Log.Warning, "WARNING", Md.rm "User Warning" ; + Log.Feedback, "FEEDBACK", Md.rm "Analyzer Feedback" ; + Log.Result, "RESULT", Md.rm "Analyzer Result" ; + Log.Failure, "FAILURE", Md.rm "Analyzer Failure" ; + Log.Debug, "DEBUG", Md.rm "Analyser Debug" ; + ] +end + +module LogKind = Dictionary(RawKind) + +(* -------------------------------------------------------------------------- *) +(* --- Log Events --- *) +(* -------------------------------------------------------------------------- *) + +module RawEvent = +struct + + module R = Record + (struct + let page = page + let name = "log" + let descr = Md.rm "Message event record." + end) + + let syntax = R.syntax + + let descr = Md.rm + let kind = R.field "kind" ~descr:(descr "Message kind") (module LogKind) + let plugin = R.field "plugin" ~descr:(descr "Emitter plugin") (module Jstring) + let message = R.field "message" ~descr:(descr "Message text") (module Jstring) + + let category = R.option "category" + ~descr:(descr "Message category (DEBUG or WARNING)") + (module Jstring) + + let source = R.option "source" ~descr:(descr "Source file position") + (module LogSource) + + type t = Log.event + + let to_json evt = + R.default () |> + R.set plugin evt.Log.evt_plugin |> + R.set kind evt.Log.evt_kind |> + R.set category evt.Log.evt_category |> + R.set source evt.Log.evt_source |> + R.set message evt.Log.evt_message |> + R.to_json + + let of_json js = + let r = R.of_json js in + { + Log.evt_plugin = R.get plugin r ; + Log.evt_kind = R.get kind r ; + Log.evt_category = R.get category r ; + Log.evt_source = R.get source r ; + Log.evt_message = R.get message r ; + } + +end + +module LogEvent = Collection(RawEvent) + +(* -------------------------------------------------------------------------- *) +(* --- Log Monitoring --- *) +(* -------------------------------------------------------------------------- *) + +let monitoring = ref false +let monitored = ref false +let events : Log.event Queue.t = Queue.create () + +let monitor flag = + if flag != !monitoring then + ( if flag then + Senv.feedback "Start logs monitoring." + else + Senv.feedback "Stop logs monitoring." ) ; + monitoring := flag ; + if !monitoring && not !monitored then + begin + monitored := true ; + Log.add_listener (fun evt -> if !monitoring then Queue.add evt events) + end + +let monitor_logs () = monitor (Senv.Log.get ()) + +let monitor_server activity = + if activity then monitor true else monitor_logs () + +let () = + Main.on monitor_server ; + Cmdline.run_after_configuring_stage monitor_logs + +(* -------------------------------------------------------------------------- *) +(* --- Log Requests --- *) +(* -------------------------------------------------------------------------- *) + +let () = Request.register + ~page ~kind:`SET ~name:"kernel.setLogs" + ~descr:(Md.rm "Turn logs monitoring on/off") + ~input:(module Jbool) ~output:(module Junit) monitor + +let () = Request.register + ~page ~kind:`GET ~name:"kernel.getLogs" + ~descr:(Md.rm "Flush the last emitted logs since last call (max 100)") + ~input:(module Junit) ~output:(module LogEvent.Jlist) + begin fun () -> + let pool = ref [] in + let count = ref 100 in + while not (Queue.is_empty events) && !count > 0 do + decr count ; + pool := Queue.pop events :: !pool + done ; + List.rev !pool + end + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_main.mli b/src/plugins/server/kernel_main.mli new file mode 100644 index 0000000000000000000000000000000000000000..726d654e626ac7c69a35f08b97cc75ca6f60a33c --- /dev/null +++ b/src/plugins/server/kernel_main.mli @@ -0,0 +1,30 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** Kernel Services *) +(* -------------------------------------------------------------------------- *) + +module LogSource : Data.S_collection with type t = Filepath.position +module LogEvent : Data.S_collection with type t = Log.event + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_project.ml b/src/plugins/server/kernel_project.ml new file mode 100644 index 0000000000000000000000000000000000000000..883cea34f37b7ec3b8725d2c62282b14c07d22d9 --- /dev/null +++ b/src/plugins/server/kernel_project.ml @@ -0,0 +1,123 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +open Data +module Sy = Syntax +module Md = Markdown +module Js = Yojson.Basic.Util + +let page = Doc.page `Kernel ~title:"Project Management" ~filename:"project.md" + +(* -------------------------------------------------------------------------- *) +(* --- Project Info --- *) +(* -------------------------------------------------------------------------- *) + +module ProjectInfo = + Collection + (struct + type t = Project.t + + let syntax = Sy.publish ~page ~name:"project-info" + ~descr:(Md.rm "Project informations") + ~synopsis:Sy.(record[ "id",ident; "name",string; "current",boolean ]) + () + + let of_json js = + Js.member "id" js |> Js.to_string |> Project.from_unique_name + + let to_json p = + `Assoc [ + "id", `String (Project.get_unique_name p) ; + "name", `String (Project.get_name p) ; + "current", `Bool (Project.is_current p) ; + ] + end) + +(* -------------------------------------------------------------------------- *) +(* --- Project Requests --- *) +(* -------------------------------------------------------------------------- *) + +module ProjectRequest = +struct + + type t = Project.t * string * json + + let syntax = Sy.publish ~page ~name:"project-request" + ~synopsis:(Sy.(record[ "project",ident; "request",string; "data",any; ])) + ~descr:(Md.rm "Request to be executed on the specified project.") () + + let of_json js = + begin + Project.from_unique_name Js.(member "project" js |> to_string) , + Js.(member "request" js |> to_string) , + Js.(member "data" js) + end + + let process kind (project,request,data) = + match Main.find request with + | Some(kd,handler) when kd = kind -> Project.on project handler data + | Some _ -> failwith (Printf.sprintf "Incompatible kind for '%s'" request) + | None -> failwith (Printf.sprintf "Request '%s' undefined" request) + +end + +(* -------------------------------------------------------------------------- *) +(* --- Project Requests --- *) +(* -------------------------------------------------------------------------- *) + +let () = Request.register ~page + ~kind:`GET ~name:"kernel.project.getCurrent" + ~descr:(Md.rm "Returns the current project") + ~input:(module Junit) ~output:(module ProjectInfo) + Project.current + +let () = Request.register ~page + ~kind:`SET ~name:"kernel.project.setCurrent" + ~descr:(Md.rm "Switches the current project") + ~input:(module Jident) ~output:(module Junit) + (fun pid -> Project.(set_current (from_unique_name pid))) + +let () = Request.register ~page + ~kind:`GET ~name:"kernel.project.getList" + ~descr:(Md.rm "Returns the list of all projects") + ~input:(module Junit) ~output:(module ProjectInfo.Jlist) + (fun () -> Project.fold_on_projects (fun ids p -> p :: ids) []) + +let () = Request.register ~page + ~kind:`GET ~name:"kernel.project.getOn" + ~descr:(Md.rm "Execute a GET request within the given project") + ~input:(module ProjectRequest) ~output:(module Jany) + (ProjectRequest.process `GET) + +let () = Request.register ~page + ~kind:`SET ~name:"kernel.project.setOn" + ~descr:(Md.rm "Execute a SET request within the given project") + ~input:(module ProjectRequest) ~output:(module Jany) + (ProjectRequest.process `SET) + +let () = Request.register ~page + ~kind:`EXEC ~name:"kernel.project.execOn" + ~descr:(Md.rm "Execute an EXEC request within the given project") + ~input:(module ProjectRequest) ~output:(module Jany) + (ProjectRequest.process `EXEC) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/kernel_project.mli b/src/plugins/server/kernel_project.mli new file mode 100644 index 0000000000000000000000000000000000000000..975e1e094f80f095534935294154b22ef3d95979 --- /dev/null +++ b/src/plugins/server/kernel_project.mli @@ -0,0 +1,32 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +open Data + +(* -------------------------------------------------------------------------- *) +(** Project Services *) +(* -------------------------------------------------------------------------- *) + +module ProjectInfo : Data.S with type t = Project.t +module ProjectRequest : Request.Input with type t = Project.t * string * json + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/main.ml b/src/plugins/server/main.ml new file mode 100644 index 0000000000000000000000000000000000000000..2602e89fe5f4b7cef908529ccf56db56f41dc3ad --- /dev/null +++ b/src/plugins/server/main.ml @@ -0,0 +1,300 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Server Main Process --- *) +(* -------------------------------------------------------------------------- *) + +module Senv = Server_parameters + +let option f = function None -> () | Some x -> f x + +(* -------------------------------------------------------------------------- *) +(* --- Registry --- *) +(* -------------------------------------------------------------------------- *) + +type kind = [ `GET | `SET | `EXEC ] +let string_of_kind = function `GET -> "GET" | `SET -> "SET" | `EXEC -> "EXEC" +let pp_kind fmt kd = Format.pp_print_string fmt (string_of_kind kd) + +let registry = Hashtbl.create 32 + +let register (kind : kind) request handler = + if Hashtbl.mem registry request then + Server_parameters.failure "Request '%s' already registered" request + else + Hashtbl.add registry request (kind,handler) + +let find request = + try Some (Hashtbl.find registry request) + with Not_found -> None + +let exec request data = (snd (Hashtbl.find registry request)) data + +(* -------------------------------------------------------------------------- *) +(* --- Public API --- *) +(* -------------------------------------------------------------------------- *) + +type json = Json.t + +type 'a request = [ + | `Poll + | `Request of 'a * string * json + | `Kill of 'a + | `Shutdown +] + +type 'a response = [ + | `Data of 'a * json + | `Error of 'a * string + | `Killed of 'a + | `Rejected of 'a +] + +type 'a message = { + requests : 'a request list ; + callback : 'a response list -> unit ; +} + +(* Private API: *) + +type 'a exec = { + id : 'a ; + request : string ; + data : json ; + handler : json -> json ; + yield : bool ; + mutable killed : bool ; +} + +type 'a server = { + rate : int ; + pretty : Format.formatter -> 'a -> unit ; + equal : 'a -> 'a -> bool ; + fetch : unit -> 'a message option ; + q_in : 'a exec Queue.t ; + q_out : 'a response Stack.t ; + mutable shutdown : bool ; + mutable coins : int ; + mutable running : 'a exec option ; +} + +exception Killed + +(* -------------------------------------------------------------------------- *) +(* --- Debug --- *) +(* -------------------------------------------------------------------------- *) + +let pp_request pp fmt (r : _ request) = + match r with + | `Poll -> Format.fprintf fmt "Poll" + | `Shutdown -> Format.fprintf fmt "Shutdown" + | `Kill id -> Format.fprintf fmt "Kill %a" pp id + | `Request(id,request,data) -> + if Senv.debug_atleast 2 then + Format.fprintf fmt "@[<hov 2>Request %s:%a@ %a@]" + request pp id Data.pretty data + else + Format.fprintf fmt "Request %s:%a" request pp id + +let pp_response pp fmt (r : _ response) = + match r with + | `Error(id,err) -> Format.fprintf fmt "Error %a: %s" pp id err + | `Rejected id -> Format.fprintf fmt "Rejected %a" pp id + | `Killed id -> Format.fprintf fmt "Killed %a" pp id + | `Data(id,data) -> + if Senv.debug_atleast 2 then + Format.fprintf fmt "@[<hov 2>Response %a@ %a@]" + pp id Data.pretty data + else + Format.fprintf fmt "Response %a" pp id + +(* -------------------------------------------------------------------------- *) +(* --- Request Handling --- *) +(* -------------------------------------------------------------------------- *) + +let no_yield () = () + +let execute yield exec : _ response = + let db = !Db.progress in + try + Db.progress := if exec.yield then yield else no_yield ; + let data = exec.handler exec.data in + Db.progress := db ; `Data(exec.id,data) + with + | Killed -> Db.progress := db ; `Killed exec.id + | exn -> + Db.progress := db ; + Senv.warning "[%s] Uncaught exception:@\n%s" + exec.request (Cmdline.protect exn) ; + `Error(exec.id,Printexc.to_string exn) + +let execute_debug pp yield exec = + if Senv.debug_atleast 1 then + Senv.debug "Trigger %s:%a" exec.request pp exec.id ; + execute yield exec + +let reply_debug server resp = + if Senv.debug_atleast 1 then + Senv.debug "%a" (pp_response server.pretty) resp ; + Stack.push resp server.q_out + +(* -------------------------------------------------------------------------- *) +(* --- Processing Requests --- *) +(* -------------------------------------------------------------------------- *) + +let raise_if_killed = function { killed } -> if killed then raise Killed +let kill_exec e = e.killed <- true +let kill_request eq id e = if eq id e.id then e.killed <- true + +let process_request (server : 'a server) (request : 'a request) : unit = + if Senv.debug_atleast 1 then + Senv.debug "%a" (pp_request server.pretty) request ; + match request with + | `Poll -> () + | `Shutdown -> + begin + option kill_exec server.running ; + Queue.clear server.q_in ; + Stack.clear server.q_out ; + server.shutdown <- true ; + end + | `Kill id -> + begin + let kill = kill_request server.equal id in + Queue.iter kill server.q_in ; + option kill server.running ; + end + | `Request(id,request,data) -> + begin + match find request with + | None -> reply_debug server (`Rejected id) + | Some( `GET , handler ) -> + let exec = { id ; request ; handler ; data ; + yield = false ; killed = false } in + reply_debug server (execute no_yield exec) + | Some( `SET , handler ) -> + let exec = { id ; request ; handler ; data ; + yield = false ; killed = false } in + Queue.push exec server.q_in + | Some( `EXEC , handler ) -> + let exec = { id ; request ; handler ; data ; + yield = true ; killed = false } in + Queue.push exec server.q_in + end + +(* -------------------------------------------------------------------------- *) +(* --- Fetching a Bunck of Messages --- *) +(* -------------------------------------------------------------------------- *) + +let communicate server = + match server.fetch () with + | None -> false + | Some message -> + let error = + try List.iter (process_request server) message.requests ; None + with exn -> Some exn in (* re-raised after message reply *) + let pool = ref [] in + Stack.iter (fun r -> pool := r :: !pool) server.q_out ; + Stack.clear server.q_out ; + message.callback !pool ; + option raise error ; true + +(* -------------------------------------------------------------------------- *) +(* --- Yielding --- *) +(* -------------------------------------------------------------------------- *) + +let do_yield server () = + begin + option raise_if_killed server.running ; + let n = server.coins in + if n < server.rate then + server.coins <- succ n + else + ( server.coins <- 0 ; ignore ( communicate server ) ) ; + end + +(* -------------------------------------------------------------------------- *) +(* --- One Step Process --- *) +(* -------------------------------------------------------------------------- *) + +let rec fetch_exec q = + if Queue.is_empty q then None + else + let e = Queue.pop q in + if e.killed then fetch_exec q else Some e + +let process server = + match fetch_exec server.q_in with + | None -> communicate server + | Some exec -> + server.running <- Some exec ; + try + reply_debug server (execute_debug server.pretty (do_yield server) exec) ; + server.running <- None ; + true + with exn -> + server.running <- None ; + raise exn + +(* -------------------------------------------------------------------------- *) +(* --- Server Main Loop --- *) +(* -------------------------------------------------------------------------- *) + +let in_range ~min:a ~max:b v = min (max a v) b + +let kill () = raise Killed +let yield () = !Db.progress () + +let demons = ref [] +let on callback = demons := !demons @ [ callback ] +let signal activity = + List.iter (fun f -> try f activity with _ -> ()) !demons + +let run ~pretty ?(equal=(=)) ~fetch () = + begin + let rate = in_range ~min:1 ~max:200 (Senv.Rate.get ()) in + let idle_ms = in_range ~min:1 ~max:2000 (Senv.Idle.get ()) in + let idle_s = float_of_int idle_ms /. 1000.0 in + let server = { + fetch ; coins = 0 ; rate ; equal ; pretty ; + q_in = Queue.create () ; + q_out = Stack.create () ; + running = None ; + shutdown = false ; + } in + try + signal true ; + Senv.feedback "Server running." ; + while not server.shutdown do + let activity = process server in + if not activity then Unix.sleepf idle_s ; + done ; + Senv.feedback "Server shutdown." ; + signal false ; + with err -> + Senv.feedback "Server interruped (fatal error)." ; + signal false ; + raise err + end + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/main.mli b/src/plugins/server/main.mli new file mode 100644 index 0000000000000000000000000000000000000000..721291d96e9b93c4d65676fb83b216fafba39bd7 --- /dev/null +++ b/src/plugins/server/main.mli @@ -0,0 +1,96 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** Server Main Process *) +(* -------------------------------------------------------------------------- *) + +type json = Json.t +type kind = [ `GET | `SET | `EXEC ] +val string_of_kind : kind -> string +val pp_kind : Format.formatter -> kind -> unit + +(* -------------------------------------------------------------------------- *) +(** {2 Request Registry} *) +(* -------------------------------------------------------------------------- *) + +val register : kind -> string -> (json -> json) -> unit +val find : string -> (kind * (json -> json)) option +val exec : string -> json -> json (** @raises Not_found if not registered *) + +(* -------------------------------------------------------------------------- *) +(** {2 Server Main Process} *) +(* -------------------------------------------------------------------------- *) + +(** Type of request messages. + Parametrized by the type of request identifiers. *) +type 'a request = [ + | `Poll + | `Request of 'a * string * json + | `Kill of 'a + | `Shutdown +] + +(** Type of response messages. + Parametrized by the type of request identifiers. *) +type 'a response = [ + | `Data of 'a * json + | `Error of 'a * string + | `Killed of 'a + | `Rejected of 'a +] + +(** A paired request-response message. + The callback will be called exactly once for each received message. *) +type 'a message = { + requests : 'a request list ; + callback : 'a response list -> unit ; +} + +(** + Run a server with the provided low-level network primitives to + actually exchange data. + + The function does not return until the server is explicitely + Shutdown. Logs are monitored unless [~logs:false] is specified. + + Default equality is the standard `(=)` one. +*) +val run : + pretty:(Format.formatter -> 'a -> unit) -> + ?equal:('a -> 'a -> bool) -> + fetch:(unit -> 'a message option) -> + unit -> unit + +(** Yield the server during the currently running request. + Actually, calls [!Db.progress()]. *) +val yield : unit -> unit + +(** Kills the currently running request. Actually raises an exception. *) +val kill : unit -> 'a + +(** Register a callback to listen for server activity. + All callbacks would be executed in their order of registration. + They shall {i never} raise any exception. *) +val on : (bool -> unit) -> unit + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/request.ml b/src/plugins/server/request.ml new file mode 100644 index 0000000000000000000000000000000000000000..b355b3907879b1e91eef4345b217d39680fb98bc --- /dev/null +++ b/src/plugins/server/request.ml @@ -0,0 +1,331 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +module Senv = Server_parameters +module Jutil = Yojson.Basic.Util + +(* -------------------------------------------------------------------------- *) +(* --- Request Registry --- *) +(* -------------------------------------------------------------------------- *) + +type json = Data.json +type kind = [ `GET | `SET | `EXEC ] + +module type Input = +sig + type t + val syntax : Syntax.t + val of_json : json -> t +end + +module type Output = +sig + type t + val syntax : Syntax.t + val to_json : t -> json +end + +type 'a input = (module Input with type t = 'a) +type 'a output = (module Output with type t = 'a) + +(* -------------------------------------------------------------------------- *) +(* --- Sanity Checks --- *) +(* -------------------------------------------------------------------------- *) + +let re_set = Str.regexp_string_case_fold "SET" +let re_get = Str.regexp_case_fold "\\(GET\\|PRINT\\)" +let re_exec = Str.regexp_case_fold "\\(EXEC\\|COMPUTE\\)" +let re_name = Str.regexp_case_fold "[a-zA-Z0-9.]+$" + +let wpage = Senv.register_warn_category "inconsistent-page" +let wkind = Senv.register_warn_category "inconsistent-kind" + +let check_name name = + if not (Str.string_match re_name name 0) then + Senv.warning ~wkey:Senv.wname + "Request %S is not a dot-separated list of (camlCased) identifiers" name + +let check_plugin plugin name = + let p = String.lowercase_ascii plugin in + let n = String.lowercase_ascii name in + let k = String.length plugin in + if not (String.length name > k && + String.sub n 0 k = p && + String.get n k = '.') + then + Senv.warning ~wkey:wpage + "Request '%s' shall be named « %s.* »" + name (String.capitalize_ascii plugin) + +let check_page page name = + match Doc.chapter page with + | `Kernel -> check_plugin "kernel" name + | `Plugin plugin -> check_plugin plugin name + | `Protocol -> + Senv.warning ~wkey:wkind + "Request '%s' shall not be published in protocol pages" name + +let check_kind kind name = + let re,key = match kind with + | `GET -> re_get , "get|print" + | `SET -> re_set , "set" + | `EXEC -> re_exec , "exec|compute" + in try ignore (Str.search_forward re name 0) with Not_found -> + Senv.warning "Request '%s' shall be named with « %s »" + name key + +(* -------------------------------------------------------------------------- *) +(* --- Multiple Fields Requests --- *) +(* -------------------------------------------------------------------------- *) + +module Fmap = Map.Make(String) + +type rq = { + mutable param : json Fmap.t ; + mutable result : json Fmap.t ; +} + +let fmap_of_json r js = + List.fold_left + (fun r (fd,js) -> Fmap.add fd js r) + r (Jutil.to_assoc js) + +let fmap_to_json r = + `Assoc (Fmap.fold (fun fd js r -> (fd,js)::r) r []) + +type 'a param = rq -> 'a +type 'a result = rq -> 'a -> unit + +(* -------------------------------------------------------------------------- *) +(* --- Input/Output Request Processing --- *) +(* -------------------------------------------------------------------------- *) + +type _ rq_input = + | Pnone + | Pdata : 'a input -> 'a rq_input + | Pfields : Syntax.field list -> unit rq_input + +type _ rq_output = + | Rnone + | Rdata : 'a output -> 'a rq_output + | Rfields : Syntax.field list -> unit rq_output + +(* json input syntax *) +let sy_input (type a) (input : a rq_input) : Syntax.t = + match input with + | Pnone -> assert false + | Pdata d -> let module D = (val d) in D.syntax + | Pfields _ -> Syntax.record [] + +(* json output syntax *) +let sy_output (type b) (output : b rq_output) : Syntax.t = + match output with + | Rnone -> assert false + | Rdata d -> let module D = (val d) in D.syntax + | Rfields _ -> Syntax.record [] + +(* json input documentation *) +let doc_input (type a) (input : a rq_input) : Markdown.block = + match input with + | Pnone -> assert false + | Pdata _ -> Markdown.empty + | Pfields fs -> Syntax.fields ~title:"Input" (List.rev fs) + +(* json output syntax *) +let doc_output (type b) (output : b rq_output) : Markdown.block = + match output with + | Rnone -> assert false + | Rdata _ -> Markdown.empty + | Rfields fs -> Syntax.fields ~title:"Output" (List.rev fs) + +(* -------------------------------------------------------------------------- *) +(* --- Multi-Parameters Requests --- *) +(* -------------------------------------------------------------------------- *) + +type ('a,'b) signature = { + page : Doc.page ; + kind : kind ; + name : string ; + descr : Markdown.text ; + details : Markdown.block ; + mutable defined : bool ; + mutable defaults : json Fmap.t ; + mutable required : string list ; + mutable input : 'a rq_input ; + mutable output : 'b rq_output ; +} + +let failure_missing fmap name = + Data.failure (fmap_to_json fmap) "Missing parameter '%s'" name + +let check_required fmap fd = + if not (Fmap.mem fd fmap) then failure_missing fmap fd + +(* -------------------------------------------------------------------------- *) +(* --- Named Input Parameters Definitions --- *) +(* -------------------------------------------------------------------------- *) + +(* current input fields *) +let fds_input s : Syntax.field list = + if s.defined then Senv.failure "Request '%s' has been finalized." s.name ; + match s.input with + | Pdata _ -> + Senv.fatal "Can not define named parameters for request '%s'" s.name + | Pnone -> [] + | Pfields fds -> fds + +let param (type a b) (s : (unit,b) signature) ~name ~descr + ?default (input : a input) : a param = + let module D = (val input) in + let syntax = if default = None then D.syntax else Syntax.option D.syntax in + let fd = Syntax.{ name ; syntax ; descr } in + s.input <- Pfields (fd :: fds_input s) ; + fun rq -> + try D.of_json (Fmap.find name rq.param) + with Not_found -> + match default with + | None -> failure_missing rq.param name + | Some v -> v + +let param_opt (type a b) (s : (unit,b) signature) ~name ~descr + (input : a input) : a option param = + let module D = (val input) in + let fd = Syntax.{ name ; syntax = Syntax.option D.syntax ; descr } in + s.input <- Pfields (fd :: fds_input s) ; + fun rq -> + try Some(D.of_json (Fmap.find name rq.param)) + with Not_found -> None + +(* -------------------------------------------------------------------------- *) +(* --- Named Output Parameters Definitions --- *) +(* -------------------------------------------------------------------------- *) + +(* current output fields *) +let fds_output s : Syntax.field list = + if s.defined then Senv.failure "Request '%s' has been finalized." s.name ; + match s.output with + | Rdata _ -> Senv.fatal "Can not define named results request '%s'" s.name + | Rnone -> [] + | Rfields fds -> fds + +let result (type a b) (s : (a,unit) signature) ~name ~descr + ?default (output : b output) : b result = + let module D = (val output) in + let fd = Syntax.{ name ; syntax = D.syntax ; descr } in + s.output <- Rfields (fd :: fds_output s) ; + begin + match default with + | None -> s.required <- name :: s.required + | Some v -> s.defaults <- Fmap.add name (D.to_json v) s.defaults + end ; + fun rq v -> rq.result <- Fmap.add name (D.to_json v) rq.result + +let result_opt (type a b) (s : (a,unit) signature) ~name ~descr + (output : b output) : b option result = + let module D = (val output) in + let fd = Syntax.{ name ; syntax = option D.syntax ; descr } in + s.output <- Rfields (fd :: fds_output s) ; + fun rq opt -> + match opt with None -> () | Some v -> + rq.result <- Fmap.add name (D.to_json v) rq.result + +(* -------------------------------------------------------------------------- *) +(* --- Opened Signature Definition --- *) +(* -------------------------------------------------------------------------- *) + +let signature + ~page ~kind ~name ~descr ?(details=Markdown.empty) + ?input ?output () = + check_name name ; + check_page page name ; + check_kind kind name ; + let input = match input with None -> Pnone | Some d -> Pdata d in + let output = match output with None -> Rnone | Some d -> Rdata d in + { + page ; kind ; name ; descr ; details ; + defaults = Fmap.empty ; required = [] ; + input ; output ; defined = false ; + } + +(* -------------------------------------------------------------------------- *) +(* --- Opened Signature Process --- *) +(* -------------------------------------------------------------------------- *) + +(* json input processing *) +let mk_input (type a) name defaults (input : a rq_input) : (rq -> json -> a) = + match input with + | Pnone -> Senv.fatal "No input defined for request '%s'" name + | Pdata d -> + let module D = (val d) in + (fun rq js -> rq.result <- defaults ; D.of_json js) + | Pfields _ -> + (fun rq js -> rq.param <- fmap_of_json rq.param js) + +(* json output processing *) +let mk_output (type b) name required (output : b rq_output) : (rq -> b -> json) = + match output with + | Rnone -> Senv.fatal "No output defined for request '%s'" name + | Rdata d -> + let module D = (val d) in (fun _rq v -> D.to_json v) + | Rfields _ -> + (fun rq () -> + List.iter (check_required rq.result) required ; + fmap_to_json rq.result) + +let register_sig (type a b) (s : (a,b) signature) (process : rq -> a -> b) = + if s.defined then + Senv.fatal "Request '%s' is defined twice" s.name ; + let input = mk_input s.name s.defaults s.input in + let output = mk_output s.name s.required s.output in + let processor js = + let rq = { param = Fmap.empty ; result = Fmap.empty } in + js |> input rq |> process rq |> output rq + in + let skind = Main.string_of_kind s.kind in + let title = Printf.sprintf "`%s` %s" skind s.name in + let synopsis = + Markdown.table + [`Center "Input" ; `Center "Output" ] + [[ Syntax.format @@ sy_input s.input ; + Syntax.format @@ sy_output s.output ]] in + let content = + Markdown.concat [ + Markdown.par s.descr ; + synopsis ; + s.details ; + doc_input s.input ; + doc_output s.output ; + ] in + let _ = Doc.publish ~page:s.page ~name:s.name ~title content [] in + Main.register s.kind s.name processor ; + s.defined <- true + +(* -------------------------------------------------------------------------- *) +(* --- Request Registration --- *) +(* -------------------------------------------------------------------------- *) + +let register ~page ~kind ~name ~descr ?details ~input ~output process = + register_sig + (signature ~page ~kind ~name ~descr ?details ~input ~output ()) + (fun _rq v -> process v) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/request.mli b/src/plugins/server/request.mli new file mode 100644 index 0000000000000000000000000000000000000000..c855405cb85088b1d88e60660a5a6e86e268493e --- /dev/null +++ b/src/plugins/server/request.mli @@ -0,0 +1,195 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** Request Registry *) +(* -------------------------------------------------------------------------- *) + +type json = Data.json +type kind = [ `GET | `SET | `EXEC ] + +module type Input = +sig + type t + val syntax : Syntax.t + val of_json : json -> t +end + +module type Output = +sig + type t + val syntax : Syntax.t + val to_json : t -> json +end + +type 'a input = (module Input with type t = 'a) +type 'b output = (module Output with type t = 'b) + +(** {2 Simple Requests Registration} *) + +(** Register a simple request of type [(a -> b)]. + + Name, page and kind must be consistent with each others: + - No publication on [`Protocol] pages + - Kernel requests shall starts with ["Kernel.*"] + - Plugin requests shall starts with ["<Plugin>.*"] + - SET requests must contain ["set"] (case insensitive) + - GET requests must contain ["get"] or ["print"] (case insensitive) + - EXEC requests must contain ["exec"] or ["compute"] (case insensitive) + +*) +val register : + page:Doc.page -> + kind:kind -> + name:string -> + descr:Markdown.text -> + ?details:Markdown.block -> + input:'a input -> + output:'b output -> + ('a -> 'b) -> unit + +(** {2 Requests with Named Parameters} + + The API below allows for creating requests with + named and optional parameters. Although such requests + could be defined with simple registration and {i record} datatypes, + the helpers below allow more flexibility and a better correspondance + between optional parameters and OCaml option types. + + To register a request with named parameters and/or named results, + you first create a {i signature}. Then you define named + parameters and results, and finally you {i register} the processing + function: + + {[ + (* ---- Exemple of Request Registration --- *) + let () = + let s = Request.signature ~page ~kind ~name ~descr () in + let get_a = Request.param s ~name:"a" ~descr:"..." (module A) in + let get_b = Request.param s ~name:"b" ~descr:"..." (module B) in + let set_c = Request.result s ~name:"c" ~descr:"..." (module C) in + let set_d = Request.result s ~name:"d" ~descr:"..." (module D) in + Request.register_sig s + (fun rq () -> + let (c,d) = some_job (get_a rq) (get_b rq) in + set_c rq c ; set_d rq d) + ]} + +*) + +(** Under definition request signature. *) +type ('a,'b) signature + +(** Create an opened request signature. + Depending on whether [~input] and [~output] datatype are provided, + you shall define named parameters and results before registering the + request processing function. *) +val signature : + page:Doc.page -> + kind:kind -> + name:string -> + descr:Markdown.text -> + ?details:Markdown.block -> + ?input:'a input -> + ?output:'b output -> + unit -> ('a,'b) signature + +(** Request JSON parameters. *) +type rq + +(** Named input parameter. *) +type 'a param = rq -> 'a + +(** Named output parameter. *) +type 'b result = rq -> 'b -> unit + +(** Register the request JSON processing function. + This call finalize the signature definition and shall be called + once on the signature. *) +val register_sig : ('a,'b) signature -> (rq -> 'a -> 'b) -> unit + +(** {2 Named Parameters and Results} + + The functions bellow must be called on a freshly created signature + {i before} its final registration. The obtained getters and setters + shall be only used within the registered process. + + The correspondance between input/output JSON syntax and OCaml values + is summarized in the tables below.Abstract_domain + + For named input parameters: + [ + + API: Input JSON OCaml Getter + ----------------------------------------------------------------------- + Request.param { f: a } 'a (* might raise an exception *) + Request.param ~default { f: a? } 'a (* defined by default *) + Request.param_opt { f: a? } 'a option + + ] + + + For named output parameters: + [ + + API: Input JSON OCaml Setter + ---------------------------------------------------------------------- + Request.result { f: a } 'a (* shall be set by process *) + Request.result ~default { f: a } 'a (* defined by default *) + Request.result_opt { f: a? } 'a option + + ] + +*) + + +(** Named input parameter. If a default value is provided, + the JSON input field becomes optional. Otherwized, it is required. *) +val param : (unit,'b) signature -> + name:string -> + descr:Markdown.text -> + ?default:'a -> + 'a input -> 'a param + +(** Named optional input parameter. *) +val param_opt : (unit,'b) signature -> + name:string -> + descr:Markdown.text -> + 'a input -> 'a option param + +(** Named output parameter. If a default value is provided, + the JSON output field is initialized with it. + Otherwized, it shall be set at each invocation of the request processing + funciton. *) +val result : ('a,unit) signature -> + name:string -> + descr:Markdown.text -> + ?default:'b -> + 'b output -> 'b result + +(** Named optional output parameter. The initial value is set to [None]. *) +val result_opt : ('a,unit) signature -> + name:string -> + descr:Markdown.text -> + 'b output -> 'b option result + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/server_batch.ml b/src/plugins/server/server_batch.ml new file mode 100644 index 0000000000000000000000000000000000000000..0aa3c03b310a2c1104baf91a712a17bd17a9f01b --- /dev/null +++ b/src/plugins/server/server_batch.ml @@ -0,0 +1,106 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* Only Compiled when package Zmq is installed *) +(* No interface, registered via side-effects *) + +(* -------------------------------------------------------------------------- *) +(* --- ZeroMQ Server Options --- *) +(* -------------------------------------------------------------------------- *) + +module Senv = Server_parameters + +let batch_group = Senv.add_group "Protocol BATCH" + +let () = Parameter_customize.set_group batch_group +module Batch = Senv.String_list + (struct + let option_name = "-server-batch" + let arg_name = "file.json,..." + let help = + "Executes all requests in each <file.json>, and save the \ + associated results in <file.out.json>." + end) + +let _ = Doc.page `Protocol ~title:"Batch Protocol" ~filename:"server_batch.md" + +(* -------------------------------------------------------------------------- *) +(* --- Execute JSON --- *) +(* -------------------------------------------------------------------------- *) + +module Js = Yojson.Basic +module Ju = Yojson.Basic.Util + +let pretty = Js.pretty_print ~std:false + +let execute_command js = + let request = Ju.member "request" js |> Ju.to_string in + let id = Ju.member "id" js in + let data = Ju.member "data" js in + match Main.find request with + | None -> + Senv.error "[batch] %a: request %S not found" pretty id request ; + `Assoc [ "id" , id ; "error" , `String "request not found" ] + | Some (kind,handler) -> + try + Senv.feedback "[%a] %s" Main.pp_kind kind request ; + `Assoc [ "id" , id ; "data" , handler data ] + with Ju.Type_error(msg,js) -> + Senv.error "[%s] incorrect encoding:@\n%s@\n@[<hov 2>At: %a@]@." + request msg pretty js ; + `Assoc [ "id" , id ; "error" , `String msg ; "at" , js ] + +let rec execute_batch js = + match js with + | `Null -> `Null + | `List js -> `List (List.map execute_batch js) + | js -> + try execute_command js + with Ju.Type_error(msg,js) -> + Senv.error "[batch] incorrect encoding:@\n%s@\n@[<hov 2>At: %a@]@." + msg pretty js ; + `Null + +(* -------------------------------------------------------------------------- *) +(* --- Execute the Scripts --- *) +(* -------------------------------------------------------------------------- *) + +let execute () = + begin + List.iter + begin fun file -> + Senv.feedback "Script %S" file ; + let response = execute_batch (Js.from_file file) in + let output = Filename.remove_extension file ^ ".out.js" in + Senv.feedback "Output %S" output ; + Js.to_file output response ; + end + (Batch.get()) ; + end + +(* -------------------------------------------------------------------------- *) +(* --- Run the Server from the Command line --- *) +(* -------------------------------------------------------------------------- *) + +let () = Db.Main.extend execute + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/server_parameters.ml b/src/plugins/server/server_parameters.ml new file mode 100644 index 0000000000000000000000000000000000000000..76a572cdf0cbd164afcf0c948b5fec0f9f40e9a6 --- /dev/null +++ b/src/plugins/server/server_parameters.ml @@ -0,0 +1,70 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(* --- Server Plugin & Options --- *) +(* -------------------------------------------------------------------------- *) + +module P = Plugin.Register + (struct + let name = "Server" + let shortname = "server" + let help = "Frama-C Request Server" + end) + +include P + +module Idle = P.Int + (struct + let option_name = "-server-idle" + let arg_name = "ms" + let default = 10 + let help = "Waiting time (in milliseconds) when idle" + end) + +module Rate = P.Int + (struct + let option_name = "-server-rate" + let arg_name = "n" + let default = 100 + let help = "Number of analysis steps between server communications" + end) + +module Doc = P.String + (struct + let option_name = "-server-doc" + let arg_name = "dir" + let default = "" + let help = "Output a markdown documentation of the server in <dir>" + end) + +module Log = P.False + (struct + let option_name = "-server-logs" + let help = "Start (or stop) monitoring logs" + end) + +let wpage = register_warn_category "inconsistent-page" +let wkind = register_warn_category "inconsistent-kind" +let wname = register_warn_category "invalid-name" + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/server_parameters.mli b/src/plugins/server/server_parameters.mli new file mode 100644 index 0000000000000000000000000000000000000000..5dc5ad153438ddc07353cb1b93cc59f22d4e91de --- /dev/null +++ b/src/plugins/server/server_parameters.mli @@ -0,0 +1,36 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(** Server Plugin & Options *) + +include Plugin.General_services + +module Idle : Parameter_sig.Int (** Idle waiting time (in ms) *) +module Rate : Parameter_sig.Int (** Number of fetch per yield *) +module Doc : Parameter_sig.String (** Generate documentation *) +module Log : Parameter_sig.Bool (** Monitor logs *) + +val wpage : warn_category (** Inconsistent page warning *) +val wkind : warn_category (** Inconsistent category warning *) +val wname : warn_category (** Invalid name warning *) + +(**************************************************************************) diff --git a/src/plugins/server/server_zmq.ml b/src/plugins/server/server_zmq.ml new file mode 100644 index 0000000000000000000000000000000000000000..82d2ce4316762eb1f638f499c928eb15000d2442 --- /dev/null +++ b/src/plugins/server/server_zmq.ml @@ -0,0 +1,141 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* Only Compiled when package Zmq is installed *) +(* No interface, registered via side-effects *) + +(* -------------------------------------------------------------------------- *) +(* --- ZeroMQ Server Options --- *) +(* -------------------------------------------------------------------------- *) + +module Senv = Server_parameters + +let zmq_group = Senv.add_group "Protocol ZeroMQ" + +let () = Parameter_customize.set_group zmq_group +module Enabled = Senv.String + (struct + let option_name = "-server-zmq" + let arg_name = "url" + let default = "" + let help = "Establish a ZeroMQ server and listen for connections" + end) + +let _ = Doc.page `Protocol ~title:"ZeroMQ Protocol" ~filename:"server_zmq.md" + +(* -------------------------------------------------------------------------- *) +(* --- ZMQ Context --- *) +(* -------------------------------------------------------------------------- *) + +let context = + let zmq = ref None in + fun () -> + match !zmq with + | Some ctxt -> ctxt + | None -> + let major,minor,patch = Zmq.version () in + Senv.feedback "ZeroMQ %d.%d.%d" major minor patch ; + let ctxt = Zmq.Context.create () in + at_exit (fun () -> Zmq.Context.terminate ctxt) ; + zmq := Some ctxt ; ctxt + +(* -------------------------------------------------------------------------- *) +(* --- Decoding Requests --- *) +(* -------------------------------------------------------------------------- *) + +exception WrongEncoding of string + +let jdecode txt = + try Yojson.Basic.from_string txt + with exn -> + (* Exception if purely local from Yojson *) + raise (WrongEncoding (Printexc.to_string exn)) + +let jencode js = + try Yojson.Basic.to_string ~std:false js + with exn -> + (* Exception if purely local from Yojson *) + raise (WrongEncoding (Printexc.to_string exn)) + +let rec decode = function + | ("GET"|"SET"|"EXEC")::id::request::data :: w -> + `Request(id,request,jdecode data) :: decode w + | "KILL"::id:: w -> `Kill id :: decode w + | "POLL" :: w -> `Poll :: decode w + | "SHUTDOWN" :: _ -> [`Shutdown] + | cmd::_ -> raise (WrongEncoding cmd) + | [] -> [] + +let rec encode = function + | `Data(id,data) :: w -> "DATA" :: id :: jencode data :: encode w + | `Error(id,msg) :: w -> "ERROR" :: id :: msg :: encode w + | `Killed id :: w -> "KILLED" :: id :: encode w + | `Rejected id :: w -> "REJECTED" :: id :: encode w + | [] -> [] + +(* -------------------------------------------------------------------------- *) +(* --- ZMQ Messages --- *) +(* -------------------------------------------------------------------------- *) + +let callback socket responses = + try + let msg = encode responses in + Zmq.Socket.send_all socket (if msg = [] then ["NONE"] else msg) + with WrongEncoding msg -> + Zmq.Socket.send_all socket [ "WRONG" ; msg ] + +let fetch socket () = + try + let msg = Zmq.Socket.recv_all ~block:false socket in + try Some Main.{ requests = decode msg ; callback = callback socket } + with WrongEncoding msg -> + Zmq.Socket.send_all socket [ "WRONG" ; msg ] ; None + with + | Unix.Unix_error( Unix.EAGAIN , _ , _ ) -> None + | Zmq.ZMQ_exception(_,msg) -> Senv.fatal "ZeroMQ error: %s" msg + +(* -------------------------------------------------------------------------- *) +(* --- Establish the Server --- *) +(* -------------------------------------------------------------------------- *) + +let establish url = + if url <> "" then + begin + let context = context () in + let socket = Zmq.Socket.(create context rep) in + try + Zmq.Socket.bind socket url ; + Senv.feedback "ZeroMQ [%s]" url ; + Main.run ~pretty:Format.pp_print_string ~fetch:(fetch socket) () ; + Zmq.Socket.close socket ; + with exn -> + Zmq.Socket.close socket ; + raise exn + end + +(* -------------------------------------------------------------------------- *) +(* --- Establish the Server from Command line --- *) +(* -------------------------------------------------------------------------- *) + +let () = Db.Main.extend (fun () -> establish (Enabled.get ())) + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/share/kernel/ast.md b/src/plugins/server/share/kernel/ast.md new file mode 100644 index 0000000000000000000000000000000000000000..b5aaa2d57d7967f620d4cd9cc363691d9df7ddef --- /dev/null +++ b/src/plugins/server/share/kernel/ast.md @@ -0,0 +1,15 @@ +# Ast Services + +The Frama-C internal representation of source files need to be computed before +being accessed. This generally involves preprocessing of sources and finally +parsing, typechecking and normalization. + +Although this step is generally performed silently on-demand inside Frama-C, +from the Server point of view, this is a non-trivial procedure that should _not_ be +triggered outside an `EXEC` request. + +Hence, most AST services might fail or return empty data if the AST has not been +actually computed before. However, in typical usage of Frama-C from the +command-line, the AST would have been computed just before any other plug-in, +including the Server. + diff --git a/src/plugins/server/share/kernel/kernel.md b/src/plugins/server/share/kernel/kernel.md new file mode 100644 index 0000000000000000000000000000000000000000..394e2a7b0e2a5aded0eae70c8f361c78afdf5401 --- /dev/null +++ b/src/plugins/server/share/kernel/kernel.md @@ -0,0 +1,21 @@ +# Kernel Services + +This section deals with requests related to the management +of the Frama-C platform and services offered by the kernel. +This covers several topics: configuration, logs. + +Configuration deals with versioning and resource directories. +Project services allow to work with several Frama-C projects. + +Logs are automatically tracked by the server plug-in and queued. +This monitoring can be controlled by `Kernel.SetLogs` command, but by +default, monitoring is turned on as soon as some server is started +(except for the batch server). The `Kernel.GetLogs` allow to flush this queue, +with a maximum number of messages. Non-flushed messages +can be recovered by subsequent calls, until the requests replies with an empty +message set. + +However, logs emitted prior to the execution of a server, or after its shutdown, +are _not_ collected by default. To enable this monitoring, for instance to collect +messages before the server is started, you shall set the `-server-logs` option, which +takes effect at configuration time (you can also use `... -then -server-logs ...`). diff --git a/src/plugins/server/share/kernel/project.md b/src/plugins/server/share/kernel/project.md new file mode 100644 index 0000000000000000000000000000000000000000..79715b8efc3e93df73d1943768f9394ddbc27f3c --- /dev/null +++ b/src/plugins/server/share/kernel/project.md @@ -0,0 +1,17 @@ +# Project Management + +The Frama-C current project can be managed with the server requests provided below. + +## Current Project + +Initially, the current project is the one selected when the server starts. +Hence, from the Frama-C command line, `-then-on <P> -server-xxx` +would start the server with current project `<P>`. + +When modifying the current project through request `Kernel.Project.SetCurrent`, +client shall wait for an acknowledgement before sending further `GET` requests. +Otherwise, the `GET` might be executed on a different project, due to the +asynchronous behavior of the server. + +However, it is still possible to execute a request on a specific project with +`Kernel.Project.{Get|Set|Exec}On` requests. diff --git a/src/plugins/server/share/kernel/text.md b/src/plugins/server/share/kernel/text.md new file mode 100644 index 0000000000000000000000000000000000000000..b73c79016e9a72b0ed8c3873527b030b23f22161 --- /dev/null +++ b/src/plugins/server/share/kernel/text.md @@ -0,0 +1,47 @@ +# Rich Text Format + +In various place of the server, the Frama-C requests might return +rich-text format, which is text annotated with special tags, +for tagging or styling purpose. + +The JSON encoding of rich-text is defined by the _text_ type, which takes one +of the following possible formats: + +| Format | Description | +|:------:|:------------| +| `"null"` | Empty text | +| _string_ | Standard UTF-8 text | +| `[` (_tag_`,`)? _text_`,`…`,` _text_ `]` | Sequence of text with an optional tag | + +Tags are simple strings, not to be printed, that encode the style or tag to +apply on the sequence. Tags starting with a sharp (`"#…"`) must be understood as +semantic tags, with a meaning depending on the context. Tags starting with +a dot (`".…"`) shall be understood as style names. Other values must be understand +as regular text. + +The empty tag (`""`) shall be ignored, but can used to group sequence of text +together. Concatenation of sequence of text must be performed without any +spacing or cut in the between. + +Text blocks can be nested. For instance, considerer the following JSON +encoding: + +<pre> +[ + "This ", + [ + "#frama-c-server-doc", + "Frama", [ ".tt", "-C" ], " server" + ], + " is ", [ ".it", "awesome" ], " isn't it?" +] +</pre> + +Provided the `#frama-c-server-doc` semantic is understood as a link to the +main page of the Frama-C server documentation, the designated rich-text +shall be printed as: + +> This [Frama-`C` server](../readme.md) is _awesome_ isn't it? + +The precise meaning of styles and semantic tags might depends on the context, +and is detailed in each occurence of _text_ format. diff --git a/src/plugins/server/share/protocol/server.md b/src/plugins/server/share/protocol/server.md new file mode 100644 index 0000000000000000000000000000000000000000..1e479798cef4eba9c42ebf5412a5886e77e6feae --- /dev/null +++ b/src/plugins/server/share/protocol/server.md @@ -0,0 +1,131 @@ +# Architecture + +The Server plug-in provides a _remote procedure call_ (RPC) interface to foreign +applications. The protocol is organized in three logic layers, organized as +follows: + +1. Many external entry points, based on various networking and system facilities +2. A generic logic run-time responsible for scheduling the requests coming from + the various entry points +3. The Frama-C implementation of requests handler, at the kernel or plug-in + level + +The intermediate, logic layer, is responsible for adding a small bit of +parallelism upon the intrinsically synchronous behavior of Frama-C. This makes +Frama-C resembling an asynchronous RPC server. + +The externally visible layer is only focused on transporting external requests +to the logic layer, and transporting back the results to the caller. The only +requirement for an entry point is to be able to transport a sequence of 1-input +message for 1-output message over time. + +The concrete layer is implemented by the Frama-C kernel and its plug-ins. All +requests must be registered _via_ the Frama-C Server OCaml API in order to be +accessible from the entry-points. Some parts of this documentation are +automatically generated from the registered requests. + +## Logical Requests + +From a functional point of view, requests are remote procedures with input +data that reply with output data. Each request is identified by a unique name. +Input and output parameters are encoded into JSON values. + +To adapt the internal synchronous Frama-C implementation with the external +asynchronous entry points, requests are classified into three kinds: + +- `GET` to instantaneously return data from the internal state of Frama-C +- `SET` to instantaneously modifies the state or configure Frama-C plug-ins +- `EXEC` to starts a resource intensive analysis in Frama-C + +During an `EXEC` requests, the implementation of the all resource demanding +computations shall repeatedly call the yielding routine `!Db.progress()` of the +Frama-C kernel to ensures a smooth asynchronous behavior of the Server. During a +_yield_ call, the Server would be allowed to handle few `GET` pending requests, +since they shall be fast without any modification. When the server is idled, any +kind of requests can be started. + +To summarize: + +| Request | During Yields | Allowed to Yield | Computation | +|:--------|:-------------:|:----------------:|:------------:| +| `GET` | ✓ | - | fast, pure | +| `SET` | - | - | fast, side-effects | +| `EXEC` | - | ✓ | resource demanding | + +## Transport Messages + +From the entry points layer, the asynchronous behavior of the Server makes +output data and input data to be dispatched into different messages. However, +from the Client side, we still want to have _one_ response message for each +incoming message. However, answer messages might contains output data from +potentially _any_ previously received requests. + +When the client has no more requests to send, but is simply waiting for pending +requests responses, it must periodically send _polling_ requests to simply get +back the expected responses. + +To implement those features, the Client-Server protocol consists of a sequence of +paired _intput messages_ and _output messages_. Each single input message consists of +a list of _commands_: + +| Commands | Parameters | Description | +|:--------:|:----------:|:------------| +| `POLL` | - | Ask for pending responses, if any | +| `GET` | `id,request,data` | En-queue the given GET request | +| `SET` | `id,request,data` | En-queue the given SET request | +| `EXEC` | `id,request,data` | En-queue the given EXEC request | +| `KILL` | `id` | Cancel the given request or interrupt its execution | +| `SHUTDOWN` | - | Makes the server to stop running | + +Similarly, a single output message consists of a list +of _replies_, listed in table below: + +| Replies | Parameters | Description | +|:--------:|:----------:|:------------| +| `DATA` | `id,data` | Response data from the identified request | +| `ERROR` | `id,message` | Error message from the identified request | +| `KILLED` | `id` | The identified request has been killed or interrupted | +| `REJECTED` | `id` | The identified request was not registered on the Server | + +The logic layer makes no usage of _identifiers_ and simply pass them unchanged into +output messages in response to received requests. + +At the transport message layer, input and output data are made of a +single `JSON` encoded value. Requests are identified by string, and +request identifiers can be of any type from the entry-points. + +**Remark** the `GET`, `SET` or `EXEC` behavior of a request is actually defined +by the request implementation, from the Frama-C internal side. The Server will +silently ignore the request kind from the incoming messages and use the actual +internal one instead. The distinction still appears in the transport protocol +only for a purpose of information, as clients shall know what they are asking +for. + +## Entry Points + +Implementations of entry points layers shall provide a non-blocking `fetch` +function that possibly returns a list of commands, associated with a +callback for emitting the paired list of replies. The Server main +loop is guaranteed to invoke the callback exactly once. + +The Server plug-in implements two entry-points, however, other Frama-C plugins might +implement their own entry-points and call the `Server.Main.run ~fetch ()` function +to make the server starting and exchanging messages with the external world. + +## Request Implementations + +It is the responsibility of Frama-C plug-ins to implement and register requests +into the Server to make them accessible _via_ any entry point. Whereas data is +encoded into JSON structures at the transport layer, requests are processes with +well typed OCaml types from the internal side. + +Hence, the requests implementations also requires data encoder and decoders to +be defined. Some predefined data types are provided by the Server plug-in, but +more complex types can be defined and shared among plug-ins _via_ the +`Server.Data` module factory. + +Registration of requests, data encoder and decoders always comes with their +markdown documentation thanks to the `Markdown` library provided by the Frama-C +kernel. Hence, a full documentation of all implemented requests with their data +formats can be generated consistently at any time. See option `-server-doc` of the +Server plug-in for more details. diff --git a/src/plugins/server/share/protocol/server_batch.md b/src/plugins/server/share/protocol/server_batch.md new file mode 100644 index 0000000000000000000000000000000000000000..1092cc6c5b2a28c8822db87182d5dde7c307ff18 --- /dev/null +++ b/src/plugins/server/share/protocol/server_batch.md @@ -0,0 +1,31 @@ +# Batch Protocol + +This section presents a Frama-C command-line entry point to Server requests. +Although it is not full-featured server entry point, it allows for executing +requests from the command line or JSON scripts. + +The `-server-script` option of the Frama-C/Server plug-in takes a (list of) +script file `<file.json>`, parse it and execute its commands, and output the +reponses in file `<file.out.json>`. + +## Input Script Format + +A JSON script is either a single JSON command or an array of JSON commands, or `null`. +Each command is a record with the following fields: + +| Input Field | Format | Description | +|:------|:-------|:-----------:| +| `id` | _any_ ? | The command identifier (optional) | +| `request` | _string_ | The request name | +| `data` | _any_ | The request input parameters | + +## Output Script Format + +Each command leads to an assiated JSON record, with the following fields: + +| Output Field | Format | Description | +|:------|:-------|:-----------:| +| `id` | _any_ ? | The command identifier (as provided in input) | +| `data` | _any_ ? | The request output parameters (if successfull) | +| `error` | _string_ ? | The request error message (in case of occurence) | +| `at` | _any_ ? | Wrongly encoded part of the request (when appropriate) | diff --git a/src/plugins/server/share/protocol/server_zmq.md b/src/plugins/server/share/protocol/server_zmq.md new file mode 100644 index 0000000000000000000000000000000000000000..f510a31888e81e509a0635425bf6a9ef3cbf75e2 --- /dev/null +++ b/src/plugins/server/share/protocol/server_zmq.md @@ -0,0 +1,62 @@ +# ZeroMQ Protocol + +This section presents a [ZeroMQ](http://zeromq.org) based entry point for Frama-C Server. +It is activated by option `-server-zmq <URL>` option of the Server plug-in, which is compiled +when the OCaml package `zmq` is detected at Frama-C configure time. + +The protocol builds a ZeroMQ socket of type `REP` which is the standard for a request server. +It is meant for accepting connection from a ZeroMQ socket of type `REQ` on the same `URL`. +The paired `REP`-`REQ` sockets use +[ZeroMQ multi-part messages](http://zguide.zeromq.org/page:all#Multipart-Messages) to transfer data. + +A typical example to start a Frama-C server for inter-process communication is: + +```shell +$ frama-c [options...] -then -server-zmq ipc:///tmp/my-server.io +``` + +## Input Message Format + +Each input message consists of a list of commands. Each command takes +a fixed number of parts from the incomming ZeroMQ message. The first part +of each command is a single string identifying the command: + +| Commands | Parts | Part 1 | Part 2 | Part 3 | Part 4 | +|:--------|:-----:|:-------|:-------|:-------|:-------| +| `POLL()` | 1 | `"POLL"` | | | | +| `GET(id,request,data)` | 4 | `"GET"` | id | request | data | +| `SET(id,request,data)` | 4 | `"SET"` | id | request | data | +| `EXEC(id,request,data)` | 4 | `"EXEC"` | id | request | data | +| `KILL(id)` | 2 | `"KILL"` | id | | | +| `SHUTDOWN` | 1 | `"SHUTDOWN"` | | | | + +## Output Message Format + +Each output message consists of a list of replies. Each reply takes +a fixed number of parts from the incomming ZeroMQ message. The first part +of each reply is a finel string identifying the reply: + +| Replies | Parts | Part 1 | Part 2 | Part 3 | +|:--------|:-----:|:-------|:-------|:-------| +| `DATA(id,data)` | 3 | `"DATA"` | id | data | +| `ERROR(id,message)` | 4 | `"ERROR"` | id | message | +| `KILLED(id)` | 2 | `"KILLED"` | id | | +| `REJECTED(id)` | 2 | `"REJECTED"` | id | | +| (special) | 2 | `"WRONG"` | message | | +| (special) | 1 | `"NONE"` | | | + +The two special responses `"WRONG"` and `"NONE"` are used to handle special issues +with the ZeroMQ layer protocol: `WRONG(message)` signals an error in the message formats; +`NONE` is used in the special case where the reply message from the server is completely +empty. This generaly means that the server is busy or idled. + +## Data Format + +Request identifiers can be any string, encoded into a single part of a ZeroMQ message. + +Data are stringified JSON data structures. Each command or reply data shall be packed +into a single JSON data, which leads to a single part of the associated ZeroMQ message. + +Since ZeroMQ prococol accepts any kind of strings as a single +message part, the stringified JSON data might contains spaces, newlines and any +other spacing characters. diff --git a/src/plugins/server/syntax.ml b/src/plugins/server/syntax.ml new file mode 100644 index 0000000000000000000000000000000000000000..565a227cde54a50190a518e5d45f036b9b44d913 --- /dev/null +++ b/src/plugins/server/syntax.ml @@ -0,0 +1,129 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) + +module Senv = Server_parameters + +let check_plugin plugin name = + let p = String.lowercase_ascii plugin in + let n = String.lowercase_ascii name in + let k = String.length plugin in + if not (String.length name > k && + String.sub n 0 k = p && + String.get n k = '.') + then + Senv.warning ~wkey:Senv.wpage + "Data %S shall be named « %s-* »" + name plugin + +let check_page page name = + match Doc.chapter page with + | `Kernel -> () + | `Plugin plugin -> check_plugin plugin name + | `Protocol -> check_plugin "server" name + +let re_name = Str.regexp "[a-z0-9-]+$" + +let check_name name = + if not (Str.string_match re_name name 0) then + Senv.warning ~wkey:Senv.wname + "Data name %S is not a dash-separated list of lowercase identifiers" name + +(* -------------------------------------------------------------------------- *) + +type t = { atomic:bool ; text:Markdown.text } + +let atom md = { atomic=true ; text=md } +let flow md = { atomic=false ; text=md } + +let format { text } = text +let protect a = + if a.atomic then a.text else Markdown.(rm "(" <+> a.text <+> rm ")") + +let publish ~page ~name ~descr ~synopsis ?(details = Markdown.empty) () = + check_name name ; + check_page page name ; + let id = Printf.sprintf "data-%s" name in + let title = Printf.sprintf "`DATA` %s" name in + let format = ref Markdown.nil in + let syntax = Markdown.fmt_block (fun fmt -> + Format.fprintf fmt "> %a ::= %a" + Markdown.pp_text !format + Markdown.pp_text synopsis.text + ) in + let content = Markdown.( par descr </> syntax </> details ) in + let href = Doc.publish ~page ~name:id ~title ~index:[name] content [] in + let link_title = Printf.sprintf "_%s_" name in + let link = Markdown.href ~title:link_title href in + format := link ; atom @@ link + +let unit = atom @@ Markdown.rm "-" +let any = atom @@ Markdown.it "any" +let int = atom @@ Markdown.it "int" +let ident = atom @@ Markdown.it "ident" +let string = atom @@ Markdown.it "string" +let number = atom @@ Markdown.it "number" +let boolean = atom @@ Markdown.it "boolean" + +let escaped name = Markdown.tt @@ Printf.sprintf "'%s'" @@ String.escaped name + +let tag name = atom @@ escaped name + +let array a = atom @@ Markdown.(tt "[" <+> protect a <+> tt ", … ]") + +let tuple ts = + atom @@ Markdown.(tt "[" + <+> glue ~sep:(raw " `,` ") (List.map protect ts) <+> + tt "]") + +let union ts = flow @@ Markdown.(glue ~sep:(raw " | ") (List.map protect ts)) + +let option t = atom @@ Markdown.(protect t <@> tt "?") + +let field (a,t) = Markdown.( escaped a <+> tt ":" <+> t.text ) + +let record fds = + let fields = + if fds = [] then Markdown.rm "…" else + Markdown.(glue ~sep:(raw " `;` ") (List.map field fds)) + in atom @@ Markdown.(tt "{" <+> fields <+> tt "}") + +type field = { + name : string ; + syntax : t ; + descr : Markdown.text ; +} + +let fields ~title (fds : field list) = + let c_field = `Left title in + let c_format = `Center "Format" in + let c_descr = `Left "Description" in + Markdown.table [ c_field ; c_format ; c_descr ] + begin + List.map + (fun f -> + [ Markdown.tt f.name ; format f.syntax ; f.descr ]) + fds + end + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/syntax.mli b/src/plugins/server/syntax.mli new file mode 100644 index 0000000000000000000000000000000000000000..ff4c575e6c3ed0e65a38b66f593272850942ddaf --- /dev/null +++ b/src/plugins/server/syntax.mli @@ -0,0 +1,59 @@ +(**************************************************************************) +(* *) +(* This file is part of Frama-C. *) +(* *) +(* Copyright (C) 2007-2019 *) +(* CEA (Commissariat à l'énergie atomique et aux énergies *) +(* alternatives) *) +(* *) +(* you can redistribute it and/or modify it under the terms of the GNU *) +(* Lesser General Public License as published by the Free Software *) +(* Foundation, version 2.1. *) +(* *) +(* It is distributed in the hope that it will be useful, *) +(* but WITHOUT ANY WARRANTY; without even the implied warranty of *) +(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *) +(* GNU Lesser General Public License for more details. *) +(* *) +(* See the GNU Lesser General Public License version 2.1 *) +(* for more details (enclosed in the file licenses/LGPLv2.1). *) +(* *) +(**************************************************************************) + +(* -------------------------------------------------------------------------- *) +(** JSON Encoding Documentation *) +(* -------------------------------------------------------------------------- *) + +type t + +val format : t -> Markdown.text + +(** The provided synopsis must be very short, to fit in one line. + Extended definition, like record fields and such, must be detailed in + the description block. *) +val publish : + page:Doc.page -> name:string -> descr:Markdown.text -> + synopsis:t -> ?details:Markdown.block -> unit -> t + +val unit : t +val any : t +val int : t (* small, non-decimal, number *) +val ident : t (* integer of string *) +val string : t +val number : t +val boolean : t + +val tag : string -> t +val array : t -> t +val tuple : t list -> t +val union : t list -> t +val option : t -> t +val record : (string * t) list -> t + +type field = { name : string ; syntax : t ; descr : Markdown.text } + +(** Builds a table with fields column named with [~title] + (shall be capitalized) *) +val fields : title:string -> field list -> Markdown.block + +(* -------------------------------------------------------------------------- *) diff --git a/src/plugins/wp/GuiSource.ml b/src/plugins/wp/GuiSource.ml index 66a2c8abcf00bd2f5a755b87814bc08bde0674fb..38697bcde348ac5389e4ffeb545c3972702ea15f 100644 --- a/src/plugins/wp/GuiSource.ml +++ b/src/plugins/wp/GuiSource.ml @@ -232,7 +232,7 @@ class highlighter (main:Design.main_window_extension_points) = ~(start:int) ~(stop:int) = let buffer = buffer#buffer in begin match loc with - | PStmt( _ , stmt ) -> + | PStmt( _ , stmt ) | PStmtStart( _ , stmt ) -> begin match effect with | Some(s,_) when Stmt.equal stmt s -> @@ -250,7 +250,7 @@ class highlighter (main:Design.main_window_extension_points) = if DEPS.mem ip deps then apply_depend buffer start stop end - | PStmtStart _ | PGlobal _ + | PGlobal _ | PVDecl _ | PTermLval _ | PLval _ | PExp _ -> () end