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:
Scott Nonnenberg 2018-01-08 13:19:25 -08:00 committed by GitHub
parent 6464d0a5fa
commit 64fe9dbfb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1782 additions and 316 deletions

17
.eslintignore Normal file
View 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
View 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
View File

@ -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
View File

@ -0,0 +1 @@
7.9.0

View File

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

View File

@ -39,3 +39,6 @@ Gruntfile.js
# misc
*.gz
*.md
# asset directories
!nyc/node_modules/istanbul-reports/lib/html/assets

View File

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

View File

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

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

@ -10,5 +10,5 @@ function shouldQuit() {
module.exports = {
shouldQuit,
markShouldQuit
markShouldQuit,
};

View File

@ -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
View File

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

View File

@ -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"
}
}

View File

@ -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
View 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',
}
};

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

1094
yarn.lock

File diff suppressed because it is too large Load Diff