Checkpoint Tray Notifier

This commit is contained in:
Roxanne Skelly 2018-09-14 11:35:20 -07:00
parent 0bf064c451
commit 5edb76ef34
5 changed files with 493 additions and 88 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

View file

@ -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);

View file

@ -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);
}

View file

@ -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;

View file

@ -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) {