From 54a68f4a8ef682a2cf348107756ec64cd5bb36aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr> Date: Thu, 16 Dec 2021 12:58:50 +0100 Subject: [PATCH] [ivette/server] factorize common services into client --- ivette/src/frama-c/client.ts | 108 +++++++++-- ivette/src/frama-c/client_socket.ts | 91 +++++---- ivette/src/frama-c/server.ts | 274 ++++++++++++++-------------- ivette/src/renderer/Controller.tsx | 77 ++++---- 4 files changed, 306 insertions(+), 244 deletions(-) diff --git a/ivette/src/frama-c/client.ts b/ivette/src/frama-c/client.ts index d9cd89613c4..15b8e1cbe37 100644 --- a/ivette/src/frama-c/client.ts +++ b/ivette/src/frama-c/client.ts @@ -20,58 +20,128 @@ /* */ /* ************************************************************************ */ +import Emitter from 'events'; import { json } from 'dome/data/json'; // -------------------------------------------------------------------------- // --- Frama-C Server Access (Client side) // -------------------------------------------------------------------------- -export interface Client { +export abstract class Client { /** Server CLI */ - commandLine(sockaddr: string, params: string[]): string[]; + abstract commandLine(sockaddr: string, params: string[]): string[]; /** Connection */ - connect(addr: string): void; + abstract connect(addr: string): void; /** Disconnection */ - disconnect(): void; + abstract disconnect(): void; /** Send Request */ - send(kind: string, id: string, request: string, data: any): void; + abstract send(kind: string, id: string, request: string, data: any): void; /** Signal ON */ - sigOn(id: string): void; + abstract sigOn(id: string): void; /** Signal ON */ - sigOff(id: string): void; + abstract sigOff(id: string): void; /** Kill Request */ - kill(id: string): void; + abstract kill(id: string): void; /** Polling */ - poll(): void; + abstract poll(): void; /** Shutdown the server */ - shutdown(): void; + abstract shutdown(): void; + + /** @internal */ + private events = new Emitter(); + + // -------------------------------------------------------------------------- + // --- DATA Event + // -------------------------------------------------------------------------- /** Request data callback */ - onData(callback: (id: string, data: json) => void): void; + onData(callback: (id: string, data: json) => void): void { + this.events.on('DATA', callback); + } + + /** @internal */ + emitData(id: string, data: json): void { + this.events.emit('DATA', id, data); + } + + // -------------------------------------------------------------------------- + // --- REJECTED Event + // -------------------------------------------------------------------------- + + /** Rejected request callback */ + onRejected(callback: (id: string) => void): void { + this.events.on('REJECTED', callback); + } + + /** @internal */ + emitRejected(id: string): void { + this.events.emit('REJECTED', id); + } + + // -------------------------------------------------------------------------- + // --- ERROR Event + // -------------------------------------------------------------------------- /** Rejected request callback */ - onRejected(callback: (id: string, msg: string) => void): void; + onError(callback: (id: string, msg: string) => void): void { + this.events.on('ERROR', callback); + } + + /** @internal */ + emitError(id: string, msg: string): void { + this.events.emit('ERROR', id, msg); + } + + // -------------------------------------------------------------------------- + // --- KILLED Event + // -------------------------------------------------------------------------- /** Killed request callback */ - onKilled(callback: (id: string) => void): void; + onKilled(callback: (id: string) => void): void { + this.events.on('KILLED', callback); + } - /** Signal callback */ - onSignal(callback: (id: string) => void): void; + /** @internal */ + emitKilled(id: string): void { + this.events.emit('KILLED', id); + } - /** Error callback */ - onError(callback: (msg: string) => void): void; + // -------------------------------------------------------------------------- + // --- SIGNAL Event + // -------------------------------------------------------------------------- - /** Idle callback */ - onIdle(callback: () => void): void; + /** Signal callback */ + onSignal(callback: (id: string) => void): void { + this.events.on('SIGNAL', callback); + } + + /** @internal */ + emitSignal(id: string): void { + this.events.emit('SIGNAL', id); + } + + // -------------------------------------------------------------------------- + // --- CONNECT Event + // -------------------------------------------------------------------------- + + /** Connection callback */ + onConnect(callback: (err?: Error) => void): void { + this.events.on('CONNECT', callback); + } + + /** @internal */ + emitConnect(err?: Error): void { + this.events.emit('CONNECT', err); + } } diff --git a/ivette/src/frama-c/client_socket.ts b/ivette/src/frama-c/client_socket.ts index cab6beb75b7..07ecc231026 100644 --- a/ivette/src/frama-c/client_socket.ts +++ b/ivette/src/frama-c/client_socket.ts @@ -22,21 +22,24 @@ import Net from 'net'; import { Debug } from 'dome'; -import Emitter from 'events'; import { json } from 'dome/data/json'; import { Client } from './client'; const D = new Debug('SocketServer'); +const RETRIES = 10; +const TIMEOUT = 200; + // -------------------------------------------------------------------------- // --- Frama-C Server API // -------------------------------------------------------------------------- -class SocketClient implements Client { +class SocketClient extends Client { - events = new Emitter(); + retries = 0; running = false; socket: Net.Socket | undefined; + timer: NodeJS.Timeout | undefined; queue: json[] = []; buffer: Buffer = Buffer.from(''); @@ -47,26 +50,49 @@ class SocketClient implements Client { /** Connection */ connect(sockaddr: string): void { + this.retries++; if (this.socket) { this.socket.destroy(); } - this.socket = Net.createConnection(sockaddr, () => { - D.log('Client connected'); + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } + const s = Net.createConnection(sockaddr, () => { + D.log('Client connected', this.retries); this.running = true; + this.retries = 0; + this.emitConnect(); this._flush(); }); // Using Buffer data encoding at this level - this.socket.on('end', () => this.disconnect()); - this.socket.on('data', (data: Buffer) => this._receive(data)); - this.socket.on('error', (err: Error) => { - D.warn('Socket error', err); + s.on('end', () => this.disconnect()); + s.on('data', (data: Buffer) => this._receive(data)); + s.on('error', (err: Error) => { + s.destroy(); + if (this.retries <= RETRIES && !this.running) { + D.log('Retry', this.retries, '/', RETRIES); + this.socket = undefined; + this.timer = setTimeout(() => this.connect(sockaddr), TIMEOUT); + } else { + D.warn('Socket error', err.toString()); + this.running = false; + this.emitConnect(err); + } }); + this.socket = s; } disconnect(): void { + D.log('Disconnect'); this.queue = []; + this.retries = 0; + this.running = false; + if (this.timer) { + clearTimeout(this.timer); + this.timer = undefined; + } if (this.socket) { - D.log('Client disconnected'); this.socket.destroy(); this.socket = undefined; } @@ -108,36 +134,6 @@ class SocketClient implements Client { this._flush(); } - /** Request data callback */ - onData(callback: (id: string, data: json) => void): void { - this.events.on('DATA', callback); - } - - /** Rejected request callback */ - onRejected(callback: (id: string, err: string) => void): void { - this.events.on('REJECT', callback); - } - - /** Request error callback */ - onError(callback: (msg: string) => void): void { - this.events.on('ERROR', callback); - } - - /** Killed request callback */ - onKilled(callback: (id: string) => void): void { - this.events.on('KILL', callback); - } - - /** Signal callback */ - onSignal(callback: (id: string) => void): void { - this.events.on('SIGNAL', callback); - } - - /** Signal callback */ - onIdle(callback: () => void): void { - this.events.on('IDLE', callback); - } - // -------------------------------------------------------------------------- // --- Low-Level Management // -------------------------------------------------------------------------- @@ -158,11 +154,12 @@ class SocketClient implements Client { const hex = Number(len).toString(16).toUpperCase(); const padding = '0000000000000000'; const header = - len < 0xFFF ? 'S' + padding.substring(hex.length, 3) : - len < 0xFFFFFFF ? 'L' + padding.substring(hex.length, 7) : + len <= 0xFFF ? 'S' + padding.substring(hex.length, 3) : + len <= 0xFFFFFFF ? 'L' + padding.substring(hex.length, 7) : 'W' + padding.substring(hex.length, 15); s.write(Buffer.from(header + hex)); s.write(data); + console.log('SENT', header + hex + data.toString('utf8'), data.length); } } @@ -191,11 +188,11 @@ class SocketClient implements Client { const cmd: any = JSON.parse(data); if (cmd !== null && typeof (cmd) === 'object') { switch (cmd.res) { - case 'DATA': this.events.emit('DATA', cmd.id, cmd.data); break; - case 'ERROR': this.events.emit('ERROR', cmd.msg); break; - case 'KILLED': this.events.emit('KILLED', cmd.id); break; - case 'REJECTED': this.events.emit('REJECT', cmd.id); break; - case 'SIGNAL': this.events.emit('SIGNAL', cmd.id); break; + case 'DATA': this.emitData(cmd.id, cmd.data); break; + case 'ERROR': this.emitError(cmd.id, cmd.msg); break; + case 'KILLED': this.emitKilled(cmd.id); break; + case 'REJECTED': this.emitRejected(cmd.id); break; + case 'SIGNAL': this.emitSignal(cmd.id); break; default: D.warn('Unknown command', cmd); } diff --git a/ivette/src/frama-c/server.ts b/ivette/src/frama-c/server.ts index bcd535cff6e..5386ff3dcb5 100644 --- a/ivette/src/frama-c/server.ts +++ b/ivette/src/frama-c/server.ts @@ -25,12 +25,12 @@ // -------------------------------------------------------------------------- /** - * Manage the current Frama-C server/client interface - * @packageDocumentation - * @module frama-c/server + Manage the current Frama-C server/client interface + @packageDocumentation + @module frama-c/server */ -import _ from 'lodash'; +import { debounce } from 'lodash'; import React from 'react'; import * as Dome from 'dome'; import * as System from 'dome/system'; @@ -88,7 +88,7 @@ export class SIGNAL extends Dome.Event { // -------------------------------------------------------------------------- /** Server stages. */ -export enum Stage { +export enum Status { /** Server is off. */ OFF = 'OFF', /** Server is starting, but not on yet. */ @@ -103,58 +103,32 @@ export enum Stage { FAILURE = 'FAILURE', } -export interface OkStatus { - readonly stage: - Stage.OFF | Stage.ON | Stage.STARTING | Stage.RESTARTING | Stage.HALTING; -} - -export interface ErrorStatus { - readonly stage: Stage.FAILURE; - /** Failure message. */ - readonly error: string; -} - -export type Status = OkStatus | ErrorStatus; - -function okStatus( - s: Stage.OFF | Stage.ON | Stage.STARTING | Stage.RESTARTING | Stage.HALTING, -) { - return { stage: s }; -} - -function errorStatus(error: string): ErrorStatus { - return { stage: Stage.FAILURE, error }; -} - -export function hasErrorStatus(s: Status): s is ErrorStatus { - return (s as ErrorStatus).error !== undefined; -} - // -------------------------------------------------------------------------- // --- Server Global State // -------------------------------------------------------------------------- /** The current server status. */ -let status: Status = okStatus(Stage.OFF); +let status: Status = Status.OFF; /** Request counter. */ let rqCount = 0; -type IndexedPair<T, U> = { - [index: string]: [T, U]; -}; - -type ResolvePromise = (value: Json.json) => void; -type RejectPromise = (error: Error) => void; - -/** Pending promise callbacks (pairs of (resolve, reject)). */ -let pending: IndexedPair<ResolvePromise, RejectPromise> = {}; +/** Pending Requests. */ +interface PendingRequest { + resolve: (data: Json.json) => void; + reject: () => void; +} +const pending = new Map<string, PendingRequest>(); /** Server process. */ let process: ChildProcess | undefined; +/** Polling timeout when server is busy. */ +//const pollingTimeout = 200; +let pollingTimer: NodeJS.Timeout | undefined; + /** Killing timeout and timer for server process hard kill. */ -const killingTimeout = 300; +const killingTimeout = 500; let killingTimer: NodeJS.Timeout | undefined; // -------------------------------------------------------------------------- @@ -187,14 +161,14 @@ export function useStatus(): Status { * Whether the server is running and ready to handle requests. * @return {boolean} Whether server stage is [[ON]]. */ -export function isRunning(): boolean { return status.stage === Stage.ON; } +export function isRunning(): boolean { return status === Status.ON; } /** * Number of requests still pending. * @return {number} Pending requests. */ export function getPending(): number { - return _.reduce(pending, (n) => n + 1, 0); + return pending.size; } /** @@ -214,16 +188,13 @@ export function onShutdown(callback: () => void) { SHUTDOWN.on(callback); } // -------------------------------------------------------------------------- function _status(newStatus: Status) { - if (Dome.DEVEL && hasErrorStatus(newStatus)) { - D.error(newStatus.error); - } - if (newStatus !== status) { + D.log('Server', newStatus); const oldStatus = status; status = newStatus; STATUS.emit(newStatus); - if (oldStatus.stage === Stage.ON) SHUTDOWN.emit(); - if (newStatus.stage === Stage.ON) READY.emit(); + if (oldStatus === Status.ON) SHUTDOWN.emit(); + if (newStatus === Status.ON) READY.emit(); } } @@ -239,23 +210,20 @@ function _status(newStatus: Status) { * - Otherwise, the Frama-C server is spawned. */ export async function start() { - switch (status.stage) { - case Stage.OFF: - case Stage.FAILURE: - case Stage.RESTARTING: - _status(okStatus(Stage.STARTING)); + switch (status) { + case Status.OFF: + case Status.FAILURE: + case Status.RESTARTING: + _status(Status.STARTING); try { await _launch(); - _status(okStatus(Stage.ON)); } catch (error) { - const msg = '' + error; - D.error(msg); - buffer.append(msg, '\n'); - _exit(msg); + buffer.log('[frama-c]', error); + _exit(false); } return; - case Stage.HALTING: - _status(okStatus(Stage.RESTARTING)); + case Status.HALTING: + _status(Status.RESTARTING); return; default: return; @@ -271,22 +239,18 @@ export async function start() { * * - If the server is starting, it is hard killed. * - If the server is running, it is shutdown gracefully. - * - If the server is restarting, restart is canceled. * - Otherwise, this is a no-op. */ export function stop() { - switch (status.stage) { - case Stage.STARTING: - _status(okStatus(Stage.HALTING)); + switch (status) { + case Status.STARTING: + _status(Status.HALTING); _kill(); return; - case Stage.ON: - _status(okStatus(Stage.HALTING)); + case Status.ON: + _status(Status.HALTING); _shutdown(); return; - case Stage.RESTARTING: - _status(okStatus(Stage.HALTING)); - return; default: return; } @@ -304,12 +268,12 @@ export function stop() { * - Otherwise, this is a no-op. */ export function kill() { - switch (status.stage) { - case Stage.STARTING: - case Stage.ON: - case Stage.HALTING: - case Stage.RESTARTING: - _status(okStatus(Stage.HALTING)); + switch (status) { + case Status.STARTING: + case Status.ON: + case Status.HALTING: + case Status.RESTARTING: + _status(Status.HALTING); _kill(); return; default: @@ -330,17 +294,17 @@ export function kill() { * - Otherwise, this is a no-op. */ export function restart() { - switch (status.stage) { - case Stage.OFF: - case Stage.FAILURE: + switch (status) { + case Status.OFF: + case Status.FAILURE: start(); return; - case Stage.ON: - _status(okStatus(Stage.RESTARTING)); + case Status.ON: + _status(Status.RESTARTING); _shutdown(); return; - case Stage.HALTING: - _status(okStatus(Stage.RESTARTING)); + case Status.HALTING: + _status(Status.RESTARTING); return; default: return; @@ -359,11 +323,12 @@ export function restart() { * - Otherwise, this is a no-op. */ export function clear() { - switch (status.stage) { - case Stage.FAILURE: - case Stage.OFF: + switch (status) { + case Status.FAILURE: + case Status.OFF: buffer.clear(); - _status(okStatus(Stage.OFF)); + _clear(); + _status(Status.OFF); return; default: return; @@ -478,19 +443,17 @@ async function _launch() { if (signal) { // [signal] is non-null. - buffer.log('Signal:', signal); - const error = `Process terminated by the signal ${signal}`; - _exit(error); + buffer.log('[frama-c]', signal); + _exit(false); return; } // [signal] is null, hence [code] is non-null (cf. NodeJS doc). if (code) { - buffer.log('Exit:', code); - const error = `Process exited with code ${code}`; - _exit(error); + buffer.log('[frama-c] exit', code); + _exit(false); } else { // [code] is zero: normal exit w/o error. - _exit(); + _exit(true); } }); // Connect to Server @@ -501,32 +464,29 @@ async function _launch() { // --- Low-level Killing // -------------------------------------------------------------------------- -function _reset() { - D.log('Reset to initial configuration'); - +function _clear() { rqCount = 0; - _.forEach(pending, ([, reject]) => reject(new Error('Server reset'))); - pending = {}; - + pending.forEach((p: PendingRequest) => p.reject()); + pending.clear(); + if (pollingTimer) { + clearTimeout(pollingTimer); + pollingTimer = undefined; + } if (killingTimer) { clearTimeout(killingTimer); killingTimer = undefined; } - } function _kill() { D.log('Hard kill'); - client.disconnect(); - if (process) { - process.kill(); - } + if (process) process.kill(); } async function _shutdown() { D.log('Shutdown'); - _reset(); + _clear(); client.shutdown(); const killingPromise = new Promise((resolve) => { if (!killingTimer) { @@ -542,17 +502,14 @@ async function _shutdown() { await killingPromise; } -function _exit(error?: string) { - _reset(); +function _exit(error: boolean) { + _clear(); client.disconnect(); process = undefined; - if (status.stage === Stage.RESTARTING) { + if (status === Status.RESTARTING) { setImmediate(start); - } else if (error) { - _status(errorStatus(error)); - } else { - _status(okStatus(Stage.OFF)); } + _status(error ? Status.FAILURE : Status.OFF); } // -------------------------------------------------------------------------- @@ -572,7 +529,7 @@ class SignalHandler { this.active = false; this.listen = false; this.sigon = this.sigon.bind(this); - this.sigoff = this.handler = _.debounce(this.sigoff.bind(this), 1000); + this.sigoff = this.handler = debounce(this.sigoff.bind(this), 1000); this.unplug = this.unplug.bind(this); } @@ -615,6 +572,10 @@ class SignalHandler { } } + emit() { + this.event.emit(); + } + unplug() { this.listen = false; this.handler.cancel(); @@ -634,8 +595,6 @@ function _signal(id: string): SignalHandler { return s; } -client.onSignal((id: string) => { _signal(id).event.emit(); }); - // --- External API /** @@ -646,7 +605,7 @@ client.onSignal((id: string) => { _signal(id).event.emit(); }); * @param {string} id The signal identifier to listen to. * @param {function} callback The callback to call upon signal. */ -export function onSignal(s: Signal, callback: any) { +export function onSignal(s: Signal, callback: () => void) { _signal(s.name).on(callback); } @@ -658,7 +617,7 @@ export function onSignal(s: Signal, callback: any) { * @param {string} id The signal identifier that was listen to. * @param {function} callback The callback to remove. */ -export function offSignal(s: Signal, callback: any) { +export function offSignal(s: Signal, callback: () => void) { _signal(s.name).off(callback); } @@ -667,7 +626,7 @@ export function offSignal(s: Signal, callback: any) { * @param {string} id The signal identifier to listen to. * @param {function} callback The callback to call upon signal. */ -export function useSignal(s: Signal, callback: any) { +export function useSignal(s: Signal, callback: () => void) { React.useEffect(() => { onSignal(s, callback); return () => { offSignal(s, callback); }; @@ -738,8 +697,8 @@ export function send<In, Out>( request: Request<RqKind, In, Out>, param: In, ): Response<Out> { - if (!isRunning()) return Promise.reject(new Error('Server not running')); - if (!request.name) return Promise.reject(new Error('Undefined request')); + if (!isRunning()) return Promise.reject('Server not running'); + if (!request.name) return Promise.reject('Undefined request'); const rid = `RQ.${rqCount}`; rqCount += 1; const response: Response<Out> = new Promise<Out>((resolve, reject) => { @@ -754,35 +713,70 @@ export function send<In, Out>( reject(err); } }; - pending[rid] = [decodedResolve, reject]; + pending.set(rid, { resolve: decodedResolve, reject }); }); - response.kill = () => { - if (pending[rid]) client.kill(rid); - }; + response.kill = () => pending.get(rid)?.reject(); client.send(request.kind, rid, request.name, param); return response; } +// -------------------------------------------------------------------------- +// --- Client Events +// -------------------------------------------------------------------------- + +function _resolved(id: string) { + pending.delete(id); + if (pending.size == 0) rqCount = 0; +} + +client.onConnect((err?: Error) => { + if (err) { + if (Dome.DEVEL) + buffer.log('[client]', err.toString()); + _status(Status.FAILURE); + } else { + if (Dome.DEVEL) + buffer.log('[client] Connected.'); + _status(Status.ON); + } +}); + client.onData((id: string, data: Json.json) => { - const [resolve] = pending[id]; - if (resolve) { - delete pending[id]; - resolve(data); + const p = pending.get(id); + if (p) { + p.resolve(data); + _resolved(id); + } +}); + +client.onKilled((id: string) => { + const p = pending.get(id); + if (p) { + p.reject(); + _resolved(id); + } +}); + +client.onRejected((id: string) => { + D.log('Rejected', id); + const p = pending.get(id); + if (p) { + p.reject(); + _resolved(id); } }); -client.onRejected((id: string, error: string) => { - const [, reject] = pending[id]; - if (reject) { - delete pending[id]; - reject(new Error(error)); +client.onError((id: string, msg: string) => { + D.log('Request error', id, msg); + const p = pending.get(id); + if (p) { + p.reject(); + _resolved(id); } }); -client.onIdle(() => { - const waiting = _.find(pending, () => true) !== undefined; - if (!waiting) - rqCount = 0; // No pending command nor pending response +client.onSignal((id: string) => { + _signal(id).emit(); }); // -------------------------------------------------------------------------- diff --git a/ivette/src/renderer/Controller.tsx b/ivette/src/renderer/Controller.tsx index 2fe67b5ed3b..6e7e38bc563 100644 --- a/ivette/src/renderer/Controller.tsx +++ b/ivette/src/renderer/Controller.tsx @@ -149,13 +149,13 @@ export const Control = () => { let stop = { enabled: false, onClick: () => { } }; let reload = { enabled: false, onClick: () => { } }; - switch (status.stage) { - case Server.Stage.OFF: - case Server.Stage.FAILURE: + switch (status) { + case Server.Status.OFF: play = { enabled: true, onClick: Server.start }; break; - case Server.Stage.ON: - stop = { enabled: true, onClick: Server.stop }; + case Server.Status.ON: + case Server.Status.FAILURE: + stop = { enabled: true, onClick: Server.clear }; reload = { enabled: true, onClick: Server.restart }; break; default: @@ -357,44 +357,45 @@ Ivette.registerView({ export const Status = () => { const status = Server.useStatus(); const pending = Server.getPending(); - let led: LEDstatus; - let blink; - let error; - - if (Server.hasErrorStatus(status)) { - led = 'negative'; - blink = false; - error = status.error; - } else { - switch (status.stage) { - case Server.Stage.OFF: - led = 'inactive'; - break; - case Server.Stage.STARTING: - led = 'active'; - blink = true; - break; - case Server.Stage.ON: - led = pending > 0 ? 'positive' : 'active'; - break; - case Server.Stage.HALTING: - led = 'negative'; - blink = true; - break; - case Server.Stage.RESTARTING: - led = 'warning'; - blink = true; - break; - default: - break; - } + let led: LEDstatus = 'inactive'; + let icon = undefined; + let running = false; + let blink = false; + + switch (status) { + case Server.Status.OFF: + break; + case Server.Status.STARTING: + led = 'active'; + blink = true; + running = true; + break; + case Server.Status.ON: + led = pending > 0 ? 'positive' : 'active'; + running = true; + break; + case Server.Status.HALTING: + led = 'negative'; + blink = true; + running = true; + break; + case Server.Status.RESTARTING: + led = 'warning'; + blink = true; + running = true; + break; + case Server.Status.FAILURE: + led = 'negative'; + blink = true; + running = false; + icon = 'WARNING'; + break; } return ( <> <LED status={led} blink={blink} /> - <Code label={status.stage} /> - {error && <Label icon="WARNING" label={error} />} + <Code icon={icon} label={running ? 'ON' : 'OFF'} /> <Toolbars.Separator /> </> ); -- GitLab