Clean logs on start - and eslint/mocha with code coverage (#1945)
* Clean logs on startup; install server-side testing/linting * Add eslint config, make all of app/ conform to its demands * Add Node.js testing and linting to CI * Lock project to Node.js 7.9.0, used by Electron 1.7.10 * New eslint error: trailing commas in function argumensts Node 7.9.0 doesn't like trailing commas, but Electron does * Move electron to devDependency, tell eslint it's built-in
This commit is contained in:
parent
6464d0a5fa
commit
64fe9dbfb2
21 changed files with 1782 additions and 316 deletions
17
.eslintignore
Normal file
17
.eslintignore
Normal file
|
@ -0,0 +1,17 @@
|
|||
build/**
|
||||
components/**
|
||||
dist/**
|
||||
libtextsecure/**
|
||||
coverage/**
|
||||
|
||||
# these aren't ready yet, pulling files in one-by-one
|
||||
js/**
|
||||
test/**
|
||||
/*.js
|
||||
!main.js
|
||||
!prepare_build.js
|
||||
|
||||
# all of these files will be new
|
||||
!test/server/**/*.js
|
||||
|
||||
# all of app/ is included
|
41
.eslintrc.js
Normal file
41
.eslintrc.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
settings: {
|
||||
'import/core-modules': [
|
||||
'electron'
|
||||
]
|
||||
},
|
||||
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'comma-dangle': ['error', {
|
||||
arrays: 'always-multiline',
|
||||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
functions: 'never',
|
||||
}],
|
||||
|
||||
// putting params on their own line helps stay within line length limit
|
||||
'function-paren-newline': ['error', 'consistent'],
|
||||
|
||||
// 90 characters allows three+ side-by-side screens on a standard-size monitor
|
||||
'max-len': ['error', {
|
||||
code: 90,
|
||||
ignoreUrls: true,
|
||||
}],
|
||||
|
||||
// it helps readability to put public API at top,
|
||||
'no-use-before-define': 'off',
|
||||
|
||||
// useful for unused or internal fields
|
||||
'no-underscore-dangle': 'off',
|
||||
|
||||
// though we have a logger, we still remap console to log to disk
|
||||
'no-console': 'off',
|
||||
}
|
||||
};
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
node_modules
|
||||
.sass-cache
|
||||
coverage/*
|
||||
build/curve25519_compiled.js
|
||||
build/icons/*
|
||||
stylesheets/*.css.map
|
||||
|
@ -11,3 +12,5 @@ config/local-*.json
|
|||
*.provisionprofile
|
||||
release/
|
||||
/dev-app-update.yml
|
||||
.nyc_output/
|
||||
*.sublime*
|
||||
|
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
7.9.0
|
|
@ -1,6 +1,6 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 'node'
|
||||
- '7.9.0'
|
||||
os:
|
||||
- linux
|
||||
dist: trusty
|
||||
|
@ -9,6 +9,8 @@ install:
|
|||
script:
|
||||
- yarn run generate
|
||||
- yarn prepare-build
|
||||
- yarn eslint
|
||||
- yarn test-server
|
||||
- ./node_modules/.bin/build --em.environment=$SIGNAL_ENV --config.mac.bundleVersion='$TRAVIS_BUILD_NUMBER' --publish=never
|
||||
- ./travis.sh
|
||||
env:
|
||||
|
|
|
@ -39,3 +39,6 @@ Gruntfile.js
|
|||
# misc
|
||||
*.gz
|
||||
*.md
|
||||
|
||||
# asset directories
|
||||
!nyc/node_modules/istanbul-reports/lib/html/assets
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const autoUpdater = require('electron-updater').autoUpdater
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const { dialog } = require('electron');
|
||||
|
||||
const config = require('./config');
|
||||
|
@ -18,7 +18,7 @@ function checkForUpdates() {
|
|||
autoUpdater.checkForUpdates();
|
||||
}
|
||||
|
||||
var showingDialog = false;
|
||||
let showingDialog = false;
|
||||
function showUpdateDialog(mainWindow, messages) {
|
||||
if (showingDialog) {
|
||||
return;
|
||||
|
@ -29,21 +29,21 @@ function showUpdateDialog(mainWindow, messages) {
|
|||
type: 'info',
|
||||
buttons: [
|
||||
messages.autoUpdateRestartButtonLabel.message,
|
||||
messages.autoUpdateLaterButtonLabel.message
|
||||
messages.autoUpdateLaterButtonLabel.message,
|
||||
],
|
||||
title: messages.autoUpdateNewVersionTitle.message,
|
||||
message: messages.autoUpdateNewVersionMessage.message,
|
||||
detail: messages.autoUpdateNewVersionInstructions.message,
|
||||
defaultId: LATER_BUTTON,
|
||||
cancelId: RESTART_BUTTON,
|
||||
}
|
||||
};
|
||||
|
||||
dialog.showMessageBox(mainWindow, options, function(response) {
|
||||
if (response == RESTART_BUTTON) {
|
||||
dialog.showMessageBox(mainWindow, options, (response) => {
|
||||
if (response === RESTART_BUTTON) {
|
||||
// We delay these update calls because they don't seem to work in this
|
||||
// callback - but only if the message box has a parent window.
|
||||
// Fixes this bug: https://github.com/WhisperSystems/Signal-Desktop/issues/1864
|
||||
setTimeout(function() {
|
||||
setTimeout(() => {
|
||||
windowState.markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
}, 200);
|
||||
|
@ -54,7 +54,7 @@ function showUpdateDialog(mainWindow, messages) {
|
|||
}
|
||||
|
||||
function onError(error) {
|
||||
console.log("Got an error while updating: ", error.stack);
|
||||
console.log('Got an error while updating: ', error.stack);
|
||||
}
|
||||
|
||||
function initialize(getMainWindow, messages) {
|
||||
|
@ -66,7 +66,7 @@ function initialize(getMainWindow, messages) {
|
|||
return;
|
||||
}
|
||||
|
||||
autoUpdater.addListener('update-downloaded', function() {
|
||||
autoUpdater.addListener('update-downloaded', () => {
|
||||
showUpdateDialog(getMainWindow(), messages);
|
||||
});
|
||||
autoUpdater.addListener('error', onError);
|
||||
|
@ -77,5 +77,5 @@ function initialize(getMainWindow, messages) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
initialize
|
||||
initialize,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
const path = require('path');
|
||||
|
||||
const config = require('config');
|
||||
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
|
||||
const environment = packageJson.environment || process.env.NODE_ENV || 'development';
|
||||
config.environment = environment;
|
||||
|
||||
// Set environment vars to configure node-config before requiring it
|
||||
process.env.NODE_ENV = environment;
|
||||
|
@ -19,8 +22,6 @@ if (environment === 'production') {
|
|||
process.env.SUPPRESS_NO_CONFIG_WARNING = '';
|
||||
}
|
||||
|
||||
const config = require('config');
|
||||
config.environment = environment;
|
||||
|
||||
// Log resulting env vars in use by config
|
||||
[
|
||||
|
@ -30,9 +31,9 @@ config.environment = environment;
|
|||
'ALLOW_CONFIG_MUTATIONS',
|
||||
'HOSTNAME',
|
||||
'NODE_APP_INSTANCE',
|
||||
'SUPPRESS_NO_CONFIG_WARNING'
|
||||
].forEach(function(s) {
|
||||
console.log(s + ' ' + config.util.getEnv(s));
|
||||
'SUPPRESS_NO_CONFIG_WARNING',
|
||||
].forEach((s) => {
|
||||
console.log(`${s} ${config.util.getEnv(s)}`);
|
||||
});
|
||||
|
||||
module.exports = config;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const app = require('electron').app;
|
||||
const { app } = require('electron');
|
||||
const _ = require('lodash');
|
||||
|
||||
const logger = require('./logging').getLogger();
|
||||
const logging = require('./logging');
|
||||
|
||||
function normalizeLocaleName(locale) {
|
||||
if (/^en-/.test(locale)) {
|
||||
|
@ -28,7 +28,8 @@ function getLocaleMessages(locale) {
|
|||
}
|
||||
|
||||
function load() {
|
||||
let english = getLocaleMessages('en');
|
||||
const logger = logging.getLogger();
|
||||
const english = getLocaleMessages('en');
|
||||
let appLocale = app.getLocale();
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
|
@ -49,7 +50,7 @@ function load() {
|
|||
// We start with english, then overwrite that with anything present in locale
|
||||
messages = _.merge(english, messages);
|
||||
} catch (e) {
|
||||
logger.error('Problem loading messages for locale ' + localeName + ' ' + e.stack);
|
||||
logger.error(`Problem loading messages for locale ${localeName} ${e.stack}`);
|
||||
logger.error('Falling back to en locale');
|
||||
|
||||
localeName = 'en';
|
||||
|
@ -58,10 +59,10 @@ function load() {
|
|||
|
||||
return {
|
||||
name: localeName,
|
||||
messages
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
load: load
|
||||
load,
|
||||
};
|
||||
|
|
202
app/logging.js
202
app/logging.js
|
@ -1,22 +1,31 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const electron = require('electron')
|
||||
const electron = require('electron');
|
||||
const bunyan = require('bunyan');
|
||||
const mkdirp = require('mkdirp');
|
||||
const _ = require('lodash');
|
||||
const readFirstLine = require('firstline');
|
||||
const readLastLines = require('read-last-lines').read;
|
||||
|
||||
|
||||
const app = electron.app;
|
||||
const ipc = electron.ipcMain;
|
||||
const {
|
||||
app,
|
||||
ipcMain: ipc,
|
||||
} = electron;
|
||||
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
||||
|
||||
let logger;
|
||||
|
||||
|
||||
function dropFirst(args) {
|
||||
return Array.prototype.slice.call(args, 1);
|
||||
}
|
||||
module.exports = {
|
||||
initialize,
|
||||
getLogger,
|
||||
// for tests only:
|
||||
isLineAfterDate,
|
||||
eliminateOutOfDateFiles,
|
||||
eliminateOldEntries,
|
||||
fetchLog,
|
||||
fetch,
|
||||
};
|
||||
|
||||
function initialize() {
|
||||
if (logger) {
|
||||
|
@ -27,38 +36,114 @@ function initialize() {
|
|||
const logPath = path.join(basePath, 'logs');
|
||||
mkdirp.sync(logPath);
|
||||
|
||||
const logFile = path.join(logPath, 'log.log');
|
||||
return cleanupLogs(logPath).then(() => {
|
||||
const logFile = path.join(logPath, 'log.log');
|
||||
|
||||
logger = bunyan.createLogger({
|
||||
name: 'log',
|
||||
streams: [{
|
||||
level: 'debug',
|
||||
stream: process.stdout
|
||||
}, {
|
||||
type: 'rotating-file',
|
||||
path: logFile,
|
||||
period: '1d',
|
||||
count: 3
|
||||
}]
|
||||
});
|
||||
logger = bunyan.createLogger({
|
||||
name: 'log',
|
||||
streams: [{
|
||||
level: 'debug',
|
||||
stream: process.stdout,
|
||||
}, {
|
||||
type: 'rotating-file',
|
||||
path: logFile,
|
||||
period: '1d',
|
||||
count: 3,
|
||||
}],
|
||||
});
|
||||
|
||||
LEVELS.forEach(function(level) {
|
||||
ipc.on('log-' + level, function() {
|
||||
// first parameter is the event, rest are provided arguments
|
||||
var args = dropFirst(arguments);
|
||||
logger[level].apply(logger, args);
|
||||
LEVELS.forEach((level) => {
|
||||
ipc.on(`log-${level}`, (first, ...rest) => {
|
||||
logger[level](...rest);
|
||||
});
|
||||
});
|
||||
|
||||
ipc.on('fetch-log', (event) => {
|
||||
fetch(logPath).then((data) => {
|
||||
event.sender.send('fetched-log', data);
|
||||
}, (error) => {
|
||||
logger.error(`Problem loading log from disk: ${error.stack}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ipc.on('fetch-log', function(event) {
|
||||
fetch(logPath).then(function(data) {
|
||||
event.sender.send('fetched-log', data);
|
||||
}, function(error) {
|
||||
logger.error('Problem loading log from disk: ' + error.stack);
|
||||
});
|
||||
function cleanupLogs(logPath) {
|
||||
const now = new Date();
|
||||
const earliestDate = new Date(Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate() - 3
|
||||
));
|
||||
|
||||
return eliminateOutOfDateFiles(logPath, earliestDate).then((remaining) => {
|
||||
const files = _.filter(remaining, file => !file.start && file.end);
|
||||
|
||||
if (!files.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return eliminateOldEntries(files, earliestDate);
|
||||
});
|
||||
}
|
||||
|
||||
function isLineAfterDate(line, date) {
|
||||
if (!line) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
return (new Date(data.time)).getTime() > date.getTime();
|
||||
} catch (e) {
|
||||
console.log('error parsing log line', e.stack, line);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function eliminateOutOfDateFiles(logPath, date) {
|
||||
const files = fs.readdirSync(logPath);
|
||||
const paths = files.map(file => path.join(logPath, file));
|
||||
|
||||
return Promise.all(_.map(
|
||||
paths,
|
||||
target => Promise.all([
|
||||
readFirstLine(target),
|
||||
readLastLines(target, 2),
|
||||
]).then((results) => {
|
||||
const start = results[0];
|
||||
const end = results[1].split('\n');
|
||||
|
||||
const file = {
|
||||
path: target,
|
||||
start: isLineAfterDate(start, date),
|
||||
end: isLineAfterDate(end[end.length - 1], date)
|
||||
|| isLineAfterDate(end[end.length - 2], date),
|
||||
};
|
||||
|
||||
if (!file.start && !file.end) {
|
||||
fs.unlinkSync(file.path);
|
||||
}
|
||||
|
||||
return file;
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
function eliminateOldEntries(files, date) {
|
||||
const earliest = date.getTime();
|
||||
|
||||
return Promise.all(_.map(
|
||||
files,
|
||||
file => fetchLog(file.path).then((lines) => {
|
||||
const recent = _.filter(lines, line => (new Date(line.time)).getTime() >= earliest);
|
||||
const text = _.map(recent, line => JSON.stringify(line)).join('\n');
|
||||
|
||||
return fs.writeFileSync(file.path, `${text}\n`);
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
function getLogger() {
|
||||
if (!logger) {
|
||||
throw new Error('Logger hasn\'t been initialized yet!');
|
||||
|
@ -68,18 +153,19 @@ function getLogger() {
|
|||
}
|
||||
|
||||
function fetchLog(logFile) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
fs.readFile(logFile, { encoding: 'utf8' }, function(err, text) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(logFile, { encoding: 'utf8' }, (err, text) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const lines = _.compact(text.split('\n'));
|
||||
const data = _.compact(lines.map(function(line) {
|
||||
const data = _.compact(lines.map((line) => {
|
||||
try {
|
||||
return _.pick(JSON.parse(line), ['level', 'time', 'msg']);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
catch (e) {}
|
||||
}));
|
||||
|
||||
return resolve(data);
|
||||
|
@ -89,19 +175,17 @@ function fetchLog(logFile) {
|
|||
|
||||
function fetch(logPath) {
|
||||
const files = fs.readdirSync(logPath);
|
||||
const paths = files.map(function(file) {
|
||||
return path.join(logPath, file)
|
||||
});
|
||||
const paths = files.map(file => path.join(logPath, file));
|
||||
|
||||
// creating a manual log entry for the final log result
|
||||
var now = new Date();
|
||||
const now = new Date();
|
||||
const fileListEntry = {
|
||||
level: 30, // INFO
|
||||
time: now.toJSON(),
|
||||
msg: 'Loaded this list of log files from logPath: ' + files.join(', '),
|
||||
msg: `Loaded this list of log files from logPath: ${files.join(', ')}`,
|
||||
};
|
||||
|
||||
return Promise.all(paths.map(fetchLog)).then(function(results) {
|
||||
return Promise.all(paths.map(fetchLog)).then((results) => {
|
||||
const data = _.flatten(results);
|
||||
|
||||
data.push(fileListEntry);
|
||||
|
@ -111,18 +195,14 @@ function fetch(logPath) {
|
|||
}
|
||||
|
||||
|
||||
function logAtLevel() {
|
||||
const level = arguments[0];
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
function logAtLevel(level, ...args) {
|
||||
if (logger) {
|
||||
// To avoid [Object object] in our log since console.log handles non-strings smoothly
|
||||
const str = args.map(function(item) {
|
||||
const str = args.map((item) => {
|
||||
if (typeof item !== 'string') {
|
||||
try {
|
||||
return JSON.stringify(item);
|
||||
}
|
||||
catch (e) {
|
||||
} catch (e) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
@ -131,20 +211,16 @@ function logAtLevel() {
|
|||
});
|
||||
logger[level](str.join(' '));
|
||||
} else {
|
||||
console._log.apply(console, consoleArgs);
|
||||
console._log(...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console._log = console.log;
|
||||
console.log = _.partial(logAtLevel, 'info');
|
||||
console._error = console.error;
|
||||
console.error = _.partial(logAtLevel, 'error');
|
||||
console._warn = console.warn;
|
||||
console.warn = _.partial(logAtLevel, 'warn');
|
||||
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
getLogger,
|
||||
};
|
||||
// This blows up using mocha --watch, so we ensure it is run just once
|
||||
if (!console._log) {
|
||||
console._log = console.log;
|
||||
console.log = _.partial(logAtLevel, 'info');
|
||||
console._error = console.error;
|
||||
console.error = _.partial(logAtLevel, 'error');
|
||||
console._warn = console.warn;
|
||||
console.warn = _.partial(logAtLevel, 'warn');
|
||||
}
|
||||
|
|
43
app/menu.js
43
app/menu.js
|
@ -1,18 +1,20 @@
|
|||
function createTemplate(options, messages) {
|
||||
const showDebugLog = options.showDebugLog;
|
||||
const showAbout = options.showAbout;
|
||||
const openReleaseNotes = options.openReleaseNotes;
|
||||
const openNewBugForm = options.openNewBugForm;
|
||||
const openSupportPage = options.openSupportPage;
|
||||
const openForums = options.openForums;
|
||||
const {
|
||||
showDebugLog,
|
||||
showAbout,
|
||||
openReleaseNotes,
|
||||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
} = options;
|
||||
|
||||
let template = [{
|
||||
const template = [{
|
||||
label: messages.mainMenuFile.message,
|
||||
submenu: [
|
||||
{
|
||||
role: 'quit',
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuEdit.message,
|
||||
|
@ -43,8 +45,8 @@ function createTemplate(options, messages) {
|
|||
},
|
||||
{
|
||||
role: 'selectall',
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuView.message,
|
||||
|
@ -77,7 +79,7 @@ function createTemplate(options, messages) {
|
|||
{
|
||||
role: 'toggledevtools',
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuWindow.message,
|
||||
|
@ -86,7 +88,7 @@ function createTemplate(options, messages) {
|
|||
{
|
||||
role: 'minimize',
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
label: messages.mainMenuHelp.message,
|
||||
|
@ -118,7 +120,7 @@ function createTemplate(options, messages) {
|
|||
label: messages.aboutSignalDesktop.message,
|
||||
click: showAbout,
|
||||
},
|
||||
]
|
||||
],
|
||||
}];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
|
@ -129,8 +131,10 @@ function createTemplate(options, messages) {
|
|||
}
|
||||
|
||||
function updateForMac(template, messages, options) {
|
||||
const showWindow = options.showWindow;
|
||||
const showAbout = options.showAbout;
|
||||
const {
|
||||
showWindow,
|
||||
showAbout,
|
||||
} = options;
|
||||
|
||||
// Remove About item and separator from Help menu, since it's on the first menu
|
||||
template[4].submenu.pop();
|
||||
|
@ -162,13 +166,13 @@ function updateForMac(template, messages, options) {
|
|||
{
|
||||
role: 'quit',
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
// Add to Edit menu
|
||||
template[1].submenu.push(
|
||||
{
|
||||
type: 'separator'
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: messages.speech.message,
|
||||
|
@ -179,11 +183,12 @@ function updateForMac(template, messages, options) {
|
|||
{
|
||||
role: 'stopspeaking',
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
// Add to Window menu
|
||||
// Replace Window menu
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
template[3].submenu = [
|
||||
{
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
const electron = require('electron')
|
||||
const path = require('path');
|
||||
|
||||
const app = electron.app;
|
||||
const Menu = electron.Menu;
|
||||
const Tray = electron.Tray;
|
||||
const {
|
||||
app,
|
||||
Menu,
|
||||
Tray,
|
||||
} = require('electron');
|
||||
|
||||
let trayContextMenu = null;
|
||||
let tray = null;
|
||||
|
||||
function createTrayIcon(getMainWindow, messages) {
|
||||
|
||||
// A smaller icon is needed on macOS
|
||||
tray = new Tray(
|
||||
process.platform == "darwin" ?
|
||||
path.join(__dirname, '..', 'images', 'icon_16.png') :
|
||||
path.join(__dirname, '..', 'images', 'icon_256.png'));
|
||||
process.platform === 'darwin' ?
|
||||
path.join(__dirname, '..', 'images', 'icon_16.png') :
|
||||
path.join(__dirname, '..', 'images', 'icon_256.png')
|
||||
);
|
||||
|
||||
tray.toggleWindowVisibility = function () {
|
||||
var mainWindow = getMainWindow();
|
||||
tray.toggleWindowVisibility = () => {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) {
|
||||
mainWindow.hide();
|
||||
|
@ -33,31 +34,28 @@ function createTrayIcon(getMainWindow, messages) {
|
|||
}
|
||||
}
|
||||
tray.updateContextMenu();
|
||||
}
|
||||
};
|
||||
|
||||
tray.updateContextMenu = function () {
|
||||
|
||||
var mainWindow = getMainWindow();
|
||||
tray.updateContextMenu = () => {
|
||||
const mainWindow = getMainWindow();
|
||||
|
||||
// NOTE: we want to have the show/hide entry available in the tray icon
|
||||
// context menu, since the 'click' event may not work on all platforms.
|
||||
// For details please refer to:
|
||||
// https://github.com/electron/electron/blob/master/docs/api/tray.md.
|
||||
trayContextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
id: 'toggleWindowVisibility',
|
||||
label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
|
||||
click: tray.toggleWindowVisibility
|
||||
},
|
||||
{
|
||||
id: 'quit',
|
||||
label: messages.quit.message,
|
||||
click: app.quit.bind(app)
|
||||
}
|
||||
]);
|
||||
trayContextMenu = Menu.buildFromTemplate([{
|
||||
id: 'toggleWindowVisibility',
|
||||
label: messages[mainWindow.isVisible() ? 'hide' : 'show'].message,
|
||||
click: tray.toggleWindowVisibility,
|
||||
},
|
||||
{
|
||||
id: 'quit',
|
||||
label: messages.quit.message,
|
||||
click: app.quit.bind(app),
|
||||
}]);
|
||||
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
}
|
||||
};
|
||||
|
||||
tray.on('click', tray.toggleWindowVisibility);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const path = require('path');
|
||||
|
||||
const app = require('electron').app;
|
||||
const { app } = require('electron');
|
||||
const ElectronConfig = require('electron-config');
|
||||
|
||||
const config = require('./config');
|
||||
|
@ -10,13 +10,13 @@ const config = require('./config');
|
|||
if (config.has('storageProfile')) {
|
||||
const userData = path.join(
|
||||
app.getPath('appData'),
|
||||
'Signal-' + config.get('storageProfile')
|
||||
`Signal-${config.get('storageProfile')}`
|
||||
);
|
||||
|
||||
app.setPath('userData', userData);
|
||||
}
|
||||
|
||||
console.log('userData: ' + app.getPath('userData'));
|
||||
console.log(`userData: ${app.getPath('userData')}`);
|
||||
|
||||
// this needs to be below our update to the appData path
|
||||
const userConfig = new ElectronConfig();
|
||||
|
|
|
@ -10,5 +10,5 @@ function shouldQuit() {
|
|||
|
||||
module.exports = {
|
||||
shouldQuit,
|
||||
markShouldQuit
|
||||
markShouldQuit,
|
||||
};
|
||||
|
|
|
@ -8,10 +8,12 @@ cache:
|
|||
install:
|
||||
- systeminfo | findstr /C:"OS"
|
||||
- set PATH=C:\Ruby23-x64\bin;%PATH%
|
||||
- ps: Install-Product node 6 x64
|
||||
- ps: Install-Product node 7.9.0 x64
|
||||
- yarn install
|
||||
|
||||
build_script:
|
||||
- yarn eslint
|
||||
- yarn test-server
|
||||
- yarn run icon-gen
|
||||
- node build\grunt.js
|
||||
- type package.json | findstr /v certificateSubjectName > temp.json
|
||||
|
|
184
main.js
184
main.js
|
@ -6,19 +6,25 @@ const _ = require('lodash');
|
|||
const electron = require('electron');
|
||||
const semver = require('semver');
|
||||
|
||||
const BrowserWindow = electron.BrowserWindow;
|
||||
const app = electron.app;
|
||||
const ipc = electron.ipcMain;
|
||||
const Menu = electron.Menu;
|
||||
const shell = electron.shell;
|
||||
const {
|
||||
BrowserWindow,
|
||||
app,
|
||||
Menu,
|
||||
shell,
|
||||
ipcMain: ipc,
|
||||
} = electron;
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
const createTrayIcon = require('./app/tray_icon');
|
||||
const createTemplate = require('./app/menu.js');
|
||||
const logging = require('./app/logging');
|
||||
const autoUpdate = require('./app/auto_update');
|
||||
const windowState = require('./app/window_state');
|
||||
|
||||
|
||||
const aumid = 'org.whispersystems.' + packageJson.name;
|
||||
console.log('setting AUMID to ' + aumid);
|
||||
const aumid = `org.whispersystems.${packageJson.name}`;
|
||||
console.log(`setting AUMID to ${aumid}`);
|
||||
app.setAppUserModelId(aumid);
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
|
@ -34,7 +40,7 @@ let tray = null;
|
|||
const startInTray = process.argv.find(arg => arg === '--start-in-tray');
|
||||
const usingTrayIcon = startInTray || process.argv.find(arg => arg === '--use-tray-icon');
|
||||
|
||||
const config = require("./app/config");
|
||||
const config = require('./app/config');
|
||||
|
||||
// Very important to put before the single instance check, since it is based on the
|
||||
// userData directory.
|
||||
|
@ -63,7 +69,7 @@ function showWindow() {
|
|||
|
||||
if (!process.mas) {
|
||||
console.log('making app single instance');
|
||||
var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) {
|
||||
const shouldQuit = app.makeSingleInstance(() => {
|
||||
// Someone tried to run a second instance, we should focus our window
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
|
@ -78,19 +84,14 @@ if (!process.mas) {
|
|||
if (shouldQuit) {
|
||||
console.log('quitting; we are the second instance');
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const logging = require('./app/logging');
|
||||
|
||||
// This must be after we set up appPath in user_config.js, so we know where logs go
|
||||
logging.initialize();
|
||||
const logger = logging.getLogger();
|
||||
|
||||
let windowConfig = userConfig.get('window');
|
||||
const loadLocale = require('./app/locale').load;
|
||||
|
||||
// Both of these will be set after app fires the 'ready' event
|
||||
let logger;
|
||||
let locale;
|
||||
|
||||
const WINDOWS_8 = '8.0.0';
|
||||
|
@ -118,20 +119,20 @@ function prepareURL(pathSegments) {
|
|||
appInstance: process.env.NODE_APP_INSTANCE,
|
||||
polyfillNotifications: polyfillNotifications ? true : undefined, // for stringify()
|
||||
proxyUrl: process.env.HTTPS_PROXY || process.env.https_proxy,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleUrl(event, target) {
|
||||
event.preventDefault();
|
||||
const protocol = url.parse(target).protocol;
|
||||
const { protocol } = url.parse(target);
|
||||
if (protocol === 'http:' || protocol === 'https:') {
|
||||
shell.openExternal(target);
|
||||
}
|
||||
}
|
||||
|
||||
function captureClicks(window) {
|
||||
window.webContents.on('will-navigate', handleUrl)
|
||||
window.webContents.on('will-navigate', handleUrl);
|
||||
window.webContents.on('new-window', handleUrl);
|
||||
}
|
||||
|
||||
|
@ -150,11 +151,11 @@ function isVisible(window, bounds) {
|
|||
|
||||
// requiring BOUNDS_BUFFER pixels on the left or right side
|
||||
const rightSideClearOfLeftBound = (window.x + window.width >= boundsX + BOUNDS_BUFFER);
|
||||
const leftSideClearOfRightBound = (window.x <= boundsX + boundsWidth - BOUNDS_BUFFER);
|
||||
const leftSideClearOfRightBound = (window.x <= (boundsX + boundsWidth) - BOUNDS_BUFFER);
|
||||
|
||||
// top can't be offscreen, and must show at least BOUNDS_BUFFER pixels at bottom
|
||||
const topClearOfUpperBound = window.y >= boundsY;
|
||||
const topClearOfLowerBound = (window.y <= boundsY + boundsHeight - BOUNDS_BUFFER);
|
||||
const topClearOfLowerBound = (window.y <= (boundsY + boundsHeight) - BOUNDS_BUFFER);
|
||||
|
||||
return rightSideClearOfLeftBound
|
||||
&& leftSideClearOfRightBound
|
||||
|
@ -162,8 +163,8 @@ function isVisible(window, bounds) {
|
|||
&& topClearOfLowerBound;
|
||||
}
|
||||
|
||||
function createWindow () {
|
||||
const screen = electron.screen;
|
||||
function createWindow() {
|
||||
const { screen } = electron;
|
||||
const windowOptions = Object.assign({
|
||||
show: !startInTray, // allow to start minimised in tray
|
||||
width: DEFAULT_WIDTH,
|
||||
|
@ -173,8 +174,8 @@ function createWindow () {
|
|||
autoHideMenuBar: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
//sandbox: true,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
// sandbox: true,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
icon: path.join(__dirname, 'images', 'icon_256.png'),
|
||||
}, _.pick(windowConfig, ['maximized', 'autoHideMenuBar', 'width', 'height', 'x', 'y']));
|
||||
|
@ -192,7 +193,7 @@ function createWindow () {
|
|||
delete windowOptions.autoHideMenuBar;
|
||||
}
|
||||
|
||||
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), function(display) {
|
||||
const visibleOnAnyScreen = _.some(screen.getAllDisplays(), (display) => {
|
||||
if (!_.isNumber(windowOptions.x) || !_.isNumber(windowOptions.y)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -225,7 +226,7 @@ function createWindow () {
|
|||
width: size[0],
|
||||
height: size[1],
|
||||
x: position[0],
|
||||
y: position[1]
|
||||
y: position[1],
|
||||
};
|
||||
|
||||
if (mainWindow.isFullScreen()) {
|
||||
|
@ -243,12 +244,13 @@ function createWindow () {
|
|||
mainWindow.on('move', debouncedCaptureStats);
|
||||
mainWindow.on('close', captureAndSaveWindowStats);
|
||||
|
||||
mainWindow.on('focus', function() {
|
||||
mainWindow.on('focus', () => {
|
||||
mainWindow.flashFrame(false);
|
||||
});
|
||||
|
||||
// Ingested in preload.js via a sendSync call
|
||||
ipc.on('locale-data', function(event, arg) {
|
||||
ipc.on('locale-data', (event) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.returnValue = locale.messages;
|
||||
});
|
||||
|
||||
|
@ -262,23 +264,21 @@ function createWindow () {
|
|||
|
||||
if (config.get('openDevTools')) {
|
||||
// Open the DevTools.
|
||||
mainWindow.webContents.openDevTools()
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
captureClicks(mainWindow);
|
||||
|
||||
mainWindow.webContents.on('will-navigate', function(e) {
|
||||
mainWindow.webContents.on('will-navigate', (e) => {
|
||||
logger.info('will-navigate');
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Emitted when the window is about to be closed.
|
||||
mainWindow.on('close', function (e) {
|
||||
|
||||
mainWindow.on('close', (e) => {
|
||||
// If the application is terminating, just do the default
|
||||
if (windowState.shouldQuit()
|
||||
|| config.environment === 'test' || config.environment === 'test-lib') {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -296,26 +296,26 @@ function createWindow () {
|
|||
});
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
mainWindow.on('closed', () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
mainWindow = null
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
ipc.on('show-window', function() {
|
||||
ipc.on('show-window', () => {
|
||||
showWindow();
|
||||
});
|
||||
}
|
||||
|
||||
function showDebugLog() {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('debug-log')
|
||||
mainWindow.webContents.send('debug-log');
|
||||
}
|
||||
}
|
||||
|
||||
function openReleaseNotes() {
|
||||
shell.openExternal('https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v' + app.getVersion());
|
||||
shell.openExternal(`https://github.com/WhisperSystems/Signal-Desktop/releases/tag/v${app.getVersion()}`);
|
||||
}
|
||||
|
||||
function openNewBugForm() {
|
||||
|
@ -348,7 +348,7 @@ function showAbout() {
|
|||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
parent: mainWindow,
|
||||
};
|
||||
|
@ -359,11 +359,11 @@ function showAbout() {
|
|||
|
||||
aboutWindow.loadURL(prepareURL([__dirname, 'about.html']));
|
||||
|
||||
aboutWindow.on('closed', function () {
|
||||
aboutWindow.on('closed', () => {
|
||||
aboutWindow = null;
|
||||
});
|
||||
|
||||
aboutWindow.once('ready-to-show', function() {
|
||||
aboutWindow.once('ready-to-show', () => {
|
||||
aboutWindow.show();
|
||||
});
|
||||
}
|
||||
|
@ -372,53 +372,64 @@ function showAbout() {
|
|||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
let ready = false;
|
||||
app.on('ready', function() {
|
||||
logger.info('app ready');
|
||||
ready = true;
|
||||
app.on('ready', () => {
|
||||
let loggingSetupError;
|
||||
logging.initialize().catch((error) => {
|
||||
loggingSetupError = error;
|
||||
}).then(() => {
|
||||
logger = logging.getLogger();
|
||||
logger.info('app ready');
|
||||
|
||||
if (!locale) {
|
||||
locale = loadLocale();
|
||||
}
|
||||
if (loggingSetupError) {
|
||||
logger.error('Problem setting up logging', loggingSetupError.stack);
|
||||
}
|
||||
|
||||
autoUpdate.initialize(getMainWindow, locale.messages);
|
||||
if (!locale) {
|
||||
locale = loadLocale();
|
||||
}
|
||||
|
||||
createWindow();
|
||||
ready = true;
|
||||
|
||||
if (usingTrayIcon) {
|
||||
const createTrayIcon = require("./app/tray_icon");
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
autoUpdate.initialize(getMainWindow, locale.messages);
|
||||
|
||||
const options = {
|
||||
showDebugLog,
|
||||
showWindow,
|
||||
showAbout,
|
||||
openReleaseNotes,
|
||||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
};
|
||||
const createTemplate = require('./app/menu.js');
|
||||
const template = createTemplate(options, locale.messages);
|
||||
createWindow();
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
})
|
||||
if (usingTrayIcon) {
|
||||
tray = createTrayIcon(getMainWindow, locale.messages);
|
||||
}
|
||||
|
||||
app.on('before-quit', function() {
|
||||
const options = {
|
||||
showDebugLog,
|
||||
showWindow,
|
||||
showAbout,
|
||||
openReleaseNotes,
|
||||
openNewBugForm,
|
||||
openSupportPage,
|
||||
openForums,
|
||||
};
|
||||
const template = createTemplate(options, locale.messages);
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
windowState.markShouldQuit();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', function () {
|
||||
app.on('window-all-closed', () => {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin' || config.environment === 'test' || config.environment === 'test-lib') {
|
||||
app.quit()
|
||||
if (process.platform !== 'darwin'
|
||||
|| config.environment === 'test'
|
||||
|| config.environment === 'test-lib') {
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('activate', function () {
|
||||
app.on('activate', () => {
|
||||
if (!ready) {
|
||||
return;
|
||||
}
|
||||
|
@ -430,46 +441,43 @@ app.on('activate', function () {
|
|||
} else {
|
||||
createWindow();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
ipc.on('set-badge-count', function(event, count) {
|
||||
ipc.on('set-badge-count', (event, count) => {
|
||||
app.setBadgeCount(count);
|
||||
});
|
||||
|
||||
ipc.on('draw-attention', function(event, count) {
|
||||
ipc.on('draw-attention', () => {
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.bounce();
|
||||
} else if (process.platform == 'win32') {
|
||||
} else if (process.platform === 'win32') {
|
||||
mainWindow.flashFrame(true);
|
||||
setTimeout(function() {
|
||||
setTimeout(() => {
|
||||
mainWindow.flashFrame(false);
|
||||
}, 1000);
|
||||
} else if (process.platform == 'linux') {
|
||||
} else if (process.platform === 'linux') {
|
||||
mainWindow.flashFrame(true);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on('restart', function(event) {
|
||||
ipc.on('restart', () => {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipc.on("set-auto-hide-menu-bar", function(event, autoHide) {
|
||||
ipc.on('set-auto-hide-menu-bar', (event, autoHide) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.setAutoHideMenuBar(autoHide);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on("set-menu-bar-visibility", function(event, visibility) {
|
||||
ipc.on('set-menu-bar-visibility', (event, visibility) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.setMenuBarVisibility(visibility);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on("close-about", function() {
|
||||
ipc.on('close-about', () => {
|
||||
if (aboutWindow) {
|
||||
aboutWindow.close();
|
||||
}
|
||||
|
|
99
package.json
99
package.json
|
@ -10,26 +10,6 @@
|
|||
"email": "support@whispersystems.org"
|
||||
},
|
||||
"main": "main.js",
|
||||
"devDependencies": {
|
||||
"asar": "^0.14.0",
|
||||
"bower": "^1.8.2",
|
||||
"electron": "1.7.10",
|
||||
"electron-builder": "^19.49.2",
|
||||
"electron-icon-maker": "^0.0.4",
|
||||
"electron-publisher-s3": "^19.49.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-cli": "^1.2.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-jshint": "^1.1.0",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-exec": "^3.0.0",
|
||||
"grunt-gitinfo": "^0.1.7",
|
||||
"grunt-jscs": "^3.0.1",
|
||||
"grunt-sass": "^2.0.0",
|
||||
"node-sass-import-once": "^1.2.0",
|
||||
"spectron": "^3.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && rimraf node_modules/dtrace-provider",
|
||||
"test": "grunt test",
|
||||
|
@ -53,7 +33,62 @@
|
|||
"release-mac": "npm run build-release -- -m --prepackaged release/mac/Signal*.app --publish=always",
|
||||
"release-win": "npm run build-release -- -w --prepackaged release/windows --publish=always",
|
||||
"release-lin": "npm run build-release -- -l --prepackaged release/linux && NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh",
|
||||
"release": "npm run release-mac && npm run release-win && npm run release-lin"
|
||||
"release": "npm run release-mac && npm run release-win && npm run release-lin",
|
||||
"test-server": "mocha --recursive test/server",
|
||||
"test-server-coverage": "nyc --reporter=lcov --reporter=text mocha --recursive test/server",
|
||||
"eslint": "eslint .",
|
||||
"open-coverage": "open coverage/lcov-report/index.html"
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.12",
|
||||
"config": "^1.28.1",
|
||||
"electron-config": "^1.0.0",
|
||||
"electron-editor-context-menu": "^1.1.1",
|
||||
"electron-updater": "^2.17.6",
|
||||
"emoji-datasource": "4.0.0",
|
||||
"emoji-datasource-apple": "4.0.0",
|
||||
"emoji-js": "^3.4.0",
|
||||
"emoji-panel": "https://github.com/scottnonnenberg/emoji-panel.git#v0.5.5",
|
||||
"firstline": "^1.2.1",
|
||||
"google-libphonenumber": "^3.0.7",
|
||||
"lodash": "^4.17.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-fetch": "https://github.com/scottnonnenberg/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
|
||||
"node-notifier": "^5.1.2",
|
||||
"os-locale": "^2.1.0",
|
||||
"proxy-agent": "^2.1.0",
|
||||
"read-last-lines": "^1.3.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
"spellchecker": "^3.4.4",
|
||||
"websocket": "^1.0.25"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asar": "^0.14.0",
|
||||
"bower": "^1.8.2",
|
||||
"chai": "^4.1.2",
|
||||
"electron": "1.7.10",
|
||||
"electron-builder": "^19.49.2",
|
||||
"electron-icon-maker": "^0.0.4",
|
||||
"electron-publisher-s3": "^19.49.0",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-cli": "^1.2.0",
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-copy": "^1.0.0",
|
||||
"grunt-contrib-jshint": "^1.1.0",
|
||||
"grunt-contrib-watch": "^1.0.0",
|
||||
"grunt-exec": "^3.0.0",
|
||||
"grunt-gitinfo": "^0.1.7",
|
||||
"grunt-jscs": "^3.0.1",
|
||||
"grunt-sass": "^2.0.0",
|
||||
"mocha": "^4.1.0",
|
||||
"node-sass-import-once": "^1.2.0",
|
||||
"nyc": "^11.4.1",
|
||||
"spectron": "^3.7.2",
|
||||
"tmp": "^0.0.33"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.whispersystems.signal-desktop",
|
||||
|
@ -169,27 +204,5 @@
|
|||
"node_modules/spellchecker/build/Release/*.node",
|
||||
"node_modules/websocket/build/Release/*.node"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"bunyan": "^1.8.12",
|
||||
"config": "^1.28.1",
|
||||
"electron-config": "^1.0.0",
|
||||
"electron-editor-context-menu": "^1.1.1",
|
||||
"electron-updater": "^2.17.6",
|
||||
"emoji-datasource": "4.0.0",
|
||||
"emoji-datasource-apple": "4.0.0",
|
||||
"emoji-js": "^3.4.0",
|
||||
"emoji-panel": "https://github.com/scottnonnenberg/emoji-panel.git#v0.5.5",
|
||||
"google-libphonenumber": "^3.0.7",
|
||||
"lodash": "^4.17.4",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-fetch": "https://github.com/scottnonnenberg/node-fetch.git#3e5f51e08c647ee5f20c43b15cf2d352d61c36b4",
|
||||
"node-notifier": "^5.1.2",
|
||||
"os-locale": "^2.1.0",
|
||||
"proxy-agent": "^2.1.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"semver": "^5.4.1",
|
||||
"spellchecker": "^3.4.4",
|
||||
"websocket": "^1.0.25"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ const fs = require('fs');
|
|||
const _ = require('lodash');
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
const version = packageJson.version;
|
||||
|
||||
|
||||
const { version } = packageJson;
|
||||
const beta = /beta/;
|
||||
|
||||
// You might be wondering why this file is necessary. It comes down to our desire to allow
|
||||
|
@ -12,7 +14,7 @@ const beta = /beta/;
|
|||
// adding the ${channel} macro to these values, but Electron-Builder didn't like that.
|
||||
|
||||
if (!beta.test(version)) {
|
||||
return;
|
||||
process.exit();
|
||||
}
|
||||
|
||||
console.log('prepare_build: updating package.json for beta build');
|
||||
|
@ -36,13 +38,12 @@ const PRODUCTION_STARTUP_WM_CLASS = 'Signal';
|
|||
const BETA_STARTUP_WM_CLASS = 'Signal Beta';
|
||||
|
||||
|
||||
|
||||
// -------
|
||||
|
||||
function checkValue(object, objectPath, expected) {
|
||||
const actual = _.get(object, objectPath)
|
||||
const actual = _.get(object, objectPath);
|
||||
if (actual !== expected) {
|
||||
throw new Error(objectPath + ' was ' + actual + '; expected ' + expected);
|
||||
throw new Error(`${objectPath} was ${actual}; expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
17
test/.eslintrc.js
Normal file
17
test/.eslintrc.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
// For reference: https://github.com/airbnb/javascript
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
// We still get the value of this rule, it just allows for dev deps
|
||||
'import/no-extraneous-dependencies': ['error', {
|
||||
devDependencies: true
|
||||
}],
|
||||
|
||||
// We want to keep each test structured the same, even if its contents are tiny
|
||||
'arrow-body-style': 'off',
|
||||
}
|
||||
};
|
271
test/server/app/logging_test.js
Normal file
271
test/server/app/logging_test.js
Normal file
|
@ -0,0 +1,271 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const tmp = require('tmp');
|
||||
const { expect } = require('chai');
|
||||
|
||||
const {
|
||||
eliminateOutOfDateFiles,
|
||||
eliminateOldEntries,
|
||||
isLineAfterDate,
|
||||
fetchLog,
|
||||
fetch,
|
||||
} = require('../../../app/logging');
|
||||
|
||||
describe('app/logging', () => {
|
||||
let basePath;
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = tmp.dirSync({
|
||||
unsafeCleanup: true,
|
||||
});
|
||||
basePath = tmpDir.name;
|
||||
});
|
||||
|
||||
afterEach((done) => {
|
||||
// we need the unsafe option to recursively remove the directory
|
||||
tmpDir.removeCallback(done);
|
||||
});
|
||||
|
||||
describe('#isLineAfterDate', () => {
|
||||
it('returns false if falsy', () => {
|
||||
const actual = isLineAfterDate('', new Date());
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if invalid JSON', () => {
|
||||
const actual = isLineAfterDate('{{}', new Date());
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if date is invalid', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
|
||||
const actual = isLineAfterDate(line, new Date('try6'));
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if log time is invalid', () => {
|
||||
const line = JSON.stringify({ time: 'try7' });
|
||||
const date = new Date('2018-01-04T19:17:00.000Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns false if date before provided date', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:00.000Z' });
|
||||
const date = new Date('2018-01-04T19:17:05.014Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(false);
|
||||
});
|
||||
it('returns true if date is after provided date', () => {
|
||||
const line = JSON.stringify({ time: '2018-01-04T19:17:05.014Z' });
|
||||
const date = new Date('2018-01-04T19:17:00.000Z');
|
||||
const actual = isLineAfterDate(line, date);
|
||||
expect(actual).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#eliminateOutOfDateFiles', () => {
|
||||
it('deletes an empty file', () => {
|
||||
const date = new Date();
|
||||
const log = '\n';
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, log);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('deletes a file with invalid JSON lines', () => {
|
||||
const date = new Date();
|
||||
const log = '{{}\n';
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, log);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('deletes a file with all dates before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:17:05.014Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(false);
|
||||
});
|
||||
});
|
||||
it('keeps a file with first line date before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:16:00.000Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(true);
|
||||
});
|
||||
});
|
||||
it('keeps a file with last line date before provided date', () => {
|
||||
const date = new Date('2018-01-04T19:17:01.000Z');
|
||||
const contents = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const target = path.join(basePath, 'log.log');
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOutOfDateFiles(basePath, date).then(() => {
|
||||
expect(fs.existsSync(target)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#eliminateOldEntries', () => {
|
||||
it('eliminates all non-parsing entries', () => {
|
||||
const date = new Date('2018-01-04T19:17:01.000Z');
|
||||
const contents = [
|
||||
'random line',
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const expected = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
|
||||
const target = path.join(basePath, 'log.log');
|
||||
const files = [{
|
||||
path: target,
|
||||
}];
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOldEntries(files, date).then(() => {
|
||||
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
|
||||
});
|
||||
});
|
||||
it('preserves all lines if before target date', () => {
|
||||
const date = new Date('2018-01-04T19:17:03.000Z');
|
||||
const contents = [
|
||||
'random line',
|
||||
JSON.stringify({ time: '2018-01-04T19:17:01.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:02.014Z' }),
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
const expected = [
|
||||
JSON.stringify({ time: '2018-01-04T19:17:03.014Z' }),
|
||||
].join('\n');
|
||||
|
||||
const target = path.join(basePath, 'log.log');
|
||||
const files = [{
|
||||
path: target,
|
||||
}];
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return eliminateOldEntries(files, date).then(() => {
|
||||
expect(fs.readFileSync(target, 'utf8')).to.equal(`${expected}\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchLog', () => {
|
||||
it('returns error if file does not exist', () => {
|
||||
const target = 'random_file';
|
||||
return fetchLog(target).then(() => {
|
||||
throw new Error('Expected an error!');
|
||||
}, (error) => {
|
||||
expect(error).to.have.property('message').that.match(/random_file/);
|
||||
});
|
||||
});
|
||||
it('returns empty array if file has no valid JSON lines', () => {
|
||||
const contents = 'line 1\nline2\n';
|
||||
const expected = [];
|
||||
const target = path.join(basePath, 'test.log');
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return fetchLog(target).then((result) => {
|
||||
expect(result).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
it('returns just three fields in each returned line', () => {
|
||||
const contents = [
|
||||
JSON.stringify({
|
||||
one: 1,
|
||||
two: 2,
|
||||
level: 1,
|
||||
time: 2,
|
||||
msg: 3,
|
||||
}),
|
||||
JSON.stringify({
|
||||
one: 1,
|
||||
two: 2,
|
||||
level: 2,
|
||||
time: 3,
|
||||
msg: 4,
|
||||
}),
|
||||
'',
|
||||
].join('\n');
|
||||
const expected = [{
|
||||
level: 1,
|
||||
time: 2,
|
||||
msg: 3,
|
||||
}, {
|
||||
level: 2,
|
||||
time: 3,
|
||||
msg: 4,
|
||||
}];
|
||||
|
||||
const target = path.join(basePath, 'test.log');
|
||||
|
||||
fs.writeFileSync(target, contents);
|
||||
|
||||
return fetchLog(target).then((result) => {
|
||||
expect(result).to.deep.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetch', () => {
|
||||
it('returns single entry if no files', () => {
|
||||
return fetch(basePath).then((results) => {
|
||||
expect(results).to.have.length(1);
|
||||
expect(results[0].msg).to.match(/Loaded this list/);
|
||||
});
|
||||
});
|
||||
it('returns sorted entries from all files', () => {
|
||||
const first = [
|
||||
JSON.stringify({ msg: 2, time: '2018-01-04T19:17:05.014Z' }),
|
||||
'',
|
||||
].join('\n');
|
||||
const second = [
|
||||
JSON.stringify({ msg: 1, time: '2018-01-04T19:17:00.014Z' }),
|
||||
JSON.stringify({ msg: 3, time: '2018-01-04T19:18:00.014Z' }),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(path.join(basePath, 'first.log'), first);
|
||||
fs.writeFileSync(path.join(basePath, 'second.log'), second);
|
||||
|
||||
return fetch(basePath).then((results) => {
|
||||
expect(results).to.have.length(4);
|
||||
expect(results[0].msg).to.equal(1);
|
||||
expect(results[1].msg).to.equal(2);
|
||||
expect(results[2].msg).to.equal(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue