diff --git a/dune-project b/dune-project
index 7c1c263..db7e269 100644
--- a/dune-project
+++ b/dune-project
@@ -23,6 +23,9 @@
(name gobview)
(synopsis "Web frontend for Goblint")
(depends
+ cohttp
+ cohttp-lwt
+ cohttp-lwt-jsoo
dune
(ocaml (>= 4.10.0))
batteries
@@ -37,6 +40,8 @@
ppx_yojson_conv ; TODO: switch to ppx_deriving_yojson like Goblint itself
conduit-lwt-unix
jsoo-react
+ lwt
+ uri
(goblint-cil (>= 2.0.0))
ctypes_stubs_js
integers_stubs_js
diff --git a/goblint-http-server/goblint.ml b/goblint-http-server/goblint.ml
index 4cc867b..5919694 100644
--- a/goblint-http-server/goblint.ml
+++ b/goblint-http-server/goblint.ml
@@ -57,11 +57,7 @@ let config_raw goblint name value =
| Ok _ -> Lwt.return_unit
| Error err -> invalid_arg err.message
-let option_whitelist = [] |> Set.of_list
-
let config goblint name value =
- if not (Set.mem name option_whitelist) then
- invalid_arg (Printf.sprintf "Option '%s' is not in the whitelist" name);
with_lock goblint (fun () -> config_raw goblint name value)
let temp_dir () = Utils.temp_dir "goblint-http-server." ""
diff --git a/goblint-http-server/goblint_http.ml b/goblint-http-server/goblint_http.ml
index 78ebf5e..1512c6c 100644
--- a/goblint-http-server/goblint_http.ml
+++ b/goblint-http-server/goblint_http.ml
@@ -3,12 +3,13 @@ open Cohttp_lwt
open Cohttp_lwt_unix
open Lwt.Infix
+module Header = Cohttp.Header
module Yojson_conv = Ppx_yojson_conv_lib.Yojson_conv
let docroot = ref "run"
let index = ref "index.html"
let addr = ref "127.0.0.1"
-let port = ref 8080
+let port = ref 8000
let goblint = ref "goblint"
let rest = ref []
@@ -24,6 +25,13 @@ let specs =
let paths = ref []
+let cors_headers =
+ [
+ ("Access-Control-Allow-Origin", "*");
+ ("Access-Control-Allow-Headers", "Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers");
+ ("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ ] |> Header.of_list
+
let process state name body =
match Hashtbl.find_option Api.registry name with
| None -> Server.respond_not_found ()
@@ -31,24 +39,24 @@ let process state name body =
let%lwt body = Body.to_string body in
let body = if body = "" then "null" else body in
match Yojson.Safe.from_string body with
- | exception Yojson.Json_error err -> Server.respond_error ~status:`Bad_request ~body:err ()
+ | exception Yojson.Json_error err -> Server.respond_error ~headers:cors_headers ~status:`Bad_request ~body:err ()
| json ->
match R.body_of_yojson json with
| exception Yojson_conv.Of_yojson_error (exn, _) ->
- Server.respond_error ~status:`Bad_request ~body:(Printexc.to_string exn) ()
+ Server.respond_error ~headers:cors_headers ~status:`Bad_request ~body:(Printexc.to_string exn) ()
| body ->
Lwt.catch
(fun () ->
R.process state body
>|= R.yojson_of_response
>|= Yojson.Safe.to_string
- >>= fun body -> Server.respond_string ~status:`OK ~body ())
- (fun exn -> Server.respond_error ~status:`Bad_request ~body:(Printexc.to_string exn) ())
+ >>= fun body -> Server.respond_string ~headers:cors_headers ~status:`OK ~body ())
+ (fun exn -> Server.respond_error ~headers:cors_headers ~status:`Bad_request ~body:(Printexc.to_string exn) ())
(* The serving of files is implemented similar as in the binary https://github.com/mirage/ocaml-cohttp/blob/master/cohttp-lwt-unix/bin/cohttp_server_lwt.ml *)
let serve_file ~docroot ~uri =
let fname = Cohttp.Path.resolve_local_file ~docroot ~uri in
- Server.respond_file ~fname ()
+ Server.respond_file ~headers:cors_headers ~fname ()
let sort lst =
let compare_kind = function
@@ -102,7 +110,7 @@ let serve uri path =
| Unix.S_DIR -> (
let path_len = String.length path in
if path_len <> 0 && path.[path_len - 1] <> '/' then (
- Server.respond_redirect ~uri:(Uri.with_path uri (path ^ "/")) ())
+ Server.respond_redirect ~headers:cors_headers ~uri:(Uri.with_path uri (path ^ "/")) ())
else (
match Sys.file_exists (Filename.concat file_name !index) with
| true -> (
@@ -122,12 +130,12 @@ let serve uri path =
f ))
(fun _exn -> Lwt.return (None, f))) files in
let body = html_of_listing uri path (sort listing) in
- Server.respond_string ~status:`OK ~body ()))
+ Server.respond_string ~headers:cors_headers ~status:`OK ~body ()))
| Unix.S_REG -> serve_file ~docroot:!docroot ~uri
| _ -> (
let body = Printf.sprintf "
Forbidden
%s is not a normal file or \
directory
" path in
- Server.respond_string ~status:`OK ~body ()))
+ Server.respond_string ~headers:cors_headers ~status:`OK ~body ()))
(function
| Unix.Unix_error (Unix.ENOENT, "stat", p) as e ->
if p = file_name then (
@@ -143,6 +151,7 @@ let callback state _ req body =
match meth, parts with
| `POST, ["api"; name] -> process state name body
| `GET, _ -> serve uri path
+ | `OPTIONS, _ -> Server.respond ~headers:cors_headers ~status:`OK ~body:`Empty () (* Used for preflight and CORS requests *)
| _ -> Server.respond_not_found ()
let main () =
diff --git a/gobview.opam b/gobview.opam
index 04e81ac..b2d6715 100644
--- a/gobview.opam
+++ b/gobview.opam
@@ -12,6 +12,9 @@ license: "MIT"
homepage: "https://github.com/goblint/gobview"
bug-reports: "https://github.com/goblint/gobview/issues"
depends: [
+ "cohttp"
+ "cohttp-lwt"
+ "cohttp-lwt-jsoo"
"dune" {>= "2.7"}
"ocaml" {>= "4.10.0"}
"batteries"
@@ -26,6 +29,8 @@ depends: [
"ppx_yojson_conv"
"conduit-lwt-unix"
"jsoo-react"
+ "lwt"
+ "uri"
"goblint-cil" {>= "2.0.0"}
"ctypes_stubs_js"
"integers_stubs_js"
diff --git a/package-lock.json b/package-lock.json
index 0566ac3..c88598f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"license": "MIT",
"dependencies": {
"bootstrap": "^5.0.2",
+ "bootstrap5-autocomplete": "^1.1.14",
"graphviz-react": "^1.2.0",
"monaco-editor": "^0.25.2",
"react": "^17.0.2",
@@ -7695,6 +7696,16 @@
"integrity": "sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==",
"requires": {}
},
+ "bootstrap-autocomplete": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/bootstrap-autocomplete/-/bootstrap-autocomplete-2.3.7.tgz",
+ "integrity": "sha512-YD2bev4bDTLsAzkr6lSTuMZOcE7k2+c2BYbtIT+j/XpPnM5ADLlbhR4NnpSsfg6Vde5xQckG/IbdRKrLlAtXOw=="
+ },
+ "bootstrap5-autocomplete": {
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/bootstrap5-autocomplete/-/bootstrap5-autocomplete-1.1.14.tgz",
+ "integrity": "sha512-QNV/aqW52tbdniydK3r9tibnvcs90schqHLKJH7ZnI1iGV19KalrdAYFsg6Z8eUSyKLZHiFh7v7IQOWL1qvk6g=="
+ },
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
diff --git a/src/App.re b/src/App.re
index 55532f4..fff73f8 100644
--- a/src/App.re
+++ b/src/App.re
@@ -1,100 +1,12 @@
-open Goblint_lib;
-open Batteries;
-open Js_of_ocaml;
open Lwt.Infix;
-module Cabs2cil = GoblintCil.Cabs2cil;
-
-exception InitFailed(string);
-
-let init_cil = environment => {
- // Restore the environment hash table, which
- // syntacticsearch uses to map temporary variables created
- // by CIL to their original counterparts.
- Hashtbl.clear(Cabs2cil.environment);
- environment
- |> Hashtbl.enum
- |> Enum.iter(((k, v)) => Hashtbl.add(Cabs2cil.environment, k, v));
-};
-
-// Each Goblint analysis module is assigned an ID, and this
-// ID depends on the module registration order, which might
-// differ from build to build. This function reorders the
-// analysis list to match the native version of Goblint.
-let renumber_goblint_analyses = registered_name => {
- let old_registered = Hashtbl.copy(MCP.registered);
- let old_registered_name = Hashtbl.copy(MCP.registered_name);
- Hashtbl.clear(MCP.registered);
- Hashtbl.clear(MCP.registered_name);
- Hashtbl.iter((name, id) => {
- let old_id = Hashtbl.find(old_registered_name, name);
- let spec = Hashtbl.find(old_registered, old_id);
- Hashtbl.replace(MCP.registered, id, spec);
- Hashtbl.replace(MCP.registered_name, name, id);
- }, registered_name);
-};
-
-let init_goblint = (solver, spec, registered_name, config, cil) => {
- AfterConfig.run(); // This registers the "base" analysis
-
- try(renumber_goblint_analyses(registered_name)) {
- | Not_found =>
- raise(InitFailed("Failed to renumber Goblint analyses"))
- };
-
- Sys.chdir("/"); // Don't remove this
-
- Sys_js.create_file(~name="/goblint/solver.marshalled", ~content=solver);
- Sys_js.create_file(~name="/goblint/config.json", ~content=config);
- Sys_js.create_file(~name="/goblint/spec_marshal", ~content=spec);
-
- GobConfig.merge_file(Fpath.v("/goblint/config.json"));
-
- GobConfig.set_bool("dbg.verbose", true);
- // TODO: Uncomment this to improve performance in future
- // GobConfig.set_bool("verify", false);
-
- // For some reason, the standard Batteries output channels
- // appear to be closed by default and writing to them
- // raises [BatInnerIO.Output_closed]. This fixes it.
- let out = IO.output_channel(Stdlib.stdout);
- Messages.formatter := Format.formatter_of_output(out);
-
- GobConfig.set_string("load_run", "goblint");
-
- // These two will be set by config.json. Reset them.
- GobConfig.set_string("save_run", "");
- GobConfig.set_bool("gobview", false);
-
- GobConfig.set_auto("trans.activated[+]", "'expeval'");
-
- Cilfacade.init();
- Maingoblint.handle_extraspecials();
- Maingoblint.handle_flags();
-
- // NOTE: Commenting this out since it breaks the node view. Semantic search
- // may depend on this code but it is currently broken because of unrelated
- // (and uknown) reasons anyway.
- // Cil.iterGlobals(cil, glob =>
- // switch (glob) {
- // | GFun(fd, _) =>
- // Cil.prepareCFG(fd);
- // Cil.computeCFGInfo(fd, true);
- // | _ => ()
- // }
- // );
- Cilfacade.current_file := cil;
-
- let goblint = GvGoblint.unmarshal(spec, cil);
-
- (goblint, cil);
-};
+open Init;
let init = (solver, spec, config, meta, cil, analyses, warnings, stats, file_loc) => {
let cil =
switch (cil) {
| Ok(s) =>
let (c, e) = Marshal.from_string(s, 0);
- init_cil(e);
+ Init.init_cil(e);
c;
| _ => raise(InitFailed("Failed to load CIL state"))
};
@@ -172,6 +84,6 @@ let handle_error = exc => {
| exc => handle_error(exc)
}
| _ => ()
- },
+ }
)
);
diff --git a/src/Init.re b/src/Init.re
new file mode 100644
index 0000000..ac70fcd
--- /dev/null
+++ b/src/Init.re
@@ -0,0 +1,142 @@
+open Goblint_lib;
+open Batteries;
+open Js_of_ocaml;
+open Lwt.Infix;
+module Cabs2cil = GoblintCil.Cabs2cil;
+
+exception InitFailed(string);
+
+let init_cil = environment => {
+ // Restore the environment hash table, which
+ // syntacticsearch uses to map temporary variables created
+ // by CIL to their original counterparts.
+ Hashtbl.clear(Cabs2cil.environment);
+ environment
+ |> Hashtbl.enum
+ |> Enum.iter(((k, v)) => Hashtbl.add(Cabs2cil.environment, k, v));
+};
+
+// Each Goblint analysis module is assigned an ID, and this
+// ID depends on the module registration order, which might
+// differ from build to build. This function reorders the
+// analysis list to match the native version of Goblint.
+let renumber_goblint_analyses = registered_name => {
+ let old_registered = Hashtbl.copy(MCP.registered);
+ let old_registered_name = Hashtbl.copy(MCP.registered_name);
+ Hashtbl.clear(MCP.registered);
+ Hashtbl.clear(MCP.registered_name);
+ Hashtbl.iter((name, id) => {
+ let old_id = Hashtbl.find(old_registered_name, name);
+ let spec = Hashtbl.find(old_registered, old_id);
+ Hashtbl.replace(MCP.registered, id, spec);
+ Hashtbl.replace(MCP.registered_name, name, id);
+ }, registered_name);
+};
+
+let init_goblint = (solver, spec, registered_name, config, cil) => {
+ AfterConfig.run(); // This registers the "base" analysis
+
+ try(renumber_goblint_analyses(registered_name)) {
+ | Not_found =>
+ raise(InitFailed("Failed to renumber Goblint analyses"))
+ };
+
+ Sys.chdir("/"); // Don't remove this
+
+ Sys_js.create_file(~name="/goblint/solver.marshalled", ~content=solver);
+ Sys_js.create_file(~name="/goblint/config.json", ~content=config);
+ Sys_js.create_file(~name="/goblint/spec_marshal", ~content=spec);
+
+ GobConfig.merge_file(Fpath.v("/goblint/config.json"));
+
+ GobConfig.set_bool("dbg.verbose", true);
+ // TODO: Uncomment this to improve performance in future
+ // GobConfig.set_bool("verify", false);
+
+ // For some reason, the standard Batteries output channels
+ // appear to be closed by default and writing to them
+ // raises [BatInnerIO.Output_closed]. This fixes it.
+ let out = IO.output_channel(Stdlib.stdout);
+ Messages.formatter := Format.formatter_of_output(out);
+
+ GobConfig.set_string("load_run", "goblint");
+
+ // These two will be set by config.json. Reset them.
+ GobConfig.set_string("save_run", "");
+ GobConfig.set_bool("gobview", false);
+
+ GobConfig.set_auto("trans.activated[+]", "'expeval'");
+
+ Cilfacade.init();
+ Maingoblint.handle_extraspecials();
+ Maingoblint.handle_flags();
+
+ // NOTE: Commenting this out since it breaks the node view. Semantic search
+ // may depend on this code but it is currently broken because of unrelated
+ // (and uknown) reasons anyway.
+ // Cil.iterGlobals(cil, glob =>
+ // switch (glob) {
+ // | GFun(fd, _) =>
+ // Cil.prepareCFG(fd);
+ // Cil.computeCFGInfo(fd, true);
+ // | _ => ()
+ // }
+ // );
+ Cilfacade.current_file := cil;
+
+ let goblint = GvGoblint.unmarshal(spec, cil);
+
+ (goblint, cil);
+};
+
+// function for reloading specific files when rerunning analysis
+let reload = (cil) => {
+ [
+ // files to be reloaded
+ "./solver.marshalled",
+ "./warnings.marshalled",
+ "./stats.marshalled",
+
+ // required for solver, solver alone cannot update
+ "./spec_marshal",
+ "./analyses.marshalled",
+ "./config.json",
+ ]
+ |> List.map(HttpClient.get)
+ |> Lwt.all
+ >>= (l) => {
+ Lwt.return(switch (l) {
+ | [solver, warnings, stats, spec, analyses, config] => {
+
+ let goblintAndCil =
+ switch (solver, spec, analyses, config) {
+ | (Ok(s), Ok(spec), Ok(t), Ok(c)) => {
+ let t = Marshal.from_string(t, 0);
+ Some(init_goblint(s, spec, t, c, cil))
+ }
+ | _ => {
+ Util.error_without_fail("Failed to load Goblint state");
+ None
+ }
+ };
+ print_endline("Initialized Goblint");
+
+ let warnings =
+ switch (warnings) {
+ | Ok(s) => Some(Marshal.from_string(s, 0))
+ | _ => Util.error_without_fail("Failed to load the warning table"); None
+ };
+ print_endline("Restored the warning table");
+
+ let stats =
+ switch (stats) {
+ | Ok(s) => Some(Marshal.from_string(s, 0))
+ | _ => Util.error_without_fail("Failed to load runtime stats"); None
+ };
+
+ Some((goblintAndCil, warnings, stats))
+ }
+ | _ => None
+ })
+ }
+};
\ No newline at end of file
diff --git a/src/Main.re b/src/Main.re
index 5ce0b10..c20e9fd 100644
--- a/src/Main.re
+++ b/src/Main.re
@@ -1,11 +1,65 @@
open Batteries;
+open Lwt.Infix;
[@react.component]
let make = (~cil, ~goblint, ~warnings, ~meta, ~stats, ~file_loc) => {
+ let (analysisState, setAnalysisState) = React.useState(_ => Option.some(ParameterView.Executed));
+
+ // reloadable components of goblint due to rerunning analyses
+ let (cil_, setCil_) = React.useState(_ => cil);
+ let (goblint_, setGoblint_) = React.useState(_ => goblint);
+ let (warnings_, setWarnings_) = React.useState(_ => warnings);
+ let (stats_, setStats_) = React.useState(_ => stats);
+
+ React.useEffect1(() => {
+ switch (analysisState) {
+ | Some(s) => {
+ if (ParameterView.is_successful(s)) {
+ // reload files
+ let reload = () => {
+ Init.reload(cil_) >>=
+ (result) => {
+ switch (result) {
+ | Some((goblintAndCil, warnings, stats)) => {
+ let _ = switch (goblintAndCil) {
+ | Some((goblint', cil')) => {
+ setGoblint_(_ => goblint')
+ setCil_(_ => cil');
+ };
+ | None => Util.error_without_fail("Could not reload goblint and cil for rerun")
+ };
+
+ switch (warnings) {
+ | Some(w) => setWarnings_(_ => w);
+ | None => Util.error_without_fail("Could not reload warnings for rerun")
+ };
+
+ switch (stats) {
+ | Some(s) => setStats_(_ => s);
+ | None => Util.error_without_fail("Could not reload stats for rerun")
+ };
+ };
+ | None => Util.error_without_fail("Could not reload metrics for rerun")
+ }
+
+ setAnalysisState(_ => None);
+ Lwt.return();
+ }
+ };
+
+ ignore(reload());
+ }
+ }
+ | None => setAnalysisState(_ => None);
+ };
+
+ None
+ }, [|analysisState|]);
+
let (state, dispatch) =
React.useReducer(
Reducer.reducer,
- State.make(~cil, ~goblint, ~warnings, ~meta, ~stats, ~file_loc, ()),
+ State.make(~cil=cil_, ~goblint=goblint_, ~warnings=warnings_, ~meta, ~stats=stats_, ~file_loc, ()),
);
let fetch_file =
@@ -48,6 +102,21 @@ let make = (~cil, ~goblint, ~warnings, ~meta, ~stats, ~file_loc) => {
[|state.display|],
);
+ // State management for ParameterView component
+ let (goblint_path, parameters) = state |> ParameterUtils.get_parameters;
+ let (destructured_params, _) = parameters |> ParameterUtils.construct_parameters;
+
+ let (history, setHistory) = React.useState(_ => [(destructured_params, Time.get_local_time(), ParameterView.Executed, "")]);
+ let (inputValue, setInputValue) = React.useState(_ => destructured_params |> ParameterUtils.concat_parameter_list);
+ let (disableRun, setDisableRun) = React.useState(_ => false);
+ let (inputState, setInputState) = React.useState(_ => ParameterView.Ok);
+ let (sortDesc, setSortDesc) = React.useState(_ => true);
+
+
+ React.useEffect1(() => {
+ None
+ }, [|history|]);
+
diff --git a/src/Util.ml b/src/Util.ml
index dd2bb71..95eaa5e 100644
--- a/src/Util.ml
+++ b/src/Util.ml
@@ -1,4 +1,5 @@
open Js_of_ocaml
let log s = Firebug.console##log (Js.string s)
-let error s = Firebug.console##error (Js.string s); failwith(s)
+let error_without_fail s = Firebug.console##error (Js.string s)
+let error s = error_without_fail(s); failwith(s)
let warning s = Firebug.console##warn (Js.string s)
diff --git a/src/dune b/src/dune
index 8af6dd1..beb8aeb 100644
--- a/src/dune
+++ b/src/dune
@@ -5,6 +5,9 @@
(modes js)
(libraries
batteries.unthreaded
+ cohttp
+ cohttp-lwt
+ cohttp-lwt-jsoo
fpath
gen_js_api
goblint-cil
@@ -20,6 +23,7 @@
lwt
ppx_deriving_yojson.runtime
ppx_deriving.runtime
+ uri
yojson
zarith
zarith_stubs_js)
diff --git a/src/ui/base/input.re b/src/ui/base/input.re
index 50f18b6..cc11966 100644
--- a/src/ui/base/input.re
+++ b/src/ui/base/input.re
@@ -1,10 +1,12 @@
[@react.component]
-let make = (~type_=?, ~class_=?, ~value, ~on_change, ~on_submit=?) => {
+let make = (~type_=?, ~class_=?, ~value, ~on_change, ~on_submit=?, ~style=?, ~id=?) => {
let (type_, class_, on_submit) =
Utils.fix_opt_args3(type_, class_, on_submit);
let type_ = Option.value(type_, ~default=`Text);
let class_ = Option.value(class_, ~default=["form-control"]);
-
+ let style_ = Option.value(style, ~default=React.Dom.Style.make());
+ let id_ = Option.value(id, ~default="");
+
let type_ =
switch (type_) {
| `Text => "text"
@@ -25,5 +27,9 @@ let make = (~type_=?, ~class_=?, ~value, ~on_change, ~on_submit=?) => {
Option.iter(cb => cb(), on_submit);
};
-
;
+ switch id_ {
+ | "" =>
;
+ | _ =>
;
+ }
+
};
diff --git a/src/ui/base/spinner.re b/src/ui/base/spinner.re
new file mode 100644
index 0000000..cb487ff
--- /dev/null
+++ b/src/ui/base/spinner.re
@@ -0,0 +1,4 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconArrowDown.re b/src/ui/icons/IconArrowDown.re
new file mode 100644
index 0000000..4c8be67
--- /dev/null
+++ b/src/ui/icons/IconArrowDown.re
@@ -0,0 +1,6 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconArrowUp.re b/src/ui/icons/IconArrowUp.re
new file mode 100644
index 0000000..ac91cc5
--- /dev/null
+++ b/src/ui/icons/IconArrowUp.re
@@ -0,0 +1,6 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconCheckmark.re b/src/ui/icons/IconCheckmark.re
new file mode 100644
index 0000000..ec9d7b6
--- /dev/null
+++ b/src/ui/icons/IconCheckmark.re
@@ -0,0 +1,6 @@
+[@react.component]
+let make = () => {
+
;
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconClock.re b/src/ui/icons/IconClock.re
new file mode 100644
index 0000000..7e019ff
--- /dev/null
+++ b/src/ui/icons/IconClock.re
@@ -0,0 +1,7 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconPlay.re b/src/ui/icons/IconPlay.re
new file mode 100644
index 0000000..3bbe543
--- /dev/null
+++ b/src/ui/icons/IconPlay.re
@@ -0,0 +1,6 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconWarning.re b/src/ui/icons/IconWarning.re
new file mode 100644
index 0000000..8e4fd6e
--- /dev/null
+++ b/src/ui/icons/IconWarning.re
@@ -0,0 +1,7 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/icons/IconX.re b/src/ui/icons/IconX.re
new file mode 100644
index 0000000..362907e
--- /dev/null
+++ b/src/ui/icons/IconX.re
@@ -0,0 +1,6 @@
+[@react.component]
+let make = () => {
+
+}
\ No newline at end of file
diff --git a/src/ui/panel/Panel.re b/src/ui/panel/Panel.re
index b7ef9af..44be345 100644
--- a/src/ui/panel/Panel.re
+++ b/src/ui/panel/Panel.re
@@ -19,6 +19,7 @@ let make_nav_pills = (current, dispatch) => {
|> List.mapi((i, (v, n)) => {
@@ -31,28 +32,26 @@ let make_nav_pills = (current, dispatch) => {
};
[@react.component]
-let make = (~state, ~dispatch) => {
- let parameters =
- switch (Yojson.Safe.Util.member("command", state.meta)) {
- | `String(command) => command
- | _ => ""
- };
+let make = (~state, ~dispatch, ~goblint_path, ~inputValue, ~setInputValue, ~disableRun, ~setDisableRun, ~inputState, ~setInputState, ~sortDesc, ~setSortDesc, ~history, ~setHistory, ~setAnalysisState) => {
let locations = (state.goblint)#dead_locations;
let current = state.selected_panel;
+
+ let component = switch (current) {
+ | Some(Warnings) =>
+ | Some(DeadCode) =>
+ | Some(Statistics) =>
+ | Some(Parameters) =>
+ | _ => React.null
+ };
+
{make_nav_pills(current, dispatch)}
- {switch (current) {
- | Some(Warnings) =>
- | Some(DeadCode) =>
- | Some(Parameters) =>
- | Some(Statistics) =>
- | _ => React.null
- }}
+ {component}
;
-};
+};
\ No newline at end of file
diff --git a/src/ui/panel/ParameterView.re b/src/ui/panel/ParameterView.re
index c6c6b00..4b272f5 100644
--- a/src/ui/panel/ParameterView.re
+++ b/src/ui/panel/ParameterView.re
@@ -1,4 +1,396 @@
+open Lwt;
+open Cohttp;
+open Cohttp_lwt;
+
+module Js = Js_of_ocaml.Js;
+module Client = Cohttp_lwt_jsoo.Client;
+module ReactDOM = React.Dom;
+
+type paramState = Executed | Executing | Canceled | Error;
+
+// For debugging purposes and potential further use
+let paramState_to_string = (p: paramState) => {
+ switch p {
+ | Executed => "Executed";
+ | Executing => "Executing";
+ | Canceled => "Canceled";
+ | Error => "Error";
+ };
+};
+
+let is_successful = (p: paramState) => p == Executed;
+
+type inputState = Blacklisted | Malformed | Empty | Ok;
+
+let inputState_to_string = (i: inputState) => {
+ switch i {
+ | Blacklisted => "Blacklisted options are not allowed";
+ | Malformed => "Options are malformed. Check the input again"
+ | Empty => "At least one parameter has to be entered";
+ | Ok => "";
+ }
+}
+
+let is_ok = (i: inputState) => i == Ok;
+
+let option_whitelist = [
+ "incremental.force-reanalyze.funs",
+ "incremental.reluctant.enabled",
+ "incremental.compare",
+ "incremental.detect-renames",
+ "incremental.restart",
+ "incremental.postsolver",
+ "annotation.goblint_precision"
+];
+
+let scheme = "http";
+let host = "127.0.0.1";
+let port = 8000;
+let analyze_path = "/api/analyze";
+let config_path = "/api/config";
+let analyze_uri = Printf.sprintf("%s://%s:%d%s", scheme, host, port, analyze_path) |> Uri.of_string;
+let config_uri = Printf.sprintf("%s://%s:%d%s", scheme, host, port, config_path) |> Uri.of_string;
+
+let headers = [
+ ("Content-Type", "application/json"),
+ ("Access-Control-Allow-Origin", Printf.sprintf("%s://%s:%d", scheme, host, port)),
+ ("Access-Control-Allow-Headers", "Content-Type"),
+ ("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+] |> Header.of_list;
+
[@react.component]
-let make = (~parameters) => {
- {parameters |> React.string}
+let make = (~goblint_path, ~inputValue, ~setInputValue, ~disableRun, ~setDisableRun, ~inputState, ~setInputState, ~sortDesc, ~setSortDesc, ~history, ~setHistory, ~setAnalysisState) => {
+ // Linked to cancelation, see reasons below in on_cancel() for why it is commented out
+ //let (disableCancel, setDisableCancel) = React.useState(_ => true);
+
+ React.useEffect1(() => {
+ None
+ }, [|inputValue|]);
+
+ React.useEffect1(() => {
+ None
+ }, [|sortDesc|]);
+
+ React.useEffect1(() => {
+ None
+ }, [|disableRun|]);
+
+ let on_sort = (ev) => {
+ React.Event.Mouse.preventDefault(ev);
+ setSortDesc(_ => !sortDesc);
+ }
+
+ let get_input_state = (parameter_list: list((string, string)), is_malformed, input_val): inputState => {
+ if (String.length(input_val) == 0) {
+ Empty
+ } else if(is_malformed || List.length(parameter_list) == 0) {
+ Malformed
+ } else {
+ let has_found_option =
+ parameter_list
+ |> List.for_all(((option, _)) => {
+ let result =
+ option_whitelist
+ |> List.map (whitelisted_option => String.starts_with(~prefix=whitelisted_option, option))
+ |> List.find_opt (b => b == true);
+
+ switch (result) {
+ | Some(b) => b;
+ | None => false;
+ }
+ });
+
+ if (!has_found_option) {
+ Blacklisted
+ } else {
+ Ok
+ }
+ }
+ }
+
+ let react_on_input = (parameter_list, is_malformed, inputValue) => {
+ let input_state = get_input_state(parameter_list, is_malformed, inputValue);
+ setInputState(_ => input_state);
+
+ let isInvalid = !is_ok(input_state)
+ setDisableRun(_ => isInvalid);
+
+ isInvalid
+ }
+
+ let check_input = (inputValue) : bool => {
+ let (tuple_parameter_list, is_malformed) =
+ inputValue
+ |> ParameterUtils.construct_parameters
+ |> ((p,b)) => (p |> ParameterUtils.tuples_from_parameters, b);
+
+ react_on_input(tuple_parameter_list, is_malformed, inputValue)
+ }
+
+ let on_add_parameter = (ev) => {
+ let target = React.Event.Mouse.target(ev) |> ReactDOM.domElement_of_js;
+ let unresolved_parameter = target##.textContent |> Js.Opt.to_option;
+
+ let parameter = switch unresolved_parameter {
+ | Some(p) => {
+ let resolved_parameter = Js.to_string(p);
+ if (String.length(inputValue) > 0) {
+ " " ++ resolved_parameter
+ } else {
+ resolved_parameter
+ }
+ };
+ | None => ""
+ };
+
+ setInputValue(inputVal => {
+ let new_inputVal = String.cat(inputVal, parameter);
+ new_inputVal
+ });
+ }
+
+ let on_change = (new_inputValue) => {
+ let _ = check_input(new_inputValue);
+ setInputValue(_ => new_inputValue);
+ };
+
+ let on_submit = () => {
+ let (parameter_list, tuple_parameter_list, is_malformed) =
+ inputValue
+ |> ParameterUtils.construct_parameters
+ |> ((p,b)) => (p, p |> ParameterUtils.tuples_from_parameters, b);
+
+ // To prevent invalid default input to be executed, with i.e. blacklisted options, we check the input value first
+ /*let isInvalid = react_on_input(tuple_parameter_list, is_malformed, inputValue);*/
+
+ if (inputState == Ok && !is_malformed && !disableRun) {
+ let time = Time.get_local_time();
+ let init_state = Executing;
+ let element = (parameter_list, time, init_state, "");
+
+ let new_history = List.cons(element, history);
+
+ setHistory(_ => new_history);
+ //setDisableCancel(_ => false);
+ setDisableRun(_ => true);
+ setAnalysisState(_ => Some(init_state));
+
+ let modify_history = (result: paramState, response_msg: string): unit => {
+ let pickedElem = new_history |> List.hd;
+
+ if (pickedElem == element) {
+ let intermediateHistory = new_history |> List.tl;
+ let new_history = List.cons(((parameter_list, time, result, response_msg)), intermediateHistory);
+ setHistory(_ => new_history);
+ //setDisableCancel(_ => true);
+ setDisableRun(_ => false);
+ setAnalysisState(_ => Some(result));
+ }
+ }
+
+ let transform_resp = ((res, body)) => {
+ let code = res |> Response.status |> Code.code_of_status;
+
+ Cohttp_lwt.Body.to_string(body) >>= (body) => {
+ if (code < 200 || code >= 400) {
+ Lwt.return((Error, body));
+ } else {
+ Lwt.return((Executed, body));
+ };
+ }
+ };
+
+ let inner_config_api_call = (config_body): Lwt.t((paramState, string)) => {
+ Client.post(config_uri, ~body=config_body, ~headers=headers) >>= transform_resp
+ };
+
+ let config_api_call = (config_opts: list((string, string))) => {
+ let config_call_function = () => {
+ config_opts
+ |> List.map(((k,v)) => {
+ let v' = if (!String.equal("true", v) &&
+ !String.equal("false", v) &&
+ (!String.starts_with(~prefix="[", v) && !String.ends_with(~suffix="]", v) &&
+ !Str.string_match((Str.regexp({|^[0-9]+$|})), v, 0))) {
+ "\"" ++ v ++ "\"" // check whether value is a string => wrap it in ""
+ } else {
+ v
+ };
+
+ `List([`String(k), Yojson.Safe.from_string(v')])
+ |> Yojson.Safe.to_string
+ |> Body.of_string;
+ })
+ |> List.map(inner_config_api_call)
+ |> Lwt.all;
+ };
+
+ let exception_handler = (exc) => {
+ Lwt.return([(Error, Printexc.to_string(exc))]);
+ };
+
+ Lwt.catch(config_call_function, exception_handler);
+ };
+
+ let inner_analyze_api_call = (analyze_body) => {
+ Client.post(analyze_uri, ~body=analyze_body, ~headers=headers) >>= transform_resp
+ };
+
+ let analyze_body =
+ `List([`String ("Functions"), `List ([])])
+ |> Yojson.Safe.to_string
+ |> Body.of_string;
+
+ let analyze_api_call = () => {
+ let exception_handler = (exc) => {
+ Lwt.return((Error, Printexc.to_string(exc)));
+ };
+
+ tuple_parameter_list
+ |> config_api_call >>=
+ (result) => {
+ let result_state = result |> List.find_opt(((state, _)) => state == Error);
+
+ switch (result_state) {
+ | None => Lwt.return((Executed, ""))
+ | Some(r) => Lwt.return(r)
+ }
+ } >>=
+ (result) => {
+ switch result {
+ | (Executed, _) => Lwt.catch(() => inner_analyze_api_call(analyze_body), exception_handler);
+ | (_, msg) => Lwt.return((Error, msg))
+ }
+ } >>=
+ ((state, msg)) => {
+ modify_history(state, msg);
+ Lwt.return()
+ };
+ };
+
+ ignore(analyze_api_call());
+ }
+ };
+
+ // This cancel function is here in case it will be implemented in the http-server, not far fetched.
+ /*let on_cancel = () => {
+ let (param, time, _) = history |> List.hd;
+ let intermediateHistory = history |> List.tl;
+ let new_history = List.cons(((param, time, Canceled)), intermediateHistory);
+ setHistory(_ => new_history);
+ setDisableCancel(_ => true);
+ };*/
+
+ // Check whether default parameters are "Ok" or not
+ let (tuple_parameter_list, is_malformed) =
+ inputValue
+ |> ParameterUtils.construct_parameters
+ |> ((p,b)) => (p |> ParameterUtils.tuples_from_parameters, b);
+ let _ = react_on_input(tuple_parameter_list, is_malformed, inputValue);
+
+ let map_history_entry_to_list_entry = (history) => {
+ history
+ |> (history) => {
+ if (!sortDesc) {
+ history |> List.rev
+ } else {
+ history
+ }
+ }
+ |> List.mapi((i, (parameter_grouping, time, paramState, msg)) =>
+ {
+
+
+ {switch paramState {
+ | Error =>
+ | _ => {
+
+ {switch paramState {
+ | Executing =>
+ | Canceled =>
+ | Executed =>
+ | Error =>
+ }}
+
+ }
+ }}
+
+
+
+ {time ++ " " |> React.string}
+
+
+
+ {parameter_grouping |> List.mapi((j,e) => {
+
+ {e |> React.string}
+
+ }) |> React.list}
+
+
+
+ }
+ );
+ };
+
+ let list_elements = history |> map_history_entry_to_list_entry;
+
+
+
+
+
+ // Commented out because http server does not support cancelation yet
+ /*
*/
+
+
+ // Input and tooltip are seperated due to display issues
+ {switch inputState {
+ | Malformed
+ | Blacklisted
+ | Empty =>
+ | Ok =>
;
+ }}
+ {switch inputState {
+ | Ok => React.null;
+ | _ => {
+
+ {inputState |> inputState_to_string |> React.string}
+
+ };
+ }}
+
+
+
+
+
+ {-
+
+
+
+ {"Status" |> React.string}
+
+
+ {"Time " |> React.string}
+ {if (sortDesc) {
+
+ } else {
+
+ }}
+
+
+ {"Parameters" |> React.string}
+
+
+
+ }
+ {list_elements |> React.list}
+
+
+
+
;
}
\ No newline at end of file
diff --git a/src/utils/ParameterUtils.re b/src/utils/ParameterUtils.re
new file mode 100644
index 0000000..3c2c653
--- /dev/null
+++ b/src/utils/ParameterUtils.re
@@ -0,0 +1,66 @@
+open State;
+
+let concat_parameter_list = String.concat(" ");
+let concat_grouped_parameters = (parameters) => parameters |> List.map(concat_parameter_list) |> concat_parameter_list;
+
+let parameters_regex = Str.regexp({|\(--\(enable\|disable\) [-a-zA-Z0-9\._]+\)\|\(--set [-a-zA-Z0-9\._]+\(\[\(\+\|\-\)\]\)? \([-a-zA-Z0-9\._\/]+\|\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|\[\(\(\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\(\([ ]*,[ ]*\)\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\)*\)\|\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\)\|[-a-zA-Z0-9\._\/]*\)\]\)\)\|\(--[-a-zA-Z0-9\._]+\(\[\(\+\|\-\)\]\)? \([-a-zA-Z0-9\._\/]+\|\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|\[\(\(\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\(\([ ]*,[ ]*\)\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\)*\)\|\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\)\|[-a-zA-Z0-9\._\/]*\)\]\)\)|});
+let enable_disable_regex = Str.regexp({|--\(enable\|disable\) \([-a-zA-Z0-9\._]+\)|});
+let set_regex = Str.regexp({|--set \([-a-zA-Z0-9\._]+\(\[\(\+\|\-\)\]\)?\) \([-a-zA-Z0-9\._\/]+\|\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|\[\(\(\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\(\([ ]*,[ ]*\)\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\)*\)\|\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\)\|[-a-zA-Z0-9\._\/]*\)\]\)|});
+let other_regex = Str.regexp({|--\([-a-zA-Z0-9\._]+\(\[\(\+\|\-\)\]\)?\) \([-a-zA-Z0-9\._\/]+\|\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|\[\(\(\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\(\([ ]*,[ ]*\)\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\|[-a-zA-Z0-9\._\/]+\)\)*\)\|\(\("\|'\)[-a-zA-Z0-9\._, \!\?\$\*\/]*\("\|'\)\)\|[-a-zA-Z0-9\._\/]*\)\]\)|});
+let string_with_upticks_regex = Str.regexp({|'\(.*\)'|});
+
+// Reads Goblint's path and the parameters which were taken into consideration by Goblint for the first analysis
+let get_parameters = (state) => {
+ let raw_parameters = switch (Yojson.Safe.Util.member("command", state.meta)) {
+ | `String(command) => command
+ | _ => ""
+ }
+ |> String.split_on_char(' ')
+ |> List.map((s) => { String.sub(s, 1, String.length(s)-2) });
+
+ let goblint_path = raw_parameters |> List.hd;
+ let parameters = raw_parameters |> List.tl |> concat_parameter_list;
+
+ (goblint_path, parameters);
+};
+
+// Reads and extracts every parameter and returns additionally whether the input is malformed or well formed
+let construct_parameters = (parameters: string): (list(string), bool) => {
+ let replaced_words = ref([]);
+ let result = parameters |> Str.global_substitute(parameters_regex, (substring => {
+ replaced_words := replaced_words.contents |> List.cons(Str.matched_group(0, substring));
+ ""
+ }));
+
+ (replaced_words.contents, (result |> String.trim |> String.length) > 0)
+};
+
+let tuples_from_parameters = (grouped_parameters: list(string)): list((string,string)) => {
+ grouped_parameters
+ |> List.map((parameter) => {
+ // The string has to be compared to a regex instead of using String.starts_with as Str.matched_group is used in each case below; otherwise an exc will be raised
+ // Additionally, every Str.matched_group call has to be preceeded by an independent Str.string_match otherwise it takes the group or returns a wrong substring
+ // This order of cases was chosen because "--" matches to all and should be the last option therefore
+ if (Str.string_match(enable_disable_regex, parameter, 0)) {
+ switch ((Str.matched_group(1, parameter), Str.matched_group(2, parameter))) {
+ | ("enable", option) => Some((option, "true"))
+ | ("disable", option) => Some((option, "false"))
+ | exception Not_found => None
+ | _ => None
+ }
+ } else if (Str.string_match(set_regex, parameter, 0) || Str.string_match(other_regex, parameter, 0)) {
+ switch ((Str.matched_group(1, parameter), Str.matched_group(4, parameter))) {
+ | (option, value) => Some((option, value))
+ | exception Not_found => None
+ }
+ } else {
+ None
+ }
+ })
+ |> List.filter(e => !Option.is_none(e))
+ |> List.map(e => {
+ let (o,v) = (e |> Option.get);
+ let v' = Str.global_replace(string_with_upticks_regex, {|"\1"|}, v);
+ (o,v')
+ })
+};
\ No newline at end of file
diff --git a/src/utils/Time.re b/src/utils/Time.re
new file mode 100644
index 0000000..3dfe1b3
--- /dev/null
+++ b/src/utils/Time.re
@@ -0,0 +1,17 @@
+open Unix
+
+let time_to_string = (time) => {
+ let rawMinutes = string_of_int(time.tm_min);
+
+ rawMinutes
+ |> String.cat(if (String.length(rawMinutes) == 1) { "0" } else { "" })
+ |> String.cat(":")
+ |> String.cat(string_of_int(time.tm_hour))
+ |> String.cat(" ");
+};
+
+let get_local_time = () => {
+ Unix.time()
+ |> Unix.localtime
+ |> time_to_string;
+};
\ No newline at end of file