import { ANALYTICS_PATH, ZEROMQ_CONNECTION_STRING, ANLYTICS_PING_INTERVAL } from '../config' const zmq = require('zeromq'); import * as childProcess from 'child_process' import * as fs from 'fs'; import * as path from 'path'; export class AnalyticsService { private _requester: any; private _ready: boolean = false; private _pingResponded = false; constructor(private _onResponse: (response: any) => void) { this._init(); } public async sendTask(msgObj: any): Promise { if(!this._ready) { return Promise.reject("Analytics is not ready"); } let message = JSON.stringify(msgObj); return this.sendMessage(message); } public async sendMessage(message: string): Promise { return new Promise((resolve, reject) => { this._requester.send(message, undefined, (err) => { if(err) { reject(err); } else { resolve(); } }); }); } public close() { // TODO: close socket & terminate process if you have any this._requester.close(); } public get ready(): boolean { return this._ready; } private async _init() { this._requester = zmq.socket('pair'); if(process.env.NODE_ENV !== 'development') { console.log('Creating analytics process...'); try { var cp = await AnalyticsService._runAnalyticsProcess(); } catch(error) { console.error('Can`t run analytics process: %s', error); return; } console.log('Ok, pid: %s', cp.pid); } console.log("Binding to zmq...: %s", ZEROMQ_CONNECTION_STRING); this._requester.connect(ZEROMQ_CONNECTION_STRING); this._requester.on("message", this._onAnalyticsMessage.bind(this)); console.log('Ok'); console.log('Start analytics pinger...'); this._runAlalyticsPinger(); console.log('Ok'); } /** * Spawns analytics process. Reads process stderr and fails if it * is not empty. No need to stop process later. * * @returns creaded child process */ private static async _runAnalyticsProcess(): Promise { let cp: childProcess.ChildProcess; if(fs.existsSync(path.join(ANALYTICS_PATH, 'dist/worker/worker'))) { console.log('dist/worker/worker'); cp = childProcess.spawn('dist/worker/worker', [], { cwd: ANALYTICS_PATH }); } else { console.log('python3 server.py'); // If compiled analytics script doesn't exist - fallback to regular python console.log(ANALYTICS_PATH); cp = childProcess.spawn('python3', ['server.py'], { cwd: ANALYTICS_PATH }); } if(cp.pid === undefined) { return new Promise((resolve, reject) => { cp.on('error', reject); }); } return new Promise((resolve, reject) => { var resolved = false; cp.stdout.on('data', () => { if(resolved) { return; } else { resolved = true; } resolve(cp); }); cp.stderr.on('data', function(data) { if(resolved) { return; } else { resolved = true; } reject(data); }); }); } private _onAnalyticsUp() { console.log('Analytics is up'); } private async _onAnalyticsDown() { console.log('Analytics is down'); if(process.env.NODE_ENV !== 'development') { await AnalyticsService._runAnalyticsProcess(); } } private _onAnalyticsMessage(text: any, error) { if(text.toString() === 'pong') { this._pingResponded = true; if(!this._ready) { this._ready = true; this._onAnalyticsUp(); } return; } console.log(`analytics message: "${text}"`); let response; try { response = JSON.parse(text); } catch (e) { console.error("Can`t parse response from analytics as json:"); console.error(text); throw new Error('Unexpected response'); } this._onResponse(response); } private async _runAlalyticsPinger() { setInterval(() => { if(!this._pingResponded && this._ready) { this._ready = false; this._onAnalyticsDown(); } this._pingResponded = false; // TODO: set life limit for this ping this.sendMessage('ping'); }, ANLYTICS_PING_INTERVAL); } }