Add a file to be renamed

This commit is contained in:
Xuewen Wang 2024-02-26 21:12:09 +00:00
parent 5fe94d786c
commit d08df4a5c0

765
opscore-utility-process.js Normal file
View file

@ -0,0 +1,765 @@
/////////////////////////////////////////////////////////////////////////////////////////////////
// Name: datastore/ui/electron/opscore-utility-process.js
// Purpose: Node utility process that does the communication with nativa C++ code
// Created: 2023/12/01
// Author: Sergio Ferreira
// Copyright: (c) 2023-2024 ITO 33 SA
//////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Utility process where opscore native modules are loaded
* and where it provides database access that then is available to
* be executed by the renderer
*
* It acts as a JSON RPC server, from where it receives the
* intention of the client to execute a Swig exported function with
* paraneters.
*
* Execute the native function and return the result using JSON RPC
* If an exception ocurr, return it to the client using JSON RPC
*
* TODO : This should be bullet proof agains crashes. When it crash, renderer just lost the connection
*/
const path = require('node:path');
const jayson = require('jayson');
const { exit } = require('node:process');
const { parseArgs } = require('node:util');
/**
* Implement of the execution of a function called by the client.
*
* The call to C++ is synchronous, so we just need to worry to
* receive and return something.
*
* Jayson (JSON RPC) implementation take care of the order
* in the channel
*/
class UtilityServerStub {
mod = null;
functionCallConfig = null;
server = null;
logger = null;
constructor() {
}
start() {
this.parseArguments();
this.initLog();
this.logger.info('STARTED');
this.handleProcessLifecycle();
this.mod = new OpscoreNativeModules(this.logger);
this.removeSocketIfExists();
this.server = new jayson.server(this.createFunctionCallConfig());
this.server.tcp().listen(this.getPipePath());
}
/**
* Initialize the logging
*/
initLog() {
const winston = require('winston');
if ( ! this.loglevel ) this.loglevel = "info";
let transports;
if ( this.logFileName ) {
let logName = `/tmp/${this.logFileName}`;
if (process.platform === 'win32')
logName = `c:\\Windows\\Temp\\${logFileName}`;
transports = [ new winston.transports.File({
filename: logName
}) ];
} else {
transports = [
new winston.transports.Console({
level: this.loglevel,
handleExceptions: true,
json: false,
colorize: true
})
]
}
const winstonConfig = {
level: this.loglevel,
format: winston.format.json(),
transports
};
this.logger = winston.createLogger(winstonConfig);
}
/**
* Parse command line arguments used to launch utility process
*/
parseArguments() {
let args = process.argv.slice(1);
if ( process.argv[0].endsWith('node') )
args = process.argv.slice(2);
const options = {
'loglevel': {
type: 'string',
short: 'l'
},
'logfilename': {
type: 'string'
}
};
try {
const { values, positionals } = parseArgs( {args, options});
if ( values.loglevel ) {
this.loglevel = values.loglevel
}
if ( values.logfilename ) {
this.logFileName = values.logfilename;
}
} catch(e) {
console.error(e.message);
}
}
/**
* Add handlers that take care of process lifecycle
* including removing sockets
*/
handleProcessLifecycle() {
process.on('beforeExit', function () {
// console.log('beforeExit fired')
})
// This is callled if signals are received (TODO : test in windows)
process.on('exit', function () {
exitUtilityProcess('EXIT', 0, 'Exit Fired');
})
// signals
process.on('SIGUSR1', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGUSR');
})
process.on('SIGTERM', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGTERM');
})
process.on('SIGPIPE', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGTERM');
})
process.on('SIGHUP', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGHUP');
})
process.on('SIGTERM', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGTERM');
})
process.on('SIGINT', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGINT');
})
process.on('SIGBREAK', function () {
exitUtilityProcess('SIGNAL', 1, 'SIGBREAK');
})
}
/**
* Create a pipe file according to the OS
* @returns The name to be used in the pipe
*/
getPipePath() {
// return(3100)
let socketSufix = "";
if (process.argv.length >= 3) {
socketSufix = "." + process.argv[2];
}
if (process.platform === 'win32')
return "\\\\.\\pipe\\opscore" + socketSufix + ".gui";
return "/tmp/opscore-gui" + socketSufix + ".socket";
}
removeSocketIfExists() {
const fs = require('fs');
if ( fs.existsSync(this.getPipePath())) {
fs.unlinkSync(this.getPipePath());
}
}
/**
* Create the object that is passed to Jayson (JSON RPC framework)
* to execute the proper swig function and return the values through JSON RPC
*
* @returns The configuration object
*/
createFunctionCallConfig() {
const writerFunctions = Object.getOwnPropertyNames(Object.getPrototypeOf(this.mod.writer));
writerFunctions.push(...Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(this.mod.writer))));
writerFunctions.push(...Object.getOwnPropertyNames(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this.mod.writer)))))
const jsonRPCCalls = {};
const getFunctions = writerFunctions.filter(f => f.startsWith('Get'));
for (const getFunction of getFunctions) {
const functionKey = 'datastore.writer.' + getFunction;
const functionName = getFunction;
jsonRPCCalls[functionKey] = this.getExecutingNativeGetFunction(functionName);
}
const loadFunctions = writerFunctions.filter(f => f.startsWith('Load'));
for (const loadFunction of loadFunctions) {
const functionKey = 'datastore.writer.' + loadFunction;
const functionName = loadFunction;
jsonRPCCalls[functionKey] = this.getExecutingNativeLoadFunction(functionName);
}
const saveFunctions = writerFunctions.filter(f => f.startsWith('Save'));
for (const saveFunction of saveFunctions) {
const functionKey = 'datastore.writer.' + saveFunction;
const functionName = saveFunction;
jsonRPCCalls[functionKey] = this.getExecutingNativeSaveFunction(functionName);
}
const deleteFunctions = writerFunctions.filter(f => f.startsWith('Delete'));
for (const deleteFunction of deleteFunctions) {
const functionKey = 'datastore.writer.' + deleteFunction;
const functionName = deleteFunction;
jsonRPCCalls[functionKey] = this.getExecutingNativeDeleteFunction(functionName);
}
// Add functions
const addFunctions = writerFunctions.filter(f => f.startsWith('Add'));
for (const addFunction of addFunctions) {
const functionName = 'datastore.writer.' + addFunction;
jsonRPCCalls[functionName] = (args, callback) => {
try {
eval('this.mod.writer.' + addFunction + '("' + args.value + '")');
callback(null, args.value);
} catch (error) {
this.logger.error(`Error executing function ${addFunction} with parameters: ${args}`);
this.logger.error(`message - ${error.message}, stack trace - ${error.stack}`);
const retException = { code: 5, message: error.message };
callback(retException, null);
}
}
}
// Enumerateds
jsonRPCCalls['datastore.enumerated'] = (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const enumValues = new SwigEnumerated(this.mod,args).getEnumValues();
callback(null, enumValues);
} catch (error) {
callback(error, null);
}
}
// Top level functions
const datastoreFunctions = [ "StringToExtIdsProvidersType" ];
for (const datastoreFunction of datastoreFunctions) {
jsonRPCCalls['datastore.function.' + datastoreFunction] = (args, callback) => {
try {
const retVal = eval('this.mod.datastore.' + 'StringToExtIdsProvidersType' + '(' + this.genArgsForEval(args) + ')');
callback(null, retVal);
} catch (error) {
const retException = { code: 6, message: error.message };
callback(retException, null);
}
}
}
// XML Dump
jsonRPCCalls['datastore.XMLDumper'] = (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const xmlString = this.dumpXML(args);
callback(null, xmlString);
} catch (error) {
callback(error, null);
}
}
jsonRPCCalls['PING'] = (args, callback) => {
callback(null, 'PONG');
}
return jsonRPCCalls;
}
/**
* Generate a string containing the args that will be necessary
* to execute a function with eval
*
* @param {*} args The args to be generated
* @return The arguments string
*/
genArgsForEval(args) {
if ( !args ) return "";
let evalArgs = "";
for (const arg in args ) {
if ( evalArgs !== "") evalArgs += ',';
if (isNaN(args[arg])) {
evalArgs += '"' + args[arg] + '"';
} else {
evalArgs += args[arg];
}
}
return evalArgs;
}
/**
* Execute a get function that get the list of objects
* @param {*} functionName Name of the get function
*
* @returns The objects returned by swig as array
*/
executeGet = (functionName, args) => {
const swigObjects = eval('this.mod.writer.' + functionName + '(' + this.genArgsForEval(args) + ')');
let arrayObjects;
if (swigObjects.constructor.name.toString().endsWith('Collection')) {
arrayObjects = [];
for (let i = 0; i < swigObjects.size(); i++) {
// Create a copy of the element since swigObjects will be garbage collected
// that invalidates the C++ item in the container
arrayObjects.push(JSON.parse(JSON.stringify(swigObjects.get(i))));
}
} else if (typeof swigObjects[Symbol.iterator] === 'function') {
arrayObjects = Array.from(swigObjects);
} else {
arrayObjects = [swigObjects];
}
this.logger.silly(`Function ${functionName} going to return : ${JSON.stringify(arrayObjects)}`);
return arrayObjects;
}
/**
* Execute one of the SWIG exported functions that load an object
* @param {*} functionName A string containing the name of the function
* @param {*} args Arguments used to load the objecty (normally id)
* @returns The returned values
*/
executeLoad = (functionName, args) => {
return eval('this.mod.writer.' + functionName + '(args).json()');
}
/**
* Execute one of the SWIG exported functions that save an object in the datastore
* @param {*} functionName A string containing the name of the function
* @param {*} args The aditional information to save. Should have the name
* of the class and the object to save in the form { className, loadFunction, objectData }
* @returns The returned values
*/
executeSave = (functionName, args) => {
let swigObject;
this.logger.debug(`Executing ${functionName} with ${JSON.stringify(args)}`);
if ( ! args.loadFunction ) {
swigObject = eval('new this.mod.datastore.' + args.className + '()');
} else {
swigObject = eval('this.mod.writer.' + args.loadFunction + '(args.objectData.id)');
}
swigObject.fromJson(JSON.stringify(args.objectData));
eval('this.mod.writer.' + functionName + '(swigObject)');
this.logger.debug(`Executed ${functionName} for object id=${swigObject.id}`);
return swigObject.id;
}
/**
* Execute one of the SWIG exported functions that remove an object from datastore
* @param {*} functionName A string containing the name of the function
* @param {*} args The aditional information to save. Should have the name
* of the class and the object to save in the form { className, objectData }
* @returns The returned values
*/
executeDelete = (functionName, args) => {
let evalArgs;
if (isNaN(args)) { evalArgs = '"' + args + '"'; }
else { evalArgs = args; }
eval('this.mod.writer.' + functionName + '(' + evalArgs + ')');
}
/**
* This function returns a function that do the proper
* execution of writer Get<something> function
* @param nativeFunctionName
*/
getExecutingNativeGetFunction = function(nativeFunctionName) {
return (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const objects = this.executeGet(nativeFunctionName,args);
callback(null, objects);
} catch (error) {
const retException = { code: 1, message: error.message };
callback(retException, null);
}
}
}
/**
* Return a reference to the function that will execute the SWIG corresponding
* load function
*
* @param {*} nativeFunctionName String containing the name of the function
* @returns The reference to the load function
*/
getExecutingNativeLoadFunction = function(nativeFunctionName) {
return (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const objects = this.executeLoad(nativeFunctionName, args.id);
this.logger.silly(`Function ${nativeFunctionName} going to return : ${JSON.stringify(objects)}`);
callback(null, objects);
} catch (error) {
const retException = { code: 2, message: error.message };
this.logger.debug(`Error executing Function ${nativeFunctionName}: ${error.message}`);
callback(retException, null);
}
}
}
/**
* Return a reference to the function that will execute the SWIG corresponding
* save function
*
* @param {*} nativeFunctionName String containing the name of the function
* @returns The reference to the load function
*/
getExecutingNativeSaveFunction = function(nativeFunctionName) {
return (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const objects = this.executeSave(nativeFunctionName, args);
callback(null, objects);
} catch (error) {
const retException = { code: 3, message: error.message };
this.logger.debug(`Error executing Function ${nativeFunctionName}: ${error.message}`);
callback(retException, null);
}
}
}
/**
* Return a reference to the function that will execute the SWIG corresponding
* save function
*
* @param {*} nativeFunctionName String containing the name of the function
* @returns The reference to the load function
*/
getExecutingNativeDeleteFunction = function(nativeFunctionName) {
return (args, callback) => {
if ( this.mod.error ) {
callback(this.mod.error, null);
}
try {
const objects = this.executeDelete(nativeFunctionName, args.id);
callback(null, objects);
} catch (error) {
const retException = { code: 4, message: error.message };
this.logger.debug(`Error executing Function ${nativeFunctionName}: ${error.message}`);
callback(retException, null);
}
}
}
/**
* Dump a list of object IDs to a string containing XML
*
* @param {*} args Contain an array with a list of ids and
* the name of the addXML specific function
*/
dumpXML(args) {
const xmlDumper = new this.mod.datastore.XMLDumper(this.mod.writer);
if (args.ids === undefined) {
throw new Error('Need an object id to add to XML dumper')
}
let evalArgs
args.ids.forEach((id) => {
if (isNaN(id)) { evalArgs = '"' + id + '"'; }
else { evalArgs = id; }
eval('xmlDumper.' + args.addIdFunctionName + '(' + evalArgs + ')');
});
return xmlDumper.GetXML();
}
}
/**
* Utilities to Handle the enumerateds exported by swig
*/
class SwigEnumerated {
nativeModule = null;
typePrefix = null;
subNamespace = null;
useEnumNameAsId = false;
useEnumNameWithoutPrefixAsId = false; // Strip prefix from the description
constructor(nativeModule, args) {
this.nativeModule = nativeModule;
this.typePrefix = args.typePrefix;
this.subNamespace = args.subNamespace;
this.useEnumNameAsId = args.useEnumNameAsId;
this.useEnumNameWithoutPrefixAsId = args.useEnumNameWithoutPrefixAsId;
}
/**
* Obtain the enumerated values according to the prefix
*/
getEnumValues() {
const values = [];
for (const enumSwigName in this.getEnumNamespace()) {
if (!enumSwigName.startsWith(this.typePrefix)) {
continue;
}
const enumAsObj = {
id: this.getId(enumSwigName),
name: this.getEnumName(enumSwigName)
}
values.push(enumAsObj);
}
return values;
}
/**
* Transform the string of the enum in something
* readable and mostly used in the lists
* (Can be overriden in the DAO(s))
*/
getEnumName(swigEnumName) {
const s = swigEnumName.substring(this.typePrefix.length, swigEnumName.length);
let name = s.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/([A-Z])([a-z])/g, ' $1$2')
.replace(/\ +/g, ' ')
.replace(/([0-9]+)([A-Z])/g, '$1 $2')
.replace(/([a-z])([0-9]+)/g, '$1 $2')
.trim();
name = name.replace(/_/g, ' ').trim();
return name;
}
/**
* Some enums are defined inside classes in C++
* To ontain it, we need to define the namespace where it exists
*/
getEnumNamespace() {
if ( this.subNamespace ) {
return this.nativeModule.datastore[this.subNamespace];
}
return this.nativeModule.datastore;
}
/**
* Get the unique id that will be used to uniquely identify a value
* @param propertyName Name of the property
* @returns The ID used
*/
getId(propertyName) {
if (this.objects) {
return this.objects;
}
if ( this.useEnumNameWithoutPrefixAsId ) {
return propertyName.slice(this.typePrefix.length);
}
if ( this.useEnumNameAsId ) {
return propertyName;
}
if ( this.subNamespace ) {
return this.nativeModule.datastore[this.subNamespace][propertyName];
}
return this.nativeModule.datastore[propertyName];
}
}
/**
* Allows writing for/of iteration loops on top of datastore iterators like StrIdIterator
* or NumIdIterator.
*
* Example:
* for ( const country of makeIterator(reader.GetSortedCountries()) ) {
* console.log(country.Name);
* console.log(country.Id);
* }
*
* Another example running through the iterator:
*
* const iter = makeIterator(reader.GetSortedCountries());
* let result = iter.next();
* while ( ! result.done ) {
* console.log(result.value.Name);
* console.log(result.value.Id);
* result = iter.next();
* }
*
* @param baseIterator iterator coming from the Reader (ie. GetSortedCountries/GetSortedCurrencies etc)
* @returns an iterator ready to use in for/of loops.
*/
function makeIterator(baseIterator) {
baseIterator.reset();
const iter = {
next() {
const result = baseIterator.next();
return { value: result, done: result == null }
},
[Symbol.iterator]() {
return this;
}
};
return iter;
}
/**
* Encapsulate the basic operations with opscore native modules
*/
class OpscoreNativeModules {
datastore = null;
common = null;
writer = null;
error = null;
logger = null;
iteratorGetters = [
'GetCountriesByCurrency',
'GetDepositaryReceiptsByEquity',
'GetGovernmentBillsByCountry',
'GetGovernmentBondsByCountry',
'GetListingsByEquity',
'GetModelInputsFor',
'GetRateInstruments',
'GetUniverseByIssuer',
'GetYieldCurvesForCurrency',
'GetYieldCurvesForCurrencyWithResidenceType',
'GetSortedBondWithWarrants',
'GetSortedBonds',
'GetSortedCBOptions',
'GetSortedCDSs',
'GetSortedCallWarrants',
'GetSortedCashRates',
'GetSortedCommonStocks',
'GetSortedConvertibleBonds',
'GetSortedCountries',
'GetSortedCurrencies',
'GetSortedCurrencyForwards',
'GetSortedCurrencyRates',
'GetSortedDefaultUniverseByIssuer',
'GetSortedDeltaOneInputs',
'GetSortedDepositaryReceipts',
'GetSortedEDSs',
'GetSortedEquitiesForIssuer',
'GetSortedEquityBaskets',
'GetSortedEquityListings',
'GetSortedGovernmentBills',
'GetSortedGovernmentBonds',
'GetSortedIRSwaps',
'GetSortedIssuers',
'GetSortedListedRegulatoryModels',
'GetSortedMandatories',
'GetSortedMarkets',
'GetSortedModelInputs',
'GetSortedMoneyMarkets',
'GetSortedOptions',
'GetSortedPrivateEquities',
'GetSortedPrivateRegulatoryModels',
'GetSortedReferenceCDSs',
'GetSortedReferenceIRFutures',
'GetSortedReferenceIRSwaps',
'GetSortedRegulatoryCapitals',
'GetSortedUniverseByEquity',
'GetSortedYieldCurves'
];
/**
* Inject iterators on functions the only have
* an iteration with get(idx)
*/
injectIterators() {
let proto = this.datastore['Writer'].prototype;
for (const getter in proto) {
if (this.iteratorGetters.indexOf(getter) != -1) {
const oldFn = proto[getter];
const wrapper = function() {
return makeIterator(oldFn.apply(this, arguments));
};
proto[getter] = wrapper;
}
}
}
/**
* Load the native modules and create a Writer Object
*
* TODO : If the native module was not loaded, some error should be
* sent to main and this one should be properly show to the user
*/
constructor(logger) {
this.logger = logger;
const modLocation = path.join(__dirname, '/../pkg/datastore.node');
try {
this.datastore = require(modLocation);
this.logger.info(`MODULE_LOADED: ${modLocation}`);
} catch (error) {
this.logger.error(`Error loading native module at: ${modLocation}`);
this.logger.error(`message - ${error.message}, stack trace - ${error.stack}`);
exitUtilityProcess('ERROR_LOADING_MODULE', 1, error.message);
}
try {
this.writer = new this.datastore.Writer();
this.logger.info(`Connected to the database`)
this.injectIterators();
} catch (error) {
const msg = 'Error connecting to database : ' + error.message;
this.logger.error(`Error connecting to the database (createe Writer()): ${modLocation}`);
this.logger.error(`message - ${error.message}, stack trace - ${error.stack}`);
exitUtilityProcess('ERROR_CONNECTING', 1, msg);
}
}
}
/**
* Exit from the process in the most gracious maner it can
* @param {*} reason The reason why is going
* @param {*} status Exist status that should be returned by the process
*/
function exitUtilityProcess(reason, status, aditionalInfo) {
switch(reason) {
case 'ERROR_LOADING_MODULE':
console.log(reason);
if ( aditionalInfo )
console.error(aditionalInfo);
break;
case 'ERROR_CONNECTING':
console.log(reason);
console.log(aditionalInfo);
if ( aditionalInfo )
console.error(aditionalInfo);
break;
case 'SIGNAL':
console.log('SIGNAL ' + reason + ' RECEIVED');
exit(status);
break;
case 'EXIT':
console.log(reason);
}
utilityServerStub.removeSocketIfExists();
process.exitCode = status;
utilityServerStub.logger.on('finish', function () {
logger.end();
});
}
// Native Module Utility Main
const utilityServerStub = new UtilityServerStub();
try {
utilityServerStub.start();
} catch (e) {
if (utilityServerStub.logger) {
utilityServerStub.logger.error('Error ocurred : ' + e.message);
} else {
console.error('Error ocurred : ' + e.message);
}
}