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