From 5edb76ef347e9e5da1e0cb4f0f769e23b4c6cb24 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Fri, 14 Sep 2018 11:35:20 -0700 Subject: [PATCH] Checkpoint Tray Notifier --- .../resources/tray-menu-notification.png | Bin 0 -> 319 bytes server-console/src/main.js | 229 +++++++++------ server-console/src/modules/hf-app.js | 71 +++++ .../src/modules/hf-notifications.js | 263 ++++++++++++++++++ server-console/src/modules/hf-process.js | 18 ++ 5 files changed, 493 insertions(+), 88 deletions(-) create mode 100644 server-console/resources/tray-menu-notification.png create mode 100644 server-console/src/modules/hf-app.js create mode 100644 server-console/src/modules/hf-notifications.js diff --git a/server-console/resources/tray-menu-notification.png b/server-console/resources/tray-menu-notification.png new file mode 100644 index 0000000000000000000000000000000000000000..569ee95d7ed77761062251173dda4a02f2d40e75 GIT binary patch literal 319 zcmV-F0l@x=P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0MtoDK~yMHElxXb z0x=MMV=b^qp`Zb}h%0abD3YFn2Bm?9bJ5UofK)jG1!+?tBsPgiw%}`QuP0ASMtYv{ zypJvTFP%&k`(F-RHJgydyyVJ5heO5b3KS6tgtApDsJy+3=*w8~{c|Tu1lkM^H0GTa z#6Lf2a9k3`2NZN$5gaJmCkfXtfg4eS}R@*jg3nv`Z2I{s5>wVTi4c RNGSjS002ovPDHLkV1gtFc~bxY literal 0 HcmV?d00001 diff --git a/server-console/src/main.js b/server-console/src/main.js index 92ebdbf36c..0fd2659fe9 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -29,58 +29,31 @@ const updater = require('./modules/hf-updater.js'); const Config = require('./modules/config').Config; const hfprocess = require('./modules/hf-process.js'); + +global.log = require('electron-log'); + const Process = hfprocess.Process; const ACMonitorProcess = hfprocess.ACMonitorProcess; const ProcessStates = hfprocess.ProcessStates; const ProcessGroup = hfprocess.ProcessGroup; const ProcessGroupStates = hfprocess.ProcessGroupStates; +const hfApp = require('./modules/hf-app.js'); +const GetBuildInfo = hfApp.getBuildInfo; +const StartInterface = hfApp.startInterface; + const osType = os.type(); const appIcon = path.join(__dirname, '../resources/console.png'); +const menuNotificationIcon = path.join(__dirname, '../resources/tray-menu-notification.png'); + const DELETE_LOG_FILES_OLDER_THAN_X_SECONDS = 60 * 60 * 24 * 7; // 7 Days const LOG_FILE_REGEX = /(domain-server|ac-monitor|ac)-.*-std(out|err).txt/; const HOME_CONTENT_URL = "http://cdn.highfidelity.com/content-sets/home-tutorial-RC40.tar.gz"; -function getBuildInfo() { - var buildInfoPath = null; - - if (osType == 'Windows_NT') { - buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); - } else if (osType == 'Darwin') { - var contentPath = ".app/Contents/"; - var contentEndIndex = __dirname.indexOf(contentPath); - - if (contentEndIndex != -1) { - // this is an app bundle - var appPath = __dirname.substring(0, contentEndIndex) + ".app"; - buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); - } - } - - const DEFAULT_BUILD_INFO = { - releaseType: "", - buildIdentifier: "dev", - buildNumber: "0", - stableBuild: "0", - organization: "High Fidelity - dev", - appUserModelId: "com.highfidelity.sandbox-dev" - }; - var buildInfo = DEFAULT_BUILD_INFO; - - if (buildInfoPath) { - try { - buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); - } catch (e) { - buildInfo = DEFAULT_BUILD_INFO; - } - } - - return buildInfo; -} -const buildInfo = getBuildInfo(); +const buildInfo = GetBuildInfo(); function getRootHifiDataDirectory(local) { var organization = buildInfo.organization; @@ -114,7 +87,6 @@ const UPDATER_LOCK_FILENAME = ".updating"; const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME; // Configure log -global.log = require('electron-log'); const oldLogFile = path.join(getApplicationDataDirectory(), '/log.txt'); const logFile = path.join(getApplicationDataDirectory(true), '/log.txt'); if (oldLogFile != logFile && fs.existsSync(oldLogFile)) { @@ -149,15 +121,23 @@ const configPath = path.join(getApplicationDataDirectory(), 'config.json'); var userConfig = new Config(); userConfig.load(configPath); - const ipcMain = electron.ipcMain; + +function isServerInstalled() { + return interfacePath && userConfig.get("serverInstalled", true); +} + +function isInterfaceInstalled() { + return dsPath && acPath && userConfig.get("interfaceInstalled", true); +} + var isShuttingDown = false; function shutdown() { log.debug("Normal shutdown (isShuttingDown: " + isShuttingDown + ")"); if (!isShuttingDown) { // if the home server is running, show a prompt before quit to ask if the user is sure - if (homeServer.state == ProcessGroupStates.STARTED) { + if (isServerInstalled() && homeServer.state == ProcessGroupStates.STARTED) { log.debug("Showing shutdown dialog."); dialog.showMessageBox({ type: 'question', @@ -184,6 +164,9 @@ function shutdownCallback(idx) { if (idx == 0 && !isShuttingDown) { isShuttingDown = true; + log.debug("Stop tray polling."); + trayNotifications.stopPolling(); + log.debug("Saving user config"); userConfig.save(configPath); @@ -191,31 +174,37 @@ function shutdownCallback(idx) { log.debug("Closing log window"); logWindow.close(); } - if (homeServer) { - log.debug("Stoping home server"); - homeServer.stop(); - } - updateTrayMenu(homeServer.state); + if(isServerInstalled()) { + if (homeServer) { + log.debug("Stoping home server"); + homeServer.stop(); - if (homeServer.state == ProcessGroupStates.STOPPED) { - // if the home server is already down, take down the server console now - log.debug("Quitting."); - app.exit(0); - } else { - // if the home server is still running, wait until we get a state change or timeout - // before quitting the app - log.debug("Server still shutting down. Waiting"); - var timeoutID = setTimeout(function() { - app.exit(0); - }, 5000); - homeServer.on('state-update', function(processGroup) { - if (processGroup.state == ProcessGroupStates.STOPPED) { - clearTimeout(timeoutID); + updateTrayMenu(homeServer.state); + + if (homeServer.state == ProcessGroupStates.STOPPED) { + // if the home server is already down, take down the server console now log.debug("Quitting."); app.exit(0); + } else { + // if the home server is still running, wait until we get a state change or timeout + // before quitting the app + log.debug("Server still shutting down. Waiting"); + var timeoutID = setTimeout(function() { + app.exit(0); + }, 5000); + homeServer.on('state-update', function(processGroup) { + if (processGroup.state == ProcessGroupStates.STOPPED) { + clearTimeout(timeoutID); + log.debug("Quitting."); + app.exit(0); + } + }); } - }); + } + } + else { + app.exit(0); } } } @@ -351,20 +340,6 @@ function openLogDirectory() { app.on('window-all-closed', function() { }); -function startInterface(url) { - var argArray = []; - - // check if we have a url parameter to include - if (url) { - argArray = ["--url", url]; - } - - // create a new Interface instance - Interface makes sure only one is running at a time - var pInterface = new Process('interface', interfacePath, argArray); - pInterface.detached = true; - pInterface.start(); -} - var tray = null; global.homeServer = null; global.domainServer = null; @@ -372,6 +347,18 @@ global.acMonitor = null; global.userConfig = userConfig; global.openLogDirectory = openLogDirectory; +const hfNotifications = require('./modules/hf-notifications.js'); +const HifiNotifications = hfNotifications.HifiNotifications; +const HifiNotificationType = hfNotifications.NotificationType; + +var pendingNotifications = {} +function notificationCallback(notificationType) { + pendingNotifications[notificationType] = true; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); +} + +var trayNotifications = new HifiNotifications(userConfig, notificationCallback); + var LogWindow = function(ac, ds) { this.ac = ac; this.ds = ds; @@ -407,7 +394,7 @@ LogWindow.prototype = { function visitSandboxClicked() { if (interfacePath) { - startInterface('hifi://localhost'); + StartInterface('hifi://localhost'); } else { // show an error to say that we can't go home without an interface instance dialog.showErrorBox("Client Not Found", binaryMissingMessage("High Fidelity client", "Interface", false)); @@ -425,6 +412,47 @@ var labels = { label: 'Version - ' + buildInfo.buildIdentifier, enabled: false }, + enableNotifications: { + label: 'Enable Notifications', + type: 'checkbox', + checked: true, + click: function() { + trayNotifications.enable(!trayNotifications.enabled(), notificationCallback); + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + goto: { + label: 'Goto', + click: function() { + StartInterface(""); + pendingNotifications[HifiNotificationType.GOTO] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + people: { + label: 'People', + click: function() { + StartInterface(""); + pendingNotifications[HifiNotificationType.PEOPLE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + wallet: { + label: 'Wallet', + click: function() { + StartInterface(""); + pendingNotifications[HifiNotificationType.WALLET] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, + marketplace: { + label: 'Marketplace', + click: function() { + StartInterface(""); + pendingNotifications[HifiNotificationType.MARKETPLACE] = false; + updateTrayMenu(homeServer ? homeServer.state : ProcessGroupStates.STOPPED); + } + }, restart: { label: 'Start Server', click: function() { @@ -489,16 +517,30 @@ function buildMenuArray(serverState) { if (isShuttingDown) { menuArray.push(labels.shuttingDown); } else { - menuArray.push(labels.serverState); - menuArray.push(labels.version); - menuArray.push(separator); - menuArray.push(labels.goHome); - menuArray.push(separator); - menuArray.push(labels.restart); - menuArray.push(labels.stopServer); - menuArray.push(labels.settings); - menuArray.push(labels.viewLogs); - menuArray.push(separator); + if(isServerInstalled()) { + menuArray.push(labels.serverState); + menuArray.push(labels.version); + menuArray.push(separator); + } + if(isInterfaceInstalled()) { + menuArray.push(labels.enableNotifications); + menuArray.push(labels.goto); + menuArray.push(labels.people); + menuArray.push(labels.wallet); + menuArray.push(labels.marketplace); + menuArray.push(separator); + } + if(isServerInstalled() && isInterfaceInstalled()) { + menuArray.push(labels.goHome); + menuArray.push(separator); + } + if(isServerInstalled()) { + menuArray.push(labels.restart); + menuArray.push(labels.stopServer); + menuArray.push(labels.settings); + menuArray.push(labels.viewLogs); + menuArray.push(separator); + } menuArray.push(labels.share); menuArray.push(separator); menuArray.push(labels.quit); @@ -528,6 +570,17 @@ function updateLabels(serverState) { labels.restart.label = "Restart Server"; labels.restart.enabled = false; } + + labels.enableNotifications.checked = trayNotifications.enabled(); + labels.people.visible = trayNotifications.enabled(); + labels.goto.visible = trayNotifications.enabled(); + labels.wallet.visible = trayNotifications.enabled(); + labels.marketplace.visible = trayNotifications.enabled(); + labels.goto.icon = pendingNotifications[HifiNotificationType.GOTO] ? menuNotificationIcon : null; + labels.people.icon = pendingNotifications[HifiNotificationType.PEOPLE] ? menuNotificationIcon : null; + labels.wallet.icon = pendingNotifications[HifiNotificationType.WALLET] ? menuNotificationIcon : null; + labels.marketplace.icon = pendingNotifications[HifiNotificationType.MARKETPLACE] ? menuNotificationIcon : null; + } function updateTrayMenu(serverState) { @@ -807,7 +860,7 @@ function onContentLoaded() { deleteOldFiles(logPath, DELETE_LOG_FILES_OLDER_THAN_X_SECONDS, LOG_FILE_REGEX); - if (dsPath && acPath) { + if (isServerInstalled()) { var dsArguments = ['--get-temp-name', '--parent-pid', process.pid]; domainServer = new Process('domain-server', dsPath, dsArguments, logPath); @@ -838,7 +891,7 @@ function onContentLoaded() { // If we were launched with the launchInterface option, then we need to launch interface now if (argv.launchInterface) { log.debug("Interface launch requested... argv.launchInterface:", argv.launchInterface); - startInterface(); + StartInterface(); } // If we were launched with the shutdownWith option, then we need to shutdown when that process (pid) @@ -869,7 +922,7 @@ app.on('ready', function() { // Create tray icon tray = new Tray(trayIcons[ProcessGroupStates.STOPPED]); - tray.setToolTip('High Fidelity Sandbox'); + tray.setToolTip('High Fidelity'); tray.on('click', function() { tray.popUpContextMenu(tray.menu); diff --git a/server-console/src/modules/hf-app.js b/server-console/src/modules/hf-app.js new file mode 100644 index 0000000000..28b97f582a --- /dev/null +++ b/server-console/src/modules/hf-app.js @@ -0,0 +1,71 @@ +const fs = require('fs'); +const extend = require('extend'); +const Config = require('./config').Config +const os = require('os'); +const pathFinder = require('./path-finder'); +const path = require('path'); +const argv = require('yargs').argv; +const hfprocess = require('./hf-process'); +const Process = hfprocess.Process; + +const binaryType = argv.binaryType; +const osType = os.type(); + +exports.getBuildInfo = function() { + var buildInfoPath = null; + + if (osType == 'Windows_NT') { + buildInfoPath = path.join(path.dirname(process.execPath), 'build-info.json'); + } else if (osType == 'Darwin') { + var contentPath = ".app/Contents/"; + var contentEndIndex = __dirname.indexOf(contentPath); + + if (contentEndIndex != -1) { + // this is an app bundle + var appPath = __dirname.substring(0, contentEndIndex) + ".app"; + buildInfoPath = path.join(appPath, "/Contents/Resources/build-info.json"); + } + } + + const DEFAULT_BUILD_INFO = { + releaseType: "", + buildIdentifier: "dev", + buildNumber: "0", + stableBuild: "0", + organization: "High Fidelity - dev", + appUserModelId: "com.highfidelity.sandbox-dev" + }; + var buildInfo = DEFAULT_BUILD_INFO; + + if (buildInfoPath) { + try { + buildInfo = JSON.parse(fs.readFileSync(buildInfoPath)); + } catch (e) { + buildInfo = DEFAULT_BUILD_INFO; + } + } + + return buildInfo; +} + +const buildInfo = exports.getBuildInfo(); +const interfacePath = pathFinder.discoveredPath("Interface", binaryType, buildInfo.releaseType); + +exports.startInterface = function(url) { + var argArray = []; + + // check if we have a url parameter to include + if (url) { + argArray = ["--url", url]; + } + console.log("Starting with " + url); + // create a new Interface instance - Interface makes sure only one is running at a time + var pInterface = new Process('Interface', interfacePath, argArray); + pInterface.detached = true; + pInterface.start(); +} + +exports.isInterfaceRunning = function(done) { + var pInterface = new Process('interface', 'interface.exe'); + return pInterface.isRunning(done); +} diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js new file mode 100644 index 0000000000..06be365208 --- /dev/null +++ b/server-console/src/modules/hf-notifications.js @@ -0,0 +1,263 @@ +const request = require('request'); +const notifier = require('node-notifier'); +const os = require('os'); +const process = require('process'); +const hfApp = require('./hf-app'); +const path = require('path'); + +const notificationIcon = path.join(__dirname, '../../resources/console-notification.png'); +const NOTIFICATION_POLL_TIME_MS = 15 * 1000; +const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://highfidelity.com' +const STORIES_URL= '/api/v1/user_stories'; +const USERS_URL= '/api/v1/users'; +const ECONOMIC_ACTIVITY_URL= '/api/v1/commerce/history'; +const UPDATES_URL= '/api/v1/commerce/available_updates'; + + +const StartInterface=hfApp.startInterface; +const IsInterfaceRunning=hfApp.isInterfaceRunning; + +const NotificationType = { + GOTO: 'goto', + PEOPLE: 'people', + WALLET: 'wallet', + MARKETPLACE: 'marketplace' +}; + +function HifiNotification(notificationType, notificationData) { + this.type = notificationType; + this.data = notificationData; +} + +HifiNotification.prototype = { + show: function() { + switch(this.type) { + case NotificationType.GOTO: + var text = this.data.username + " " + this.data.action_string + " in " + this.data.place_name; + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: "Click to goto " + this.data.place_name, + wait: true, + url: "hifi://" + this.data.place_name + this.data.path + }); + break; + + case NotificationType.PEOPLE: + var text = this.data.username + " has logged in."; + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: "Click to join them in " + this.data.location.root.name, + wait: true, + url: "hifi://" + this.data.location.root.name + this.data.location.path + }); + break; + + case NotificationType.WALLET: + var text = "Economic activity."; + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: "Click to open your wallet", + wait: true, + app: "Wallet" + }); + break; + + case NotificationType.MARKETPLACE: + var text = "One of your marketplace items has an update."; + notifier.notify({ + notificationType: this.type, + icon: notificationIcon, + title: text, + message: "Click to start the marketplace app", + wait: true, + app: "Marketplace" + }); + break; + } + } +} + +function HifiNotifications(config, callback) { + this.config = config; + this.callback = callback; + this.since = new Date(this.config.get("notifySince", "1970-01-01T00:00:00.000Z")); + this.enable(this.enabled()); + notifier.on('click', function(notifierObject, options) { + console.log(options); + StartInterface(options.url); + }); +} + +HifiNotifications.prototype = { + enable: function(enabled) { + this.config.set("enableTrayNotifications", enabled); + if(enabled) { + var _this = this; + this.pollTimer = setInterval(function() { + var _since = _this.since; + _this.since = new Date(); + IsInterfaceRunning(function(running) { + if(running) { + return; + } + _this.pollForStories(_since, _this.callback); + _this.pollForConnections(_since, _this.callback); + _this.pollForEconomicActivity(_since, _this.callback); + _this.pollForMarketplaceUpdates(_since, _this.callback); + }); + }, + NOTIFICATION_POLL_TIME_MS); + } + else if(this.pollTimer) { + clearInterval(this.pollTimer); + } + }, + enabled: function() { + return this.config.get("enableTrayNotifications", true); + }, + stopPolling: function() { + this.config.set("notifySince", this.since.toISOString()); + if(this.pollTimer) { + clearInterval(this.pollTimer); + } + }, + pollForStories: function(since, callback) { + var _this = this; + var actions = 'announcement'; + var options = [ + 'now=' + new Date().toISOString(), + 'since=' + since.toISOString(), + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true' + ]; + console.log("Polling for stories"); + var url = METAVERSE_SERVER_URL + STORIES_URL + '?' + options.join('&'); + request({ + uri: url + }, function (error, data) { + if (error || !data.body) { + console.log("Error: unable to get " + url); + return; + } + var content = JSON.parse(data.body); + if(!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return; + } + content.user_stories.forEach(function(story) { + var updated_at = new Date(story.updated_at); + if (updated_at < since) { + return; + } + callback(NotificationType.GOTO); + var notification = new HifiNotification(NotificationType.GOTO, story); + notification.show(); + }); + }); + }, + pollForConnections: function(since, callback) { + var _this = this; + var options = [ + 'filter=connections', + 'since=' + since.toISOString(), + 'status=online' + ]; + console.log("Polling for connections"); + var url = METAVERSE_SERVER_URL + USERS_URL + '?' + options.join('&'); + request({ + uri: url + }, function (error, data) { + if (error || !data.body) { + console.log("Error: unable to get " + url); + return; + } + var content = JSON.parse(data.body); + if(!content || content.status != 'success') { + console.log("Error: unable to get " + url); + return; + } + console.log(content.data); + content.data.users.forEach(function(user) { + if(user.online) { + callback(NotificationType.PEOPLE); + var notification = new HifiNotification(NotificationType.PEOPLE, user); + notification.show(); + } + }); + }); + }, + pollForEconomicActivity: function(since, callback) { + var _this = this; + var options = [ + 'filter=connections', + 'since=' + since.toISOString(), + 'status=online' + ]; + console.log("Polling for economic activity"); + var url = METAVERSE_SERVER_URL + ECONOMIC_ACTIVITY_URL + '?' + options.join('&'); + request.post({ + uri: url + }, function (error, data) { + if (error || !data.body) { + console.log("Error " + error + ": unable to post " + url); + console.log(data); + return; + } + var content = JSON.parse(data.body); + if(!content || content.status != 'success') { + console.log(data.body); + console.log("Error " + content.status + ": unable to post " + url); + return; + } + console.log(content.data); + content.data.users.forEach(function(user) { + if(user.online) { + callback(NotificationType.PEOPLE); + var notification = new HifiNotification(NotificationType.PEOPLE, user); + notification.show(); + } + }); + }); + }, + pollForMarketplaceUpdates: function(since, callback) { + var _this = this; + var options = [ + 'filter=connections', + 'since=' + since.toISOString(), + 'status=online' + ]; + console.log("Polling for marketplace update"); + var url = METAVERSE_SERVER_URL + UPDATES_URL + '?' + options.join('&'); + request.put({ + uri: url + }, function (error, data) { + if (error || !data.body) { + console.log("Error " + error + ": unable to put " + url); + return; + } + var content = JSON.parse(data.body); + if(!content || content.status != 'success') { + console.log(data.body); + console.log("Error " + content.status + ": unable to put " + url); + return; + } + content.data.users.forEach(function(user) { + if(user.online) { + callback(NotificationType.PEOPLE); + var notification = new HifiNotification(NotificationType.PEOPLE, user); + notification.show(); + } + }); + }); + } +}; + +exports.HifiNotifications = HifiNotifications; +exports.NotificationType = NotificationType; \ No newline at end of file diff --git a/server-console/src/modules/hf-process.js b/server-console/src/modules/hf-process.js index 797ee38a0d..7fbc9a894e 100644 --- a/server-console/src/modules/hf-process.js +++ b/server-console/src/modules/hf-process.js @@ -259,6 +259,24 @@ Process.prototype = extend(Process.prototype, { }; return logs; }, + isRunning: function(done) { + var _command = this.command; + if (os.type == 'Windows_NT') { + childProcess.exec('tasklist /FO CSV', function(err, stdout, stderr) { + var running = false; + stdout.split("\n").forEach(function(line) { + var exeData = line.split(","); + var executable = exeData[0].replace(/\"/g, "").toLowerCase(); + if(executable == _command) { + running = true; + } + }); + done(running); + }); + } else if (os.type == 'Darwin') { + console.log("TODO IsRunning Darwin"); + } + }, // Events onChildStartError: function(error) {