/* jslint node: true */ 'use strict'; // ENiGMA½ const PluginModule = require('./plugin_module.js').PluginModule; const Config = require('./config.js').get; const Log = require('./logger.js').log; const { Errors } = require('./enig_error.js'); const _ = require('lodash'); const later = require('@breejs/later'); const path = require('path'); const pty = require('node-pty'); const sane = require('sane'); const moment = require('moment'); const paths = require('path'); const fse = require('fs-extra'); exports.getModule = EventSchedulerModule; exports.EventSchedulerModule = EventSchedulerModule; // allow for loadAndStart exports.moduleInfo = { name: 'Event Scheduler', desc: 'Support for scheduling arbritary events', author: 'NuSkooler', }; const SCHEDULE_REGEXP = /(?:^|or )?(@watch:)([^\0]+)?$/; const ACTION_REGEXP = /@(method|execute):([^\0]+)?$/; class ScheduledEvent { constructor(events, name) { this.name = name; this.schedule = this.parseScheduleString(events[name].schedule); this.action = this.parseActionSpec(events[name].action); if (this.action) { this.action.args = events[name].args || []; } } get isValid() { if ( !this.schedule || (!this.schedule.sched && !this.schedule.watchFile) || !this.action ) { return false; } if ('method' === this.action.type && !this.action.location) { return false; } return true; } parseScheduleString(schedStr) { if (!schedStr) { return false; } let schedule = {}; const m = SCHEDULE_REGEXP.exec(schedStr); if (m) { schedStr = schedStr.substr(0, m.index).trim(); if ('@watch:' === m[1]) { schedule.watchFile = m[2]; } } if (schedStr.length > 0) { const sched = later.parse.text(schedStr); if (-1 === sched.error) { schedule.sched = sched; } } // return undefined if we couldn't parse out anything useful if (!_.isEmpty(schedule)) { return schedule; } } parseActionSpec(actionSpec) { if (actionSpec) { if ('@' === actionSpec[0]) { const m = ACTION_REGEXP.exec(actionSpec); if (m) { if (m[2].indexOf(':') > -1) { const parts = m[2].split(':'); return { type: m[1], location: parts[0], what: parts[1], }; } else { return { type: m[1], what: m[2], }; } } } else { return { type: 'execute', what: actionSpec, }; } } } executeAction(reason, cb) { Log.info( { eventName: this.name, action: this.action, reason: reason }, `Executing scheduled event "${this.name}"...` ); if ('method' === this.action.type) { const modulePath = path.join(__dirname, '../', this.action.location); // enigma-bbs base + supplied location (path/file.js') try { const methodModule = require(modulePath); methodModule[this.action.what](this.action.args, err => { if (err) { Log.debug( { error: err.message, eventName: this.name, action: this.action, }, 'Error performing scheduled event action' ); } return cb(err); }); } catch (e) { Log.warn( { error: e.message, eventName: this.name, action: this.action }, 'Failed to perform scheduled event action' ); return cb(e); } } else if ('execute' === this.action.type) { const opts = { // :TODO: cwd name: this.name, cols: 80, rows: 24, env: process.env, }; let proc; try { proc = pty.spawn(this.action.what, this.action.args, opts); } catch (e) { Log.warn({ error: 'Failed to spawn @execute process', reason: e.message, eventName: this.name, action: this.action, what: this.action.what, args: this.action.args, }); return cb(e); } proc.once('exit', exitCode => { if (exitCode) { Log.warn( { eventName: this.name, action: this.action, exitCode: exitCode }, 'Bad exit code while performing scheduled event action' ); } return cb( exitCode ? Errors.ExternalProcess( `Bad exit code while performing scheduled event action: ${exitCode}` ) : null ); }); } } } function EventSchedulerModule(options) { PluginModule.call(this, options); const config = Config(); if (_.has(config, 'eventScheduler')) { this.moduleConfig = config.eventScheduler; } const self = this; this.runningActions = new Set(); this.performAction = function (schedEvent, reason) { if (self.runningActions.has(schedEvent.name)) { return; // already running } self.runningActions.add(schedEvent.name); schedEvent.executeAction(reason, () => { self.runningActions.delete(schedEvent.name); }); }; } // convienence static method for direct load + start EventSchedulerModule.loadAndStart = function (cb) { const loadModuleEx = require('./module_util.js').loadModuleEx; const loadOpts = { name: path.basename(__filename, '.js'), path: __dirname, }; loadModuleEx(loadOpts, (err, mod) => { if (err) { return cb(err); } const modInst = new mod.getModule(); modInst.startup(err => { return cb(err, modInst); }); }); }; EventSchedulerModule.prototype.startup = function (cb) { this.eventTimers = []; const self = this; if (this.moduleConfig && _.has(this.moduleConfig, 'events')) { const events = Object.keys(this.moduleConfig.events).map(name => { return new ScheduledEvent(this.moduleConfig.events, name); }); events.forEach(schedEvent => { if (!schedEvent.isValid) { Log.warn({ eventName: schedEvent.name }, 'Invalid scheduled event entry'); return; } Log.debug( { eventName: schedEvent.name, schedule: this.moduleConfig.events[schedEvent.name].schedule, action: schedEvent.action, next: schedEvent.schedule.sched ? moment( later.schedule(schedEvent.schedule.sched).next(1) ).format('ddd, MMM Do, YYYY @ h:m:ss a') : 'N/A', }, 'Scheduled event loaded' ); if (schedEvent.schedule.sched) { this.eventTimers.push( later.setInterval(() => { self.performAction(schedEvent, 'Schedule'); }, schedEvent.schedule.sched) ); } if (schedEvent.schedule.watchFile) { const watcher = sane(paths.dirname(schedEvent.schedule.watchFile), { glob: `**/${paths.basename(schedEvent.schedule.watchFile)}`, }); // :TODO: should track watched files & stop watching @ shutdown? ['change', 'add', 'delete'].forEach(event => { watcher.on(event, (fileName, fileRoot) => { const eventPath = paths.join(fileRoot, fileName); if (schedEvent.schedule.watchFile === eventPath) { self.performAction(schedEvent, `Watch file: ${eventPath}`); } }); }); fse.exists(schedEvent.schedule.watchFile, exists => { if (exists) { self.performAction( schedEvent, `Watch file: ${schedEvent.schedule.watchFile}` ); } }); } }); } cb(null); }; EventSchedulerModule.prototype.shutdown = function (cb) { if (this.eventTimers) { this.eventTimers.forEach(et => et.clear()); } cb(null); };