UNPKG

openframe

Version:

Openframe controller process which runs on the machine controlling the frame.

551 lines (490 loc) 18.2 kB
/** * The main controller for the frame. */ // app dependencies var url = require('url'), path = require('path'), debug = require('debug')('openframe:frame_controller'), Swagger = require('swagger-client'), downloader = require('./downloader'), pubsub = require('./pubsub'), proc_man = require('./process-manager'), ext_man = require('./extension-manager'), config = require('./config'), frame = require('./frame'), user = require('./user'), rest = require('./rest'), Spinner = require('cli-spinner').Spinner, spinner = new Spinner('[%s]'), https = require('https'), requireg = require('requireg'), chalk = require('chalk'); var fc = {}; module.exports = fc; /** * Initialize the frame controller * - login user * - connect frame * - update frame * - update extensions * - if necessary, install extensions * - init extensions */ fc.init = function() { debug('init'); console.log('\n'); spinner.setSpinnerString(1); spinner.start(); this.login() .then(this.connect) .then(this.ready) .catch(function(err) { debug(err); }); }; /** * Install a extension. * - login user * - pull latest frame state * - install extension package * - add extension to frame.extensions * - exit with user-facing success/error message * * @param {String} extension An npm-style dependency string (package[@version]); */ fc.installExtension = function(extension) { debug('installExtension', extension); var _this = this; var extensionParts = extension.split('@'), packageName = extensionParts[0], version = extensionParts.length > 1 ? extensionParts[1] : undefined, npmRepo; if (extension.startsWith('github:')) { debug('github extension') var repoPath = extension.replace(/^(github:)/,'').split('#')[0]; var branch = extension.split('#')[1] || 'master'; debug('branch', branch); var packageJsonURL = 'https://raw.githubusercontent.com/' + repoPath + '/' + branch + '/package.json'; debug('packageJsonURL', packageJsonURL); var result = ''; https.get(packageJsonURL, function(response) { if (response.statusCode != 200) debug('Couldn\'t download package.json from Github') response.on('data',d => result += d).on('end',() => { var packageJson = JSON.parse(result); version = undefined; packageName = packageJson.name; npmRepo = extension _install(); }); }).on('error', (e) => { debug(e); }); } else if (extension.startsWith('file:')) { debug('extension on local filesystem') var repoPath = extension.replace(/^(file:)/,'') var packageJson = require(path.join(repoPath,'package.json')); version = undefined; packageName = packageJson.name; npmRepo = extension _install(); } else { debug('regular extension') npmRepo = packageName _install(); } function _install() { debug('packageName',packageName) debug('version', version) _this.login() .then(function() { frame.fetch() .then(function() { ext_man.installExtension(npmRepo, version, true) .then(function() { debug('Installed ' + extension + ' successfully, saving frame...'); var ext = requireg(packageName, true); // Make sure to check for installed extensions globally and skip local packages // successfully installed extension locally, add to frame frame.state.extensions[packageName] = version || "*"; frame.state.settings = frame.state.settings || {}; frame.state.settings[packageName] = ext.props.settings || {}; debug('EXT', ext); debug('SETTINGS', frame.state.settings); frame.save() .then(function() { console.log('[o] Extension installed successfully!\n'); }); }); }) .catch(function(err) { if (err.status === 404) { console.log('[o] ERROR: This frame has been set up perviously, but is not attached this user.'); console.log('\n'); console.log('To reset the frame entirely, restart using: openframe -r'); } }); }); }; }; /** * Uninstall a extension. * - login user * - pull latest frame state * - uninstall extension package * - remove extension to frame.extensions * - exit with user-facing success/error message * * @param {String} extension name (npm package); */ fc.uninstallExtension = function(packageName) { debug('uninstallExtension', packageName); this.login() .then(function() { frame.fetch() .then(function() { ext_man.uninstallExtension(packageName) .then(function() { debug('Uninstalled ' + packageName + ' successfully, saving frame...'); // successfully installed extension locally, add to frame if (packageName in frame.state.extensions) { delete frame.state.extensions[packageName]; } if (packageName in frame.state.settings) { delete frame.state.settings[packageName]; } frame.save() .then(function(resp) { // debug(resp); console.log('[o] Extension uninstalled successfully!\n'); }); }).catch(function(err) { console.log('[o] ERROR: Problem uninstalling the extension. Are you sure it is installed?\n'); }); }) .catch(function(err) { if (err.status === 404) { console.log('[o] ERROR: This frame has been set up perviously, but is not attached this user.'); console.log('\n'); console.log('To reset the frame entirely, restart using: openframe -r'); } }); }); }; /** * Called when the frame has finished initializing. */ fc.ready = function() { debug('ready'); spinner.stop(true); if (frame.state && frame.state.current_artwork) { fc.changeArtwork(); } else { var url = config.ofrc.network.app_base; // No current artwork... give the user a message: console.log('[o] Connected! You can now push artwork to this frame.'); console.log('\n'); console.log('This frame should now appear as ' + frame.state.name + ' when you log in to Openframe at ' + url + '.'); console.log('\n'); } }; /** * Authenticate with the API server using the supplied user/pass. * * @return {Promise} A promise resolving with the logged-in user's ID */ fc.login = function() { debug('login'); var creds = user.state; return new Promise(function(resolve, reject) { rest.client.OpenframeUser.OpenframeUser_login({ credentials: creds }).then(function(resp) { if (resp.obj.id) { creds.access_token = resp.obj.id; rest.client.clientAuthorizations.add('access_token', new Swagger.ApiKeyAuthorization('access_token', resp.obj.id, 'query')); } rest.client.OpenframeUser.OpenframeUser_config().then(function(conf_resp) { debug(resp); config.ofrc.pubsub_url = conf_resp.obj.config.pubsub_url; config.save().then(function() { resolve(resp.obj.userId); }); }); }).catch(function(err) { // login failed... debug('Login failed. Please try again.'); spinner.stop(true); console.log('[o] Login failed. Please try again.'); console.log('\n'); user.state = {}; user .save() .then(function() { process.exit(0); }); }); }); }; /** * Connect this Frame. If the Frame has not yet been created, i.e. there is no * id on the Frame object in ofrc, create a new Frame. * * @param {String} userId * @return {Promise} */ fc.connect = function(userId) { debug('connect', userId); return new Promise(function(resolve, reject) { // called when frame is ready to connect function readyToConnect() { debug('readyToConnect'); fc.pubsub = pubsub.init(fc); resolve(); } // XXX - POTENTIAL BUG - catch extension init error separately, otherwise a new frame is created. frame .fetch() .then(function() { debug('ready to init...'); // initExtensions now always resolves, is never rejected return ext_man.initExtensions(frame.state.extensions, fc.extensionApi); }) .then(readyToConnect) .catch(function(err) { debug(err); fc.registerNewFrame(userId) .then(readyToConnect) .catch(function(err) { debug(err); reject(err); }); }); }); }; /** * Register this as a new frame for user [userId]. This creates a new * Frame object on the server via the REST api. * * TODO: should this be on the frame model? a create method? * * @param {String} userId * @return {Promise} A promise resolving with the newly created Frame object */ fc.registerNewFrame = function(userId) { debug('registerNewFrame', userId); return new Promise(function(resolve, reject) { rest.client.OpenframeUser.OpenframeUser_prototype_create_owned_frames({ data: { name: frame.state.name }, id: userId }).then(function(data) { debug(data.obj); frame.state = data.obj; frame.persistStateToFile(); ext_man.installExtensions(frame.state.extensions) .then(function() { debug('-----> extensions installed'); ext_man.initExtensions(frame.state.extensions, fc.extensionApi) .then(function() { resolve(frame.state); }); }) .catch(function(err) { reject(err); }); }).catch(function(err) { debug(err); reject(err); }); }); }; /** * Change the artwork being displayed to that which is stored in the * Frame's current_artwork. */ fc.changeArtwork = function() { debug('changeArtwork', frame.state.current_artwork); var old_artwork = fc.current_artwork || undefined, new_artwork = frame.state.current_artwork, old_format = old_artwork && frame.formats[old_artwork.format], new_format = frame.formats[new_artwork.format], new_artwork_conf = new_artwork.config || {}, tokens = {}, parsed, file_name; debug('details', old_artwork, old_format, new_artwork, new_format); return new Promise(function(resolve, reject) { //artwork extension not installed, don't update if (!new_format) { console.error(chalk.red('The artwork format is not installed: ' + new_artwork.format + '\n')); return reject(); } // old artwork is new artwork, don't update. if (old_artwork && JSON.stringify(old_artwork) === JSON.stringify(new_artwork)) { debug('new artwork same as current', old_artwork.id, new_artwork.id); return reject(); } function swapArt() { debug('swapArt'); if (old_artwork) { _endArt(old_format, old_artwork) .then(function() { _startArt(new_format, new_artwork).then(function() { fc.current_artwork = new_artwork; fc.pubsub.publish('/frame/'+frame.state.id+'/frame_updated', frame.state.id); resolve(); }); }) .catch(reject); } else { _startArt(new_format, new_artwork).then(function() { fc.current_artwork = new_artwork; resolve(); }); } } if (new_format.download) { debug('download'); // this artwork needs to be downloaded parsed = url.parse(new_artwork.url); file_name = path.basename(parsed.pathname); downloader.downloadFile(new_artwork.url, new_artwork.id + file_name, file_name) .then(function(filePath) { tokens['$url'] = new_artwork.url; tokens['$id'] = new_artwork.id; tokens['$filepath'] = filePath; tokens['$filename'] = file_name; new_artwork.tokens = tokens; swapArt(); }) .catch(reject); } else { debug('DO NOT download'); // this artwork can be displayed via the url tokens['$url'] = new_artwork.url; new_artwork.tokens = tokens; swapArt(); } }); }; fc.updateFrame = function() { // let subscribers know the frame is updating fc.pubsub.publish('/frame/'+frame.state.id+'/frame_updating', frame.state.id); // fetch the latest frame state, and update as needed frame.fetch() .then(function(new_state) { if (frame.state.current_artwork) { fc.changeArtwork() .then(function() { // success changing artwork, do nothing more... }) .catch(function() { // error changing artwork, reset frame.state.current_artwork to true current frame.state.current_artwork = fc.current_artwork; }); } }); }; /** * A sandboxed API passed into extensions' init function. * @type {Object} */ fc.extensionApi = { // let the extension add a format to the frame addFormat: frame.addFormat, // give the extension access to the global pubsub getPubsub: function() { return fc.pubsub; }, // access to the Swagger rest client getRest: function() { return rest.client; }, // access to the frame model reference getFrame: function() { return frame; } }; /** * Start an artwork. * @param {string} new_format * @param {object} new_artwork * @return {Promise} */ function _startArt(new_format, new_artwork) { debug('startArt'); var _command = new_format.start_command, tokens = new_artwork.tokens || {}; if (typeof _command === 'function') { // we're passing artwork-specific args and tokens here, letting the format // construct the command dynamically... var options = new_artwork.options || {}; _command = _command.call(new_format, options, tokens); // start_command of the extension can return either Promise or a value, both is okay. return Promise.resolve(_command).then(function(value) { return new Promise(function(resolve, reject) { _command = value resolve(replaceTokensAndStart()) }) }) } else { return replaceTokensAndStart() } function replaceTokensAndStart() { var command = _replaceTokens(_command, tokens); return new Promise(function(resolve, reject) { // TODO: proc_man.startProcess doesn't return Promise // can we know when the process is ready without ipc? proc_man.startProcess(command); resolve(); }); } } /** * End a playing artwork. * @param {string} old_format * @param {object} old_artwork * @return {Promise} Resolves when command is complete. */ function _endArt(old_format, old_artwork) { debug('endArt'); var _command = old_format.end_command, tokens = old_artwork.tokens || {}; if (typeof _command === 'function') { // we're passing artwork-specific args and tokens here, letting the format // construct the command dynamically... var options = old_artwork.options || {}; _command = _command.call(old_format, options, tokens); } var command = _replaceTokens(_command, tokens); return new Promise(function(resolve, reject) { proc_man.exec(command, function(err) { if (err) { debug(err); } resolve(); }); }); } /** * Replace placeholder tokens in a string. * * TODO: move to a util module * * @param {string} _str * @param {object} tokens * @return {string} The string with tokens replaced. */ function _replaceTokens(_str, tokens) { var str = _str, key; for (key in tokens) { if (tokens.hasOwnProperty(key)) { // TODO: better token replacement (global replacement?) str = str.replace(key, tokens[key]); } } return str; }