diff --git a/ivette/src/frama-c/client.ts b/ivette/src/frama-c/client.ts index 0398a92fcb28ac2ca8b61d8ff00a00d0141e059c..ae1acb242e98e7f18c14e371fdb9aeebf8bf1794 100644 --- a/ivette/src/frama-c/client.ts +++ b/ivette/src/frama-c/client.ts @@ -44,7 +44,7 @@ export abstract class Client { /** Signal ON */ abstract sigOn(id: string): void; - /** Signal ON */ + /** Signal OFF */ abstract sigOff(id: string): void; /** Kill Request */ @@ -143,6 +143,20 @@ export abstract class Client { this.events.emit('CONNECT', err); } + // -------------------------------------------------------------------------- + // --- CMDLINE Event + // -------------------------------------------------------------------------- + + /** Signal callback */ + onCmdLine(callback: (cmd: boolean) => void): void { + this.events.on('CMDLINE', callback); + } + + /** @internal */ + emitCmdLine(cmd: boolean): void { + this.events.emit('CMDLINE', cmd); + } + } // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/client_socket.ts b/ivette/src/frama-c/client_socket.ts index d8360cf31fda671bd4522387ff558273f71d1e35..4f6d62223cc1c431cc6019c41bde1bdf6719ebae 100644 --- a/ivette/src/frama-c/client_socket.ts +++ b/ivette/src/frama-c/client_socket.ts @@ -27,7 +27,7 @@ import { Client } from './client'; const D = new Debug('SocketServer'); -const RETRIES = 10; +const RETRIES = 30; const TIMEOUT = 200; // -------------------------------------------------------------------------- @@ -193,8 +193,14 @@ class SocketClient extends Client { default: D.warn('Unknown command', cmd); } - } else - D.warn('Misformed data', data); + } else { + switch (cmd) { + case 'CMDLINEON': this.emitCmdLine(true); break; + case 'CMDLINEOFF': this.emitCmdLine(false); break; + default: + D.warn('Misformed data', data); + } + } } catch (err) { D.warn('Misformed JSON', data, err); } diff --git a/ivette/src/frama-c/client_zmq.ts b/ivette/src/frama-c/client_zmq.ts index 45be4a2c04832c2079a0973c19bee13c8e0d8793..e66610de46cc555c1c555555398bd747303a7ad6 100644 --- a/ivette/src/frama-c/client_zmq.ts +++ b/ivette/src/frama-c/client_zmq.ts @@ -190,6 +190,12 @@ class ZmqClient extends Client { D.error(`ZMQ Protocol Error: ${err}`); } break; + case 'CMDLINEON': + this.emitCmdLine(true); + break; + case 'CMDLINEOFF': + this.emitCmdLine(false); + break; default: D.error(`Unknown Response: ${cmd}`); return; diff --git a/ivette/src/frama-c/server.ts b/ivette/src/frama-c/server.ts index 8906457a68102a451c932b45eb023dad1c1cc71d..a621798462c09a889bdb455af074f4205445c2bb 100644 --- a/ivette/src/frama-c/server.ts +++ b/ivette/src/frama-c/server.ts @@ -50,8 +50,10 @@ export enum Status { OFF = 'OFF', /** Server is starting, but not on yet. */ STARTING = 'STARTING', - /** Server is on. */ + /** Server is running. */ ON = 'ON', + /** Server is running command line. */ + CMD = 'CMD', /** Server is halting, but not off yet. */ HALTING = 'HALTING', /** Server is restarting. */ @@ -76,7 +78,7 @@ const STATUS = new Dome.Event<Status>('frama-c.server.status'); /** * Server is actually started and running. - * This event is emitted when ther server _enters_ the `ON` state. + * This event is emitted when ther server _enters_ the `ON` or `CMD` state. * The server is now ready to handle requests. */ const READY = new Dome.Event('frama-c.server.ready'); @@ -84,7 +86,7 @@ const READY = new Dome.Event('frama-c.server.ready'); /** * Server Status Notification Event - * This event is emitted when ther server _leaves_ the `ON` state. + * This event is emitted when ther server _leaves_ the `ON` or `CMD` state. * The server is no more able to handle requests until restart. */ const SHUTDOWN = new Dome.Event('frama-c.server.shutdown'); @@ -121,7 +123,7 @@ const pending = new Map<string, PendingRequest>(); let process: ChildProcess | undefined; /** Polling timeout when server is busy. */ -const pollingTimeout = 200; +const pollingTimeout = 50; let pollingTimer: NodeJS.Timeout | undefined; /** Killing timeout and timer for server process hard kill. */ @@ -154,11 +156,17 @@ export function useStatus(): Status { return status; } +const running = (st: Status): boolean => + (st === Status.ON || st === Status.CMD); + /** * Whether the server is running and ready to handle requests. - * @return {boolean} Whether server stage is [[ON]]. + * @return {boolean} Whether server is in running stage, + * defined by status `ON` or `CMD`. */ -export function isRunning(): boolean { return status === Status.ON; } +export function isRunning(): boolean { + return running(status); +} /** * Number of requests still pending. @@ -170,7 +178,7 @@ export function getPending(): number { /** * Register callback on `READY` event. - * @param {function} callback Invoked when the server enters [[ON]] stage. + * @param {function} callback Invoked when the server enters running stage. */ export function onReady(callback: () => void): void { READY.on(callback); @@ -178,7 +186,7 @@ export function onReady(callback: () => void): void { /** * Register callback on `SHUTDOWN` event. - * @param {function} callback Invoked when the server leaves [[ON]] stage. + * @param {function} callback Invoked when the server leaves running stage. */ export function onShutdown(callback: () => void): void { SHUTDOWN.on(callback); @@ -193,11 +201,15 @@ function _status(newStatus: Status): void { const oldStatus = status; status = newStatus; STATUS.emit(newStatus); - if (oldStatus === Status.ON) SHUTDOWN.emit(); - if (newStatus === Status.ON) READY.emit(); + const oldRun = running(oldStatus); + const newRun = running(newStatus); + if (oldRun && !newRun) SHUTDOWN.emit(); + if (!oldRun && newRun) READY.emit(); } } +const _update: () => void = debounce(() => STATUS.emit(status), 100); + // -------------------------------------------------------------------------- // --- Server Control (Start) // -------------------------------------------------------------------------- @@ -226,6 +238,7 @@ export async function start(): Promise<void> { _status(Status.RESTARTING); return; case Status.ON: + case Status.CMD: case Status.STARTING: return; default: @@ -252,6 +265,7 @@ export function stop(): void { _kill(); return; case Status.ON: + case Status.CMD: case Status.HALTING: _status(Status.HALTING); _shutdown(); @@ -283,6 +297,7 @@ export function stop(): void { export function kill(): void { switch (status) { case Status.ON: + case Status.CMD: case Status.HALTING: case Status.STARTING: case Status.RESTARTING: @@ -318,6 +333,7 @@ export function restart(): void { start(); return; case Status.ON: + case Status.CMD: _status(Status.RESTARTING); _shutdown(); return; @@ -477,6 +493,26 @@ async function _launch(): Promise<void> { client.connect(sockaddr); } +// -------------------------------------------------------------------------- +// --- Polling Management +// -------------------------------------------------------------------------- + +function _startPolling(): void { + if (!pollingTimer) { + const polling = (config && config.polling) || pollingTimeout; + pollingTimer = setInterval(() => { + client.poll(); + }, polling); + } +} + +function _stopPolling(): void { + if (pollingTimer) { + clearInterval(pollingTimer); + pollingTimer = undefined; + } +} + // -------------------------------------------------------------------------- // --- Low-level Killing // -------------------------------------------------------------------------- @@ -485,10 +521,7 @@ function _clear(): void { rqCount = 0; pending.forEach((p: PendingRequest) => p.reject('clear')); pending.clear(); - if (pollingTimer) { - clearTimeout(pollingTimer); - pollingTimer = undefined; - } + _stopPolling(); if (killingTimer) { clearTimeout(killingTimer); killingTimer = undefined; @@ -732,12 +765,8 @@ export function send<In, Out>( }); response.kill = () => pending.get(rid)?.reject('kill'); client.send(request.kind, rid, request.name, param as unknown as Json.json); - if (!pollingTimer) { - const polling = (config && config.polling) || pollingTimeout; - pollingTimer = setInterval(() => { - client.poll(); - }, polling); - } + _startPolling(); + _update(); return response; } @@ -747,20 +776,20 @@ export function send<In, Out>( function _resolved(id: string): void { pending.delete(id); - if (pending.size === 0) { + if (pending.size === 0 && status === Status.ON) { rqCount = 0; - if (pollingTimer) { - clearInterval(pollingTimer); - pollingTimer = undefined; - } + _stopPolling(); + _update(); } } client.onConnect((err?: Error) => { if (err) { _status(Status.FAILURE); + _clear(); } else { - _status(Status.ON); + _status(Status.CMD); + _startPolling(); } }); @@ -800,4 +829,14 @@ client.onSignal((id: string) => { _signal(id).emit(); }); +client.onCmdLine((cmd: boolean) => { + _status(cmd ? Status.CMD : Status.ON); + if (cmd) + _startPolling(); + else { + if (pending.size === 0) + _stopPolling(); + } +}); + // -------------------------------------------------------------------------- diff --git a/ivette/src/frama-c/states.ts b/ivette/src/frama-c/states.ts index 8c62b47cb54da7b22daf6334d739063e3ab3574a..ff7b0310ffc622958094a9f668b70a6edcf18bbb 100644 --- a/ivette/src/frama-c/states.ts +++ b/ivette/src/frama-c/states.ts @@ -304,6 +304,7 @@ class SyncState<A> { `Fail to set value of SyncState '${this.handler.name}'.`, `${error}`, ); + this.UPDATE.emit(); } } @@ -311,16 +312,18 @@ class SyncState<A> { try { this.upToDate = true; this.value = undefined; + this.UPDATE.emit(); if (Server.isRunning()) { const v = await Server.send(this.handler.getter, null); this.value = v; + this.UPDATE.emit(); } - this.UPDATE.emit(); } catch (error) { D.error( `Fail to update SyncState '${this.handler.name}'.`, `${error}`, ); + this.UPDATE.emit(); } } } diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx index 3cd1379e16540683b05593d2c40404040e45e1db..996c3c0878b8d1b8ae46831698fb6e446c77c0ce 100644 --- a/ivette/src/renderer/Controller.tsx +++ b/ivette/src/renderer/Controller.tsx @@ -160,6 +160,7 @@ export const Control = () => { play = { enabled: true, onClick: Server.start }; break; case Server.Status.ON: + case Server.Status.CMD: case Server.Status.FAILURE: stop = { enabled: true, onClick: Server.stop }; reload = { enabled: true, onClick: Server.restart }; @@ -372,36 +373,50 @@ export const Status = () => { const status = Server.useStatus(); const pending = Server.getPending(); let led: LEDstatus = 'inactive'; + let title = undefined; let icon = undefined; let running = 'OFF'; let blink = false; switch (status) { case Server.Status.OFF: + title = 'Server is off'; break; case Server.Status.STARTING: led = 'active'; blink = true; running = 'BOOT'; + title = 'Server is starting'; break; case Server.Status.ON: - led = pending > 0 ? 'positive' : 'active'; + led = 'active'; + blink = pending > 0; running = 'ON'; + title = 'Server is running'; + break; + case Server.Status.CMD: + led = 'positive'; + blink = true; + running = 'CMD'; + title = 'Command-line processing'; break; case Server.Status.HALTING: led = 'negative'; blink = true; running = 'HALT'; + title = 'Server is halting'; break; case Server.Status.RESTARTING: led = 'warning'; blink = true; running = 'REBOOT'; + title = 'Server is restarting'; break; case Server.Status.FAILURE: led = 'negative'; blink = true; running = 'ERR'; + title = 'Server halted because of failure'; icon = 'WARNING'; break; } @@ -409,7 +424,7 @@ export const Status = () => { return ( <> <LED status={led} blink={blink} /> - <Code icon={icon} label={running} /> + <Code icon={icon} label={running} title={title} /> <Toolbars.Separator /> </> ); diff --git a/src/plugins/server/main.ml b/src/plugins/server/main.ml index 29aac7f954eabee8cbecf1fbe404d82fe3d93239..006d4b9d15221f5cc3013df7bbb3e0edc4d647a3 100644 --- a/src/plugins/server/main.ml +++ b/src/plugins/server/main.ml @@ -25,6 +25,7 @@ (* -------------------------------------------------------------------------- *) module Senv = Server_parameters +module Signals = Set.Make(String) (* -------------------------------------------------------------------------- *) (* --- Registry --- *) @@ -69,6 +70,8 @@ type 'a response = [ | `Killed of 'a | `Signal of string | `Rejected of 'a + | `CmdLineOn + | `CmdLineOff ] type 'a message = { @@ -87,7 +90,10 @@ type 'a process = { mutable killed : bool ; } -module Sigs = Set.Make(String) +type 'a running = + | Idle (* Server is waiting for requests *) + | CmdLine (* Frama-C command line is running *) + | ExecRequest of 'a process (* Running EXEC process *) (* Server with request identifier (RqId) of type ['a] *) type 'a server = { @@ -95,13 +101,14 @@ type 'a server = { equal : 'a -> 'a -> bool ; (* RqId equality *) polling : int ; (* server polling, in milliseconds *) fetch : unit -> 'a message option ; (* fetch some client message *) - q_in : 'a process Queue.t ; (* queue of pending jobs *) + q_in : 'a process Queue.t ; (* queue of pending `EXEC and `GET jobs *) q_out : 'a response Queue.t ; (* queue of pending responses *) mutable daemon : Db.daemon option ; (* Db.yield daemon *) - mutable s_active : Sigs.t ; (* signals the client is listening to *) - mutable s_signal : Sigs.t ; (* emitted signals since last synchro *) + mutable s_active : Signals.t ; (* signals the client is listening to *) + mutable s_signal : Signals.t ; (* emitted signals since last synchro *) mutable shutdown : bool ; (* server has been asked to shut down *) - mutable running : 'a process option ; (* currently running EXEC request *) + mutable running : 'a running ; (* server running state *) + mutable cmdline : bool option ; (* cmdline signal management *) } exception Killed @@ -133,8 +140,18 @@ let pp_response pp fmt (r : _ response) = | `Rejected id -> Format.fprintf fmt "Rejected %a" pp id | `Killed id -> Format.fprintf fmt "Killed %a" pp id | `Signal sg -> Format.fprintf fmt "Signal %S" sg + | `CmdLineOn -> Format.pp_print_string fmt "CmdLine On" + | `CmdLineOff -> Format.pp_print_string fmt "CmdLine Off" | `Data(id,data) -> - Format.fprintf fmt "@[<hov 2>Replies [%a]@ %a@]" pp id Data.pretty data + if Senv.debug_atleast 3 then + Format.fprintf fmt "@[<hov 2>Response %a:@ %a@]" pp id Data.pretty data + else + Format.fprintf fmt "Replied %a" pp id + +let pp_running pp fmt = function + | Idle -> Format.pp_print_string fmt "Idle" + | CmdLine -> Format.pp_print_string fmt "CmdLine" + | ExecRequest { id } -> Format.fprintf fmt "ExectRequest [%a]" pp id (* -------------------------------------------------------------------------- *) (* --- Request Handling --- *) @@ -167,9 +184,7 @@ let execute server ?yield proc = ?on_delayed:(delayed proc.request) yield run proc | _ -> run proc - in - Senv.debug ~level:2 "%a" (pp_response server.pretty) resp ; - Queue.push resp server.q_out + in Queue.push resp server.q_out (* -------------------------------------------------------------------------- *) (* --- Signals --- *) @@ -203,37 +218,49 @@ let emit s = !emitter s (* --- 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 raise_if_killed = function + | Idle -> () + | CmdLine -> () + | ExecRequest { killed } -> if killed then raise Killed + +let kill_running ?id s = + match s.running with + | Idle -> () + | CmdLine -> if id = None then Db.cancel () + | ExecRequest p -> + match id with + | None -> p.killed <- true + | Some id -> if s.equal id p.id then p.killed <- true + +let kill_request eq id p = if eq id p.id then p.killed <- true let process_request (server : 'a server) (request : 'a request) : unit = - if Senv.debug_atleast 1 && (Senv.debug_atleast 3 || request <> `Poll) then + if Senv.debug_atleast 1 && (Senv.debug_atleast 2 || request <> `Poll) then Senv.debug "%a" (pp_request server.pretty) request ; match request with | `Poll -> () | `Shutdown -> begin - Option.iter kill_exec server.running ; + kill_running server ; Queue.clear server.q_in ; Queue.clear server.q_out ; server.shutdown <- true ; end | `SigOn sg -> begin - server.s_active <- Sigs.add sg server.s_active ; + server.s_active <- Signals.add sg server.s_active ; notify sg true ; end | `SigOff sg -> begin - server.s_active <- Sigs.remove sg server.s_active ; + server.s_active <- Signals.remove sg server.s_active ; notify sg false ; end | `Kill id -> begin + kill_running ~id server ; let set_killed = kill_request server.equal id in Queue.iter set_killed server.q_in ; - Option.iter set_killed server.running ; end | `Request(id,request,data) -> begin @@ -260,16 +287,27 @@ let process_request (server : 'a server) (request : 'a request) : unit = (* -------------------------------------------------------------------------- *) let communicate server = + Senv.debug ~level:3 "fetch" ; match server.fetch () with | None -> false | Some message -> + Senv.debug ~level:2 "message(s) received" ; 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 Queue.iter (fun r -> pool := r :: !pool) server.q_out ; + Option.iter + (fun cmd -> + pool := (if cmd then `CmdLineOn else `CmdLineOff) :: !pool ; + ) server.cmdline ; + pool := List.rev !pool ; Queue.clear server.q_out ; - server.s_signal <- Sigs.empty ; + server.cmdline <- None ; + server.s_signal <- Signals.empty ; + Senv.debug ~level:2 "response(s) callback" ; + if Senv.debug_atleast 2 then + List.iter (Senv.debug "%a" (pp_response server.pretty)) !pool ; message.callback !pool ; Option.iter raise error ; true @@ -279,13 +317,13 @@ let communicate server = (* -------------------------------------------------------------------------- *) let do_yield server () = - Option.iter raise_if_killed server.running ; + raise_if_killed server.running ; ignore ( communicate server ) let do_signal server s = - if Sigs.mem s server.s_active && not (Sigs.mem s server.s_signal) then + if Signals.mem s server.s_active && not (Signals.mem s server.s_signal) then begin - server.s_signal <- Sigs.add s server.s_signal ; + server.s_signal <- Signals.add s server.s_signal ; Queue.push (`Signal s) server.q_out ; end @@ -302,14 +340,14 @@ let rec fetch_exec q = let process server = match fetch_exec server.q_in with | None -> communicate server - | Some proc -> - server.running <- Some proc ; + | Some exec -> + server.running <- ExecRequest exec ; try - execute server ~yield:(do_yield server) proc ; - server.running <- None ; + execute server ~yield:(do_yield server) exec ; + server.running <- Idle ; true with exn -> - server.running <- None ; + server.running <- Idle ; raise exn (* -------------------------------------------------------------------------- *) @@ -331,10 +369,11 @@ let create ~pretty ?(equal=(=)) ~fetch () = fetch ; polling ; equal ; pretty ; q_in = Queue.create () ; q_out = Queue.create () ; - s_active = Sigs.empty ; - s_signal = Sigs.empty ; + s_active = Signals.empty ; + s_signal = Signals.empty ; daemon = None ; - running = None ; + running = Idle ; + cmdline = None ; shutdown = false ; } @@ -342,49 +381,72 @@ let create ~pretty ?(equal=(=)) ~fetch () = (* --- Start / Stop --- *) (* -------------------------------------------------------------------------- *) +(* public API ; shall be scheduled at command line main stage *) let start server = - emitter := do_signal server ; - match server.daemon with - | Some _ -> () - | None -> - begin - Senv.feedback "Server enabled." ; - let daemon = - Db.on_progress - ~debounced:server.polling - ?on_delayed:(delayed "command line") - (do_yield server) - in - server.daemon <- Some daemon ; - set_active true ; - end - + begin + Senv.debug ~level:2 "Server started (was %a)" + (pp_running server.pretty) server.running ; + server.running <- CmdLine ; + server.cmdline <- Some true ; + emitter := do_signal server ; + match server.daemon with + | Some _ -> () + | None -> + begin + Senv.feedback "Server enabled." ; + let daemon = + Db.on_progress + ~debounced:server.polling + ?on_delayed:(delayed "command line") + (do_yield server) + in + server.daemon <- Some daemon ; + set_active true ; + end + end + +(* public API ; can be invoked to force server shutdown *) let stop server = - emitter := nop ; - match server.daemon with - | None -> () - | Some daemon -> - begin - Senv.feedback "Server disabled." ; - server.daemon <- None ; - Db.off_progress daemon ; - set_active false ; - end - + begin + Senv.debug ~level:2 "Server stopped (was %a)" + (pp_running server.pretty) server.running ; + kill_running server ; + emitter := nop ; + match server.daemon with + | None -> () + | Some daemon -> + begin + Senv.feedback "Server disabled." ; + server.daemon <- None ; + server.running <- Idle ; + server.cmdline <- None ; + Db.off_progress daemon ; + set_active false ; + end + end + +(* internal only ; invoked by run when command line is finished *) let foreground server = - emitter := do_signal server ; - match server.daemon with - | None -> () - | Some daemon -> - begin - server.daemon <- None ; - Db.off_progress daemon ; - end + begin + Senv.debug ~level:2 "Server foreground (was %a)" + (pp_running server.pretty) server.running ; + server.running <- Idle ; + server.cmdline <- Some false ; + emitter := do_signal server ; + match server.daemon with + | None -> () + | Some daemon -> + begin + server.daemon <- None ; + Db.off_progress daemon ; + end + end (* -------------------------------------------------------------------------- *) (* --- Main Loop --- *) (* -------------------------------------------------------------------------- *) +(* public API ; shall be invoked at command line normal exit *) let run server = try ( (* TODO: catch-break to be removed once Why3 signal handler is fixed *) diff --git a/src/plugins/server/main.mli b/src/plugins/server/main.mli index 167692bbafdef2279b32f3594f1091de5771d413..9b649da2166f9c06ffd33a6bec2a75261f80c7f0 100644 --- a/src/plugins/server/main.mli +++ b/src/plugins/server/main.mli @@ -68,6 +68,8 @@ type 'a response = [ | `Killed of 'a | `Rejected of 'a | `Signal of string + | `CmdLineOn + | `CmdLineOff ] (** A paired request-response message. @@ -89,19 +91,38 @@ val create : fetch:(unit -> 'a message option) -> unit -> 'a server -(** Run the server forever. - The function will {i not} return until the server is actually shut down. *) -val run : 'a server -> unit - (** Start the server in background. - The function returns immediately after installing a daemon that accepts GET - requests received by the server on calls to [Db.yield()]. *) + The function returns immediately after installing a daemon that (only) + accepts GET requests received by the server on calls to [Db.yield()]. + + Shall be scheduled at command line main stage {i via} + [Db.Main.extend] extension point. +*) val start : 'a server -> unit -(** Stop the server if it is running in background. *) +(** Stop the server if it is running in background. + + Can be invoked to force server shutdown at any time. + + It shall be typically scheduled {i via} [Extlib.safe_at_exit] along with + other system cleanup operations to make sure the server is property + shutdown before Frama-C main process exits. +*) val stop : 'a server -> unit +(** Run the server forever. + The server would now accept any kind of requests and start handling them. + While executing an [`EXEC] request, the server would + continue to handle (only) [`GET] pending requests on [Db.yield()] + at every [server.polling] time interval. + + The function will {i not} return until the server is actually shutdown. + + Shall be scheduled at normal command line termination {i via} + [Cmdline.at_normal_exit] extension point. *) +val run : 'a server -> unit + (** Kill the currently running request by raising an exception. *) val kill : unit -> 'a diff --git a/src/plugins/server/server_socket.ml b/src/plugins/server/server_socket.ml index 13453b4fb283d1556e6192b922c024795d6251a7..b0f2e5b03fa47ea1dbd7bad43e8a5b960e564a90 100644 --- a/src/plugins/server/server_socket.ml +++ b/src/plugins/server/server_socket.ml @@ -41,6 +41,15 @@ module Socket = Senv.String Finally, the server is executed until shutdown." end) +let () = Parameter_customize.set_group socket_group +module SocketSize = Senv.Int + (struct + let option_name = "-server-socket-size" + let arg_name = "n" + let default = 256 + let help = "Control the size of socket buffers (in ko, default 256)." + end) + let _ = Server_doc.protocole ~title:"Unix Socket Protocol" ~readme:"server_socket.md" @@ -57,29 +66,42 @@ type channel = { bsnd : Buffer.t ; (* SND data buffer, accumulated *) } -let feed_bytes { sock ; rcv ; brcv } = - try - (* rcv buffer is only used locally *) - let s = Bytes.length rcv in - let n = Unix.read sock rcv 0 s in - Buffer.add_subbytes brcv rcv 0 n ; - with Unix.Unix_error((EAGAIN|EWOULDBLOCK),_,_) -> () +let read_bytes { sock ; rcv ; brcv } = + (* rcv buffer is only used locally *) + let s = Bytes.length rcv in + let rec scan p = + (* try to fill RCV buffer *) + let n = + try Unix.read sock rcv p (s-p) + with Unix.Unix_error((EAGAIN|EWOULDBLOCK),_,_) -> 0 + in + let p = p + n in + if n > 0 && p < s then scan p else p + in + let n = scan 0 in + if n > 0 then Buffer.add_subbytes brcv rcv 0 n let send_bytes { sock ; snd ; bsnd } = - try - (* snd buffer is only used locally *) - let n = Buffer.length bsnd in - if n > 0 then - let s = Bytes.length snd in - let w = min n s in - Buffer.blit bsnd 0 snd 0 w ; - let r = Unix.single_write sock snd 0 w in - if r > 0 then - (* TODO[LC]: inefficient move. Requires a ring-buffer. *) - let rest = Buffer.sub bsnd r (n-r) in - Buffer.reset bsnd ; - Buffer.add_string bsnd rest - with Unix.Unix_error((EAGAIN|EWOULDBLOCK),_,_) -> () + (* snd buffer is only used locally *) + let n = Buffer.length bsnd in + if n > 0 then + let s = Bytes.length snd in + let rec send p = + (* try to flush BSND buffer *) + let w = min (n-p) s in + Buffer.blit bsnd p snd 0 w ; + let r = + try Unix.single_write sock snd 0 w + with Unix.Unix_error((EAGAIN|EWOULDBLOCK),_,_) -> 0 + in + let p = p + r in + if r > 0 && p < n then send p else p + in + let p = send 0 in + if p > 0 then + let tail = Buffer.sub bsnd p (n-p) in + Buffer.reset bsnd ; + Buffer.add_string bsnd tail (* -------------------------------------------------------------------------- *) (* --- Data Chunks Encoding --- *) @@ -118,7 +140,6 @@ let write_data ch data = in Buffer.add_string ch.bsnd hex ; Buffer.add_string ch.bsnd data ; - send_bytes ch ; end (* -------------------------------------------------------------------------- *) @@ -167,6 +188,8 @@ let encode (resp : string Main.response) : string = | `Signal id -> `Assoc [ "res", `String "SIGNAL" ; "id", `String id ] + | `CmdLineOn -> `String "CMDLINEON" + | `CmdLineOff -> `String "CMDLINEOFF" in Yojson.Basic.to_string ~std:false js let parse ch = @@ -190,13 +213,14 @@ let callback ch rs = | data -> write_data ch data | exception err -> Senv.debug "Socket: encoding error %S@." (Printexc.to_string err) - ) rs + ) rs ; + send_bytes ch let commands ch = begin - feed_bytes ch ; + read_bytes ch ; match parse ch with - | [] -> None + | [] -> send_bytes ch ; None | requests -> Some Main.{ requests ; callback = callback ch } end @@ -214,14 +238,28 @@ let close (s: socket) = s.channel <- None ; Unix.close ch.sock +let set_socket_size sock opt s = + begin + let nbytes = s * 1024 in + (try Unix.setsockopt_int sock opt nbytes + with Unix.Unix_error(err,_,_) -> + let msg = Unix.error_message err in + Senv.warning ~once:true + "Invalid socket size (%d: %s)" nbytes msg) ; + Unix.getsockopt_int sock opt + end + let channel (s: socket) = match s.channel with | Some _ as chan -> chan | None -> try let sock,_ = Unix.accept ~cloexec:true s.socket in - let snd = Unix.getsockopt_int sock SO_SNDBUF in - let rcv = Unix.getsockopt_int sock SO_RCVBUF in + Unix.set_nonblock sock ; + let size = SocketSize.get () in + let rcv = set_socket_size sock SO_RCVBUF size in + let snd = set_socket_size sock SO_SNDBUF size in + Senv.debug ~level:2 "Socket size in:%d out:%d@." rcv snd ; Senv.debug "Client connected" ; let ch = Some { sock ; @@ -245,12 +283,10 @@ let fetch (s:socket) () = Senv.warning "Socket: exn %s" (Printexc.to_string exn) ; close s ; None -let bind fd = +let establish_server fd = let socket = { socket = fd ; channel = None } in try - Unix.set_nonblock fd ; Unix.listen fd 1 ; - Unix.set_nonblock fd ; ignore (Sys.signal Sys.sigpipe Signal_ignore) ; let pretty = Format.pp_print_string in let server = Main.create ~pretty ~fetch:(fetch socket) () in @@ -276,17 +312,16 @@ let bind fd = let server = ref None let cmdline () = - let addr = Socket.get () in - match !server with - | Some addr0 -> - if Senv.debug_atleast 1 && addr <> addr0 then + let option = match Socket.get () with "" -> None | a -> Some a in + match !server, option with + | _ , None -> () + | Some addr0, Some addr -> + if addr0 <> addr then Senv.warning "Socket server already running on [%s]." addr0 - else - Senv.feedback "Socket server already running." - | None -> - if addr <> "" then + | None, Some addr -> + begin try - server := Some addr ; + server := option ; if Sys.file_exists addr then Unix.unlink addr ; let fd = Unix.socket PF_UNIX SOCK_STREAM 0 in Unix.bind fd (ADDR_UNIX addr) ; @@ -294,10 +329,11 @@ let cmdline () = Senv.feedback "Socket server running on [%s]." addr else Senv.feedback "Socket server running." ; - bind fd + establish_server fd ; with exn -> Senv.fatal "Server socket failed.@\nError: %s@" (Printexc.to_string exn) + end let () = Db.Main.extend cmdline diff --git a/src/plugins/server/server_zmq.ml b/src/plugins/server/server_zmq.ml index d6c4ad728f20218cd05d1f26f2cd802ac3fcfc02..d19dd35c8d7ee38d02a3eda1147331b929221ae3 100644 --- a/src/plugins/server/server_zmq.ml +++ b/src/plugins/server/server_zmq.ml @@ -111,6 +111,8 @@ let rec encode = function | `Killed id :: w -> "KILLED" :: id :: encode w | `Rejected id :: w -> "REJECTED" :: id :: encode w | `Signal sg :: w -> "SIGNAL" :: sg :: encode w + | `CmdLineOn :: w -> "CMDLINEON" :: encode w + | `CmdLineOff :: w -> "CMDLINEOFF" :: encode w | [] -> [] (* -------------------------------------------------------------------------- *) diff --git a/src/plugins/server/share/server.md b/src/plugins/server/share/server.md index 8d308a85a4d36f59e3c33607d981eec88fc2bd41..92163ccf852d3e68516ba74f438640d50f84416e 100644 --- a/src/plugins/server/share/server.md +++ b/src/plugins/server/share/server.md @@ -38,7 +38,7 @@ asynchronous entry points, requests are classified into three kinds: - `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 +computations shall repeatedly call the yielding routine `Db.yield()` 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 @@ -59,19 +59,25 @@ to the client. However, since a lot of signals might be emitted, the server must be aware of which signals the client is listening to. Signal are identified by unique strings. -The server and client can exchange two special commands to manage signals: +The server and client can exchange other special commands to manage signals: | Command | Issued by | Effect | |:--------|:---------:|:----------------------------| | `SIGON s` | client | start listening to signal `<s>` | | `SIGOFF s` | client | stop listening to signal `<s>` | | `SIGNAL s` | server | signal `<s>` has been emitted | +| `CMDLINEON` | server | execution of the Frama-C command line | +| `CMDLINEOFF` | server | termination of the Frama-C command line | When one or many requests emit some signal `<s>` several times, the client would be notified only once per cycle of data exchange. Signals will be notified in addition to responses or logical requests or server polling. +During the execution of the Frama-C command line, the server behaves just like +during an `EXEC` request: only `GET` requests are processed at `Db.yield()` calls +until the (normal) termination of the command line. + ## Transport Messages From the entry points layer, the asynchronous behavior of the Server makes @@ -89,7 +95,7 @@ paired _intput messages_ and _output messages_. Each single input message consis a list of _commands_: | Commands | Parameters | Description | -|:--------:|:----------:|:------------| +|:---------|:-----------|:------------| | `POLL` | - | Ask for pending responses and signals, if any | | `GET` | `id,request,data` | En-queue the given GET request | | `SET` | `id,request,data` | En-queue the given SET request | @@ -103,12 +109,14 @@ 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 | | `SIGNAL` | `id` | The identified signal has been emitted since last exchange | | `KILLED` | `id` | The identified request has been killed or interrupted | | `REJECTED` | `id` | The identified request was not registered on the Server | +| `CMDLINEON` | - | The command line has started | +| `CMDLINEOFF` | - | The command line is terminated | The logic layer makes no usage of _identifiers_ and simply pass them unchanged into output messages in response to received requests. @@ -131,9 +139,21 @@ 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. +To integrate your own server into the Frama-C command-line framework, you need to +provide an implementation of the non-blocking `fetch` function and create a server +with `Server.create`. Then, you shall: + +- Schedule `Server.start myServer` during the main plug-in extension phase _via_ + `Db.Main.extend`; + +- Schedule `Server.run myServer` at normal command line termination phase _via_ + `Cmdline.at_normal_exit`; + +- Schedule `Server.stop myServer` with other cleaning operations at exit phase + _via_ `Extlib.at_exit_safe`. + +This way, your server will integrate well with the command line execution of +Frama-C and the other plug-ins. ## Request Implementations diff --git a/src/plugins/server/share/server_socket.md b/src/plugins/server/share/server_socket.md index c43990c8cea51c2ee65bfd10ad0227af1038b038..04b580e658790f58fc0483928db8ad91dd1182e6 100644 --- a/src/plugins/server/share/server_socket.md +++ b/src/plugins/server/share/server_socket.md @@ -54,6 +54,8 @@ An output message chunk consists of a single response encoded as follows: | `KILLED(id)` | `{ res = 'KILLED', id }` | | `REJECTED(id)` | `{ res = 'REJECTED', id }` | | `SIGNAL(id)` | `{ res = 'SIGNAL', id }` | +| `CMDLINEON` | `"CMDLINEON"` | +| `CMDLINEOFF` | `"CMDLINEOFF"` | The special last case is used when the server is busy or died or some low-level error occurs. diff --git a/src/plugins/server/share/server_zmq.md b/src/plugins/server/share/server_zmq.md index 67999c67a5900e251f1340f539d1c447dad6b0a7..63e1d7e830a658a2653b3871efacccebf73a3703 100644 --- a/src/plugins/server/share/server_zmq.md +++ b/src/plugins/server/share/server_zmq.md @@ -45,6 +45,8 @@ of each reply is a finel string identifying the reply: | `KILLED(id)` | 2 | `"KILLED"` | id | | | `REJECTED(id)` | 2 | `"REJECTED"` | id | | | `SIGNAL(id)` | 2 | `"SIGNAL"` | id | | +| `CMDLINEON` | 1 | `"CMDLINEON"` | | | +| `CMDLINEOFF` | 1 | `"CMDLINEOFF"` | | | | (special) | 2 | `"WRONG"` | message | | | (special) | 1 | `"NONE"` | | |