From ec98374bcfa2ea8153529ecc5f56b267155c0495 Mon Sep 17 00:00:00 2001 From: Kiran Niranjan Date: Thu, 19 Apr 2018 17:56:42 +0000 Subject: [PATCH] Electron-433 (Changes the logic for updating user config) (#341) - change logic in handling user config selection on install - fix log issues - Update user config by validating execPath or is mac - Remove post install from aip config - Update user config by validating execPath or is mac - Fix lint issues - use native isNaN method - Remove unwanted code - PR review --- installer/mac/postinstall.sh | 7 +-- installer/win/Symphony-x64.aip | 5 -- installer/win/Symphony-x86.aip | 5 -- js/config.js | 98 ++++++++++++------------------ js/log.js | 3 + js/main.js | 107 ++++++++++++++++++++------------- js/utils/compareSemVersions.js | 86 ++++++++++++++++++++++++++ package.json | 3 +- 8 files changed, 197 insertions(+), 117 deletions(-) create mode 100644 js/utils/compareSemVersions.js diff --git a/installer/mac/postinstall.sh b/installer/mac/postinstall.sh index 7013218e..fbd12f48 100755 --- a/installer/mac/postinstall.sh +++ b/installer/mac/postinstall.sh @@ -93,9 +93,4 @@ sed -i "" -E "s#\"fullscreen\" ?: ?([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])#\"ful sed -i "" -E "s#\"openExternal\" ?: ?([Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])#\"openExternal\":\ $open_external_app#g" ${newPath} ## Remove the temp permissions file created ## -rm -f ${permissionsFilePath} - -## For launching symphony with sandbox enabled, create a shell script that is used as the launch point for the app -EXEC_PATH=${installPath}/Symphony.app/Contents/MacOS -exec ${EXEC_PATH}/Symphony --install ${newPath} ${launch_on_startup} -chmod 755 ${EXEC_PATH}/Symphony \ No newline at end of file +rm -f ${permissionsFilePath} \ No newline at end of file diff --git a/installer/win/Symphony-x64.aip b/installer/win/Symphony-x64.aip index 54cc9bf8..dac07114 100755 --- a/installer/win/Symphony-x64.aip +++ b/installer/win/Symphony-x64.aip @@ -335,7 +335,6 @@ - @@ -648,8 +647,6 @@ - - @@ -672,8 +669,6 @@ - - diff --git a/installer/win/Symphony-x86.aip b/installer/win/Symphony-x86.aip index 268304a2..e7c45313 100755 --- a/installer/win/Symphony-x86.aip +++ b/installer/win/Symphony-x86.aip @@ -321,7 +321,6 @@ - @@ -623,8 +622,6 @@ - - @@ -646,8 +643,6 @@ - - diff --git a/js/config.js b/js/config.js index 473a35a0..2730fe32 100644 --- a/js/config.js +++ b/js/config.js @@ -4,7 +4,6 @@ const electron = require('electron'); const app = electron.app; const path = require('path'); const fs = require('fs'); -const AppDirectory = require('appdirectory'); const omit = require('lodash.omit'); const pick = require('lodash.pick'); const difference = require('lodash.difference'); @@ -16,7 +15,6 @@ const log = require('./log.js'); const logLevels = require('./enums/logLevels.js'); const configFileName = 'Symphony.config'; -const dirs = new AppDirectory('Symphony'); // cached config when first reading files. initially undefined and will be // updated when read from disk. @@ -70,7 +68,11 @@ function getUserConfigField(fieldName) { * @returns {Promise} */ function readUserConfig(customConfigPath) { + + log.send(logLevels.INFO, `custom config path ${customConfigPath}`); + return new Promise((resolve, reject) => { + if (userConfig) { resolve(userConfig); return; @@ -81,26 +83,33 @@ function readUserConfig(customConfigPath) { if (!configPath) { configPath = path.join(app.getPath('userData'), configFileName); } + + log.send(logLevels.INFO, `config path ${configPath}`); fs.readFile(configPath, 'utf8', (err, data) => { + if (err) { - reject('cannot open user config file: ' + configPath + ', error: ' + err); - } else { - try { - // data is the contents of the text file we just read - userConfig = JSON.parse(data); - } catch (e) { - reject('can not parse user config file data: ' + data + ', error: ' + err); - return; - } - resolve(userConfig); + log.send(logLevels.INFO, `cannot open user config file ${configPath}, error is ${err}`); + reject(`cannot open user config file ${configPath}, error is ${err}`); + return; } + + try { + // data is the contents of the text file we just read + userConfig = JSON.parse(data); + log.send(logLevels.INFO, `user config data is ${JSON.stringify(userConfig)}`); + resolve(userConfig); + } catch (e) { + log.send(logLevels.INFO, `cannot parse user config data ${data}, error is ${e}`); + reject(`cannot parse user config data ${data}, error is ${e}`); + } + }); }); } /** - * Gets a specific user config value for a field + * Gets a specific global config value for a field * @param fieldName * @returns {Promise} */ @@ -230,19 +239,18 @@ function updateUserConfig(oldUserConfig) { // create a new object from the old user config // by ommitting the user related settings from // the old user config + log.send(logLevels.INFO, `old user config string ${JSON.stringify(oldUserConfig)}`); let newUserConfig = omit(oldUserConfig, ignoreSettings); let newUserConfigString = JSON.stringify(newUserConfig, null, 2); + log.send(logLevels.INFO, `new config string ${newUserConfigString}`); + // get the user config path let userConfigFile; - if (isMac) { - userConfigFile = path.join(dirs.userConfig(), configFileName); - } else { - userConfigFile = path.join(app.getPath('userData'), configFileName); - } + userConfigFile = path.join(app.getPath('userData'), configFileName); if (!userConfigFile) { - return reject('user config file doesn\'t exist'); + return reject(`user config file doesn't exist`); } // write the new user config changes to the user config file @@ -255,11 +263,10 @@ function updateUserConfig(oldUserConfig) { } /** - * Manipulates user config on windows - * @param {String} perUserInstall - Is a flag to determine if we are installing for an individual user + * Manipulates user config on first time launch * @returns {Promise} */ -function updateUserConfigWin(perUserInstall) { +function updateUserConfigOnLaunch() { return new Promise((resolve, reject) => { @@ -268,36 +275,6 @@ function updateUserConfigWin(perUserInstall) { // if it's not a per user installation or if the // user config file doesn't exist, we simple move on - if (!perUserInstall || !fs.existsSync(userConfigFile)) { - log.send(logLevels.WARN, 'config: Could not find the user config file!'); - reject(); - return; - } - - // In case the file exists, we remove it so that all the - // values are fetched from the global config - // https://perzoinc.atlassian.net/browse/ELECTRON-126 - readUserConfig(userConfigFile).then((data) => { - resolve(updateUserConfig(data)); - }).catch((err) => { - reject(err); - }); - - }); - -} - -/** - * Manipulates user config on macOS - * @param {String} globalConfigPath - The global config path from installer - * @returns {Promise} - */ -function updateUserConfigMac() { - return new Promise((resolve, reject) => { - const userConfigFile = path.join(dirs.userConfig(), configFileName); - - // if user config file does't exist, just use the global config settings - // i.e. until an user makes changes manually using the menu items if (!fs.existsSync(userConfigFile)) { log.send(logLevels.WARN, 'config: Could not find the user config file!'); reject(); @@ -307,13 +284,18 @@ function updateUserConfigMac() { // In case the file exists, we remove it so that all the // values are fetched from the global config // https://perzoinc.atlassian.net/browse/ELECTRON-126 - readUserConfig(userConfigFile).then((data) => { - resolve(updateUserConfig(data)); + readUserConfig(userConfigFile).then((data) => { + // Add version info to the user config data + const version = app.getVersion().toString(); + const updatedData = Object.assign(data, { version }); + + resolve(updateUserConfig(updatedData)); }).catch((err) => { reject(err); }); - + }); + } /** @@ -417,8 +399,8 @@ module.exports = { getConfigField, updateConfigField, - updateUserConfigWin, - updateUserConfigMac, + updateUserConfigOnLaunch, + getMultipleConfigField, // items below here are only exported for testing, do NOT use! @@ -429,5 +411,5 @@ module.exports = { // use only if you specifically need to read global config fields getGlobalConfigField, - + getUserConfigField }; diff --git a/js/log.js b/js/log.js index 00e7f178..aa9be2ac 100644 --- a/js/log.js +++ b/js/log.js @@ -1,5 +1,7 @@ 'use strict'; +const {app} = require('electron'); +const path = require('path'); const getCmdLineArg = require('./utils/getCmdLineArg.js'); const logLevels = require('./enums/logLevels.js'); @@ -102,6 +104,7 @@ let loggerInstance = new Logger(); function initializeLocalLogger() { // eslint-disable-next-line global-require electronLog = require('electron-log'); + electronLog.transports.file.file = path.join(app.getPath('logs'), 'app.log'); electronLog.transports.file.level = 'debug'; electronLog.transports.file.format = '{h}:{i}:{s}:{ms} {text}'; electronLog.transports.file.maxSize = 10 * 1024 * 1024; diff --git a/js/main.js b/js/main.js index f6b45d9b..39e660fb 100644 --- a/js/main.js +++ b/js/main.js @@ -9,9 +9,17 @@ const shellPath = require('shell-path'); const squirrelStartup = require('electron-squirrel-startup'); const AutoLaunch = require('auto-launch'); const urlParser = require('url'); +const nodePath = require('path'); +const compareSemVersions = require('./utils/compareSemVersions.js'); // Local Dependencies -const {getConfigField, updateUserConfigWin, updateUserConfigMac, readConfigFileSync} = require('./config.js'); +const { + getConfigField, + getGlobalConfigField, + readConfigFileSync, + updateUserConfigOnLaunch, + getUserConfigField +} = require('./config.js'); const {setCheckboxValues} = require('./menus/menuTemplate.js'); const { isMac, isDevEnv } = require('./utils/misc.js'); const protocolHandler = require('./protocolHandler'); @@ -22,10 +30,6 @@ const { deleteIndexFolder } = require('./search/search.js'); require('electron-dl')(); -// ELECTRON-261: On Windows, due to gpu issues, we need to disable gpu -// to ensure screen sharing works effectively with multiple monitors -// https://github.com/electron/electron/issues/4380 - //setting the env path child_process issue https://github.com/electron/electron/issues/7688 shellPath() .then((path) => { @@ -155,7 +159,10 @@ setChromeFlags(); * initialization and is ready to create browser windows. * Some APIs can only be used after this event occurs. */ -app.on('ready', readConfigThenOpenMainWindow); +app.on('ready', () => { + checkFirstTimeLaunch() + .then(readConfigThenOpenMainWindow); +}); /** * Is triggered when all the windows are closed @@ -219,45 +226,63 @@ function setupThenOpenMainWindow() { processProtocolAction(process.argv); isAppAlreadyOpen = true; - - // allows installer to launch app and set appropriate global / user config params. - let hasInstallFlag = getCmdLineArg(process.argv, '--install', true); - let perUserInstall = getCmdLineArg(process.argv, '--peruser', true); - let customDataArg = getCmdLineArg(process.argv, '--userDataPath=', false); - - if (customDataArg && customDataArg.split('=').length > 1) { - let customDataFolder = customDataArg.split('=')[1]; - app.setPath('userData', customDataFolder); - } - if (!isMac && hasInstallFlag) { - getConfigField('launchOnStartup') - .then(setStartup) - .then(() => updateUserConfigWin(perUserInstall)) - .then(app.quit) - .catch(app.quit); - return; - } - - // allows mac installer to overwrite user config - if (isMac && hasInstallFlag) { - // This value is being sent from post install script - // as the app is launched as a root user we don't get - // access to the config file - let launchOnStartup = process.argv[3]; - // We wire this in via the post install script - // to get the config file path where the app is installed - setStartup(launchOnStartup) - .then(updateUserConfigMac) - .then(app.quit) - .catch(app.quit); - return; - } - getUrlAndCreateMainWindow(); - + // Event that fixes the remote desktop issue in Windows // by repositioning the browser window electron.screen.on('display-removed', windowMgr.verifyDisplays); + +} + +function checkFirstTimeLaunch() { + + return new Promise((resolve) => { + + getUserConfigField('version') + .then((configVersion) => { + + const appVersionString = app.getVersion().toString(); + + const execPath = nodePath.dirname(app.getPath('exe')); + const shouldUpdateUserConfig = execPath.indexOf('AppData/Local/Programs') !== -1 || isMac; + + if (!(configVersion + && typeof configVersion === 'string' + && (compareSemVersions.check(appVersionString, configVersion) !== 1)) && shouldUpdateUserConfig) { + return setupFirstTimeLaunch(); + } + + return resolve(); + }) + .catch(() => { + return setupFirstTimeLaunch(); + }); + return resolve(); + }); + +} + +/** + * Setup and update user config + * on first time launch or if the latest app version + * + * @return {Promise} + */ +function setupFirstTimeLaunch() { + return new Promise(resolve => { + log.send(logLevels.INFO, 'setting first time launch config'); + getGlobalConfigField('launchOnStartup') + .then(setStartup) + .then(updateUserConfigOnLaunch) + .then(() => { + log.send(logLevels.INFO, 'first time launch config changes succeeded -> '); + return resolve(); + }) + .catch((err) => { + log.send(logLevels.ERROR, 'first time launch config changes failed -> ' + err); + return resolve(); + }); + }); } /** diff --git a/js/utils/compareSemVersions.js b/js/utils/compareSemVersions.js new file mode 100644 index 00000000..74a20e48 --- /dev/null +++ b/js/utils/compareSemVersions.js @@ -0,0 +1,86 @@ +// regex match the semver (semantic version) this checks for the pattern X.Y.Z +// ex-valid v1.2.0, 1.2.0, 2.3.4-r51 +const semver = /^v?(?:\d+)(\.(?:[x*]|\d+)(\.(?:[x*]|\d+)(?:-[\da-z-]+(?:\.[\da-z-]+)*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?)?)?$/i; +const patch = /-([0-9A-Za-z-.]+)/; + +/** + * This function splits the versions + * into major, minor and patch + * @param v + * @returns {T[]} + */ +function split(v) { + const temp = v.replace(/^v/, '').split('.'); + const arr = temp.splice(0, 2); + arr.push(temp.join('.')); + return arr; +} + +function tryParse(v) { + return isNaN(Number(v)) ? v : Number(v); +} + +/** + * This validates the version + * with the semver regex and returns + * -1 if not valid else 1 + * @param version + * @returns {number} + */ +function validate(version) { + if (typeof version !== 'string') { + return -1; + } + if (!semver.test(version)) { + return -1; + } + return 1; +} + +/** + * This function compares the v1 version + * with the v2 version for all major, minor, patch + * if v1 > v2 returns 1 + * if v1 < v2 returns -1 + * if v1 = v2 returns 0 + * @param v1 + * @param v2 + * @returns {number} + */ +function check(v1, v2) { + if (validate(v1) === -1 || validate(v2) === -1) { + return -1; + } + + const s1 = split(v1); + const s2 = split(v2); + + for (let i = 0; i < 3; i++) { + const n1 = parseInt(s1[i] || '0', 10); + const n2 = parseInt(s2[i] || '0', 10); + + if (n1 > n2) return 1; + if (n2 > n1) return -1; + } + + if ([ s1[2], s2[2] ].every(patch.test.bind(patch))) { + const p1 = patch.exec(s1[2])[1].split('.').map(tryParse); + const p2 = patch.exec(s2[2])[1].split('.').map(tryParse); + + for (let k = 0; k < Math.max(p1.length, p2.length); k++) { + if (p1[k] === undefined || typeof p2[k] === 'string' && typeof p1[k] === 'number') return -1; + if (p2[k] === undefined || typeof p1[k] === 'string' && typeof p2[k] === 'number') return 1; + + if (p1[k] > p2[k]) return 1; + if (p2[k] > p1[k]) return -1; + } + } else if ([ s1[2], s2[2] ].some(patch.test.bind(patch))) { + return patch.test(s1[2]) ? -1 : 1; + } + + return 0; +} + +module.exports = { + check +}; diff --git a/package.json b/package.json index 2b8a95bc..a7ef778c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "library/search-launch-agent.sh", "library/search-launch-daemon.sh" ], - "appId": "symphony-electron-desktop", + "appId": "com.symphony.electron-desktop", "mac": { "target": "dmg", "category": "public.app-category.business" @@ -106,7 +106,6 @@ }, "dependencies": { "@paulcbetts/system-idle-time": "1.0.4", - "appdirectory": "0.1.0", "archiver": "2.1.1", "async.map": "0.5.2", "async.mapseries": "0.5.2",