UNPKG

kevoree-core

Version:
372 lines (336 loc) 12.9 kB
const kevoree = require('kevoree-library'); const EventEmitter = require('events').EventEmitter; const adaptationsExecutor = require('./lib/adaptation-executor'); const NAME_PATTERN = /^[\w]+$/; /** * KevoreeCore is the kernel of the Kevoree JavaScript runtime * * @param {Resolver} resolver service to download the DeployUnits * @param {KevScript} kevscript service to interpret the KevScript * @param {LoggerFactory} loggerFactory service to create loggers * @throws {Error} undefined constructor params * @constructor */ function KevoreeCore(resolver, kevscript, loggerFactory) { if (!resolver || !kevscript || !loggerFactory) { throw new Error('KevoreeCore constructor needs: Resolver, KevScript engine and a LoggerFactory'); } this.resolver = resolver; this.loggerFactory = loggerFactory; this.log = loggerFactory.create('Core'); this.kevs = kevscript; this.stopping = false; this.currentModel = null; this.deployModel = null; this.nodeName = null; this.nodeInstance = null; this.firstBoot = true; this.scriptQueue = []; this.emitter = new EventEmitter(); } KevoreeCore.prototype = { /** * Starts the core * This method is synchronous. * Once started you can #deploy(...) models * * @param {string} nodeName the name of the node to bootstrap on * @throws {Error} invalid nodeName */ start(nodeName) { if (!nodeName || nodeName.length === 0) { nodeName = 'node0'; } if (nodeName.match(NAME_PATTERN)) { this.nodeName = nodeName; const factory = new kevoree.factory.DefaultKevoreeFactory(); this.currentModel = factory.createContainerRoot(); factory.root(this.currentModel); // create platform node const node = factory.createContainerNode(); node.name = this.nodeName; node.started = false; // add platform node this.currentModel.addNodes(node); // hang-on until the core is stopped if (process.stdin) { process.stdin.resume(); } this.log.info('Platform node name: ' + nodeName); } else { throw new Error('Platform node name must match this regex ' + NAME_PATTERN.toString()); } }, /** * Stops the core * @return {Promise} resolved when the core is stopped */ stop() { if (this.nodeInstance === null) { this.log.info('Stopping Kevoree...'); clearInterval(this.loopId); if (process.stdin) { process.stdin.emit('end'); } this.emitter.emit('stopped'); return Promise.resolve(); } else { const factory = new kevoree.factory.DefaultKevoreeFactory(); const cloner = factory.createModelCloner(); const stopModel = cloner.clone(this.currentModel, false); const node = stopModel.findNodesByID(this.nodeName); if (node.started) { this.log.info('Stopping Kevoree...'); node.started = false; const subNodes = node.hosts.iterator(); while (subNodes.hasNext()) { subNodes.next().delete(); } const groups = node.groups.iterator(); while (groups.hasNext()) { groups.next().delete(); } const bindings = stopModel.mBindings.iterator(); while (bindings.hasNext()) { const binding = bindings.next(); if (binding.port.eContainer() && binding.port.eContainer().eContainer() && binding.port.eContainer().eContainer().name === node.name) { if (binding.hub) { binding.hub.delete(); } } } const comps = node.components.iterator(); while (comps.hasNext()) { comps.next().delete(); } this.stopping = true; return this.deploy(stopModel) .then(() => { this.log.info('Platform stopped: ' + this.nodeInstance.getName()); if (process.stdin) { process.stdin.emit('end'); } this.emitter.emit('stopped'); }) .catch((err) => { this.log.error(err); this.log.error('Something went wrong while stopping Kevoree. Force stop.'); if (process.stdin) { process.stdin.emit('end'); } this.emitter.emit('stopped'); throw err; }); } else { return Promise.resolve(); } } }, /** * Deploy a model * Ask the node platform to create a delta between the current model and the * given one. The resulting delta must be a set of commands to sequentially * execute in order to move to the next state (described by the given model) * * @param {ContainerRoot} model the new state to converge to * @param {any} deprecatedCb this should not be set (it used to be callback-based but now it is Promise-based * so to prevent further error, setting this param will throw an Error) * @return {Promise} resolved when the model is deployed */ deploy(model, deprecatedCb) { if (typeof deprecatedCb !== 'undefined') { return Promise.reject(new Error('Core.deploy() is now Promise-based, please update your code accordingly')); } if (!this.deployModel) { this.emitter.emit('deploying', model); if (model && !model.findNodesByID(this.nodeName)) { return Promise.reject(new Error('Deploy model failure: unable to find ' + this.nodeName + ' in given model')); } else { this.log.debug((this.stopping ? 'Stopping' : 'Deploy') + ' process started...'); if (model) { // ensure node instance is bootstrapped before deploying return this.checkBootstrapNode(model) .then(() => { // monkey-patch model because of KMF monkeyPatchKMF(model); const factory = new kevoree.factory.DefaultKevoreeFactory(); // clone model so that adaptations won't modify the proposed one const cloner = factory.createModelCloner(); this.deployModel = cloner.clone(model, true); // set it read-only to ensure adaptations consistency this.deployModel.setRecursiveReadOnly(); // make a diff between the current model and the model to deploy const diffSeq = factory.createModelCompare().diff(this.currentModel, this.deployModel); let adaptations; try { // ask the node platform to create the needed adaptation commands adaptations = this.nodeInstance.processTraces(diffSeq, this.deployModel); } catch (err) { this.log.error(err); throw new Error('Something went wrong while planning adaptations'); } // execute adaptation commands return adaptationsExecutor(this, model, adaptations); }, (err) => { this.log.error(err); err = new Error('Bootstrap failed'); this.deployModel = null; // automatically stops the core return this.stop() // then throw the bootstrap failure error .then(() => { throw err; }); }) .catch((err) => { this.emitter.emit('error', err); throw err; }); } else { return Promise.reject(new Error('Model is not defined or null. Deploy aborted.')); } } } else { // TODO add the possibility to put new deployment in pending queue this.log.warn('New deploy process requested: aborted because another one is in process (retry later?)'); return Promise.reject(new Error('New deploy process requested: aborted because another one is in process (retry later?)')); } }, submitScript(script) { if (this.deployModel === null) { // good to go return this.kevs.parse(script, this.currentModel) .then(({ model }) => this.deploy(model)) .then(() => { this.log.info('KevScript submission succeeded'); }, (err) => { this.log.warn('KevScript submission threw an error'); this.log.warn(err.stack); throw err; }); } else { // in "deploying state" => need to queue request to process it afterwards const promise = Promise.resolve(); this.scriptQueue.push({ script: script, promise: promise }); this.log.debug('Script added to queue at position ' + this.scriptQueue.length - 1); return promise; } }, processScriptQueue() { if (this.scriptQueue.length > 0) { // retrieve first queued script const item = this.scriptQueue[0]; // remove first queued script from the queue this.scriptQueue.splice(0, 1); // execute first queued script this.log.debug('Core.processScriptQueue parsing ' + item.script); item.promise.then(() => this.submitScript(item.script)); } }, /** * Checks whether or not the node platform is bootstrapped. * If not, then download the DeployUnit, installs it and run it. * Resolves directly if already bootstrapped. * * @param {ContainerRoot} model the model used to retrieve the node * @return {Promise} resolves when bootstrapped */ checkBootstrapNode(model) { if (this.nodeInstance) { return Promise.resolve(); } else { this.log.debug('Start \'' + this.nodeName + '\' bootstrapping...'); const node = model.findNodesByID(this.nodeName); if (node) { const meta = node.typeDefinition.select('deployUnits[]/filters[name=platform,value=js]'); if (meta.size() > 0) { return this.resolver.resolve(meta.get(0).eContainer()) .then((Node) => { const deployNode = model.findNodesByID(this.nodeName); const currentNode = this.currentModel.findNodesByID(this.nodeName); // create node instance this.nodeInstance = new Node(this, deployNode, this.nodeName); // bootstrap node dictionary const factory = new kevoree.factory.DefaultKevoreeFactory(); currentNode.dictionary = factory.createDictionary().withGenerated_KMF_ID('0'); if (deployNode.typeDefinition.dictionaryType) { deployNode.typeDefinition.dictionaryType.attributes.array.forEach((attr) => { if (!attr.fragmentDependant) { const param = factory.createValue(); param.name = attr.name; const currVal = deployNode.dictionary.findValuesByID(param.name); if (!currVal) { param.value = attr.defaultValue; currentNode.dictionary.addValues(param); this.log.debug('Set default node param: ' + param.name + '=' + param.value); } } }); } }, (err) => { this.log.error(err); throw new Error('Unable to bootstrap \'' + this.nodeName + '\''); }); } else { return Promise.reject(new Error('No DeployUnit found for \'' + this.nodeName + '\' that matches the \'js\' platform')); } } else { return Promise.reject(new Error('Unable to find \'' + this.nodeName + '\' in the given model.')); } } }, on(event, handler) { this.emitter.on(event, handler); }, once(event, handler) { this.emitter.once(event, handler); }, off(event, handler) { this.emitter.removeListener(event, handler); }, getResolver() { return this.resolver; }, getCurrentModel() { return this.currentModel; }, getDeployModel() { return this.deployModel; }, getLastModel() { if (this.deployModel) { return this.deployModel; } else { return this.currentModel; } }, getNodeName() { return this.nodeName; }, getLoggerFactory() { return this.loggerFactory; }, }; function hash(str) { let val = 0; if (str.length === 0) { return val + ''; } for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); val = ((val << 5) - val) + char; val = val & val; // Convert to 32bit integer } return (val & 0xfffffff) + ''; } function bindingHash(binding) { const hubPath = binding.hub ? binding.hub.path() : 'UNDEFINED'; const portPath = binding.port ? binding.port.path() : 'UNDEFINED'; return hash(hubPath + '_' + portPath); } function monkeyPatchKMF(proposedModel) { proposedModel.mBindings.array.forEach((possibleBinding) => { possibleBinding.generated_KMF_ID = bindingHash(possibleBinding); }); } module.exports = KevoreeCore;