From c94d4efd180e69b7a6ea5b94c6dafe7efb387510 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Mon, 30 Oct 2017 13:57:13 -0700 Subject: [PATCH] Beta versions support: SxS support, in-app env/instance display (#1606) * Script for beta config; unique data dir, in-app env/type display To release a beta build, increment the version and add -beta-N to the end, then go through all the standard release activities. The prepare-build npm script then updates key bits of the package.json to ensure that the beta build can be installed alongside a production build. This includes a new name ('Signal Beta') and a different location for application data. Note: Beta builds can be installed alongside production builds. As part of this, a couple new bits of data are shown across the app: - Environment (development or test, not shown if production) - App Instance (disabled in production; used for multiple accounts) These are shown in: - The window title - both environment and app instance. You can tell beta builds because the app name, preceding these data bits, is different. - The about window - both environment and app instance. You can tell beta builds from the version number. - The header added to the debug log - just environment. The version number will tell us if it's a beta build, and app instance isn't helpful. * Turn on single-window mode in non-production modes Because it's really frightening when you see 'unable to read from db' errors in the console. * aply.sh: More instructions for initial setup and testing * Gruntfile: Get consistent with use of package.json datas * Linux: manually update desktop keys, since macros not available --- .travis.yml | 1 + Gruntfile.js | 18 +++++----- about.html | 14 ++++++++ app/config.js | 9 ++--- app/user_config.js | 4 ++- appveyor.yml | 1 + aptly.sh | 30 +++++++++++++--- js/background.js | 9 +++++ js/conversation_controller.js | 4 +-- js/logging.js | 7 +++- main.js | 23 ++++++++----- package.json | 7 ++-- prepare_build.js | 65 +++++++++++++++++++++++++++++++++++ 13 files changed, 158 insertions(+), 34 deletions(-) create mode 100644 prepare_build.js diff --git a/.travis.yml b/.travis.yml index 83662df008b2..acee1fe243a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ install: - yarn install script: - yarn run generate + - yarn prepare-build - ./node_modules/.bin/build --em.environment=$SIGNAL_ENV --config.mac.bundleVersion='$TRAVIS_BUILD_NUMBER' --publish=never - ./travis.sh env: diff --git a/Gruntfile.js b/Gruntfile.js index 5ec898f3eae5..bdab6a77f0cf 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,5 @@ var path = require('path'); +var packageJson = require('./package.json'); module.exports = function(grunt) { 'use strict'; @@ -204,23 +205,23 @@ module.exports = function(grunt) { }, 'test-release': { osx: { - archive: 'mac/Signal.app/Contents/Resources/app.asar', - appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml', - exe: 'mac/Signal.app/Contents/MacOS/Signal' + archive: 'mac/' + packageJson.productName + '.app/Contents/Resources/app.asar', + appUpdateYML: 'mac/' + packageJson.productName + '.app/Contents/Resources/app-update.yml', + exe: 'mac/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName }, mas: { archive: 'mas/Signal.app/Contents/Resources/app.asar', appUpdateYML: 'mac/Signal.app/Contents/Resources/app-update.yml', - exe: 'mas/Signal.app/Contents/MacOS/Signal' + exe: 'mas/' + packageJson.productName + '.app/Contents/MacOS/' + packageJson.productName }, linux: { archive: 'linux-unpacked/resources/app.asar', - exe: 'linux-unpacked/signal-desktop' + exe: 'linux-unpacked/' + packageJson.name }, win: { archive: 'win-unpacked/resources/app.asar', appUpdateYML: 'win-unpacked/resources/app-update.yml', - exe: 'win-unpacked/Signal.exe' + exe: 'win-unpacked/' + packageJson.productName + '.exe' } }, gitinfo: {} // to be populated by grunt gitinfo @@ -273,7 +274,6 @@ module.exports = function(grunt) { require('mkdirp').sync('release'); var fs = require('fs'); var done = this.async(); - var package_json = grunt.config.get('pkg'); var gitinfo = grunt.config.get('gitinfo'); var https = require('https'); @@ -281,7 +281,7 @@ module.exports = function(grunt) { var keyBase = 'WhisperSystems/Signal-Desktop'; var sha = gitinfo.local.branch.current.SHA; var files = [{ - zip: 'signal-desktop-' + package_json.version + '.zip', + zip: packageJson.name + '-' + packageJson.version + '.zip', extractedTo: 'linux' }]; @@ -451,7 +451,7 @@ module.exports = function(grunt) { return app.client.getTitle(); }).then(function (title) { // Verify the window's title - assert.equal(title, 'Signal'); + assert.equal(title, packageJson.productName); console.log('title ok'); }).then(function () { assert(app.chromeDriver.logLines.indexOf('NODE_ENV ' + environment) > -1); diff --git a/about.html b/about.html index 93eba7e256c5..338527a2c6fd 100644 --- a/about.html +++ b/about.html @@ -30,6 +30,20 @@ a { document.write('v', window.config.version); +
+ +
signal.org
diff --git a/app/config.js b/app/config.js index 4939cf05baab..047d9c15e0d5 100644 --- a/app/config.js +++ b/app/config.js @@ -1,12 +1,9 @@ -const fs = require('fs'); const path = require('path'); -console.log('reading package.json'); -const jsonFile = fs.readFileSync(path.join(__dirname, '..', 'package.json')); -const package_json = JSON.parse(jsonFile, 'utf-8'); -const environment = package_json.environment || process.env.NODE_ENV || 'development'; +const packageJson = require('../package.json'); -console.log('configuring'); + +const environment = packageJson.environment || process.env.NODE_ENV || 'development'; // Set environment vars to configure node-config before requiring it process.env.NODE_ENV = environment; diff --git a/app/user_config.js b/app/user_config.js index 164ffe24e04b..4f77368b5137 100644 --- a/app/user_config.js +++ b/app/user_config.js @@ -1,9 +1,11 @@ -const app = require('electron').app; const path = require('path'); + +const app = require('electron').app; const ElectronConfig = require('electron-config'); const config = require('./config'); + // use a separate data directory for development if (config.has('storageProfile')) { const userData = path.join( diff --git a/appveyor.yml b/appveyor.yml index 249dc1979f5d..6711829856d8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,6 +16,7 @@ build_script: - node build\grunt.js - type package.json | findstr /v certificateSubjectName > temp.json - move temp.json package.json + - yarn prepare-build - node_modules\.bin\build --em.environment=%SIGNAL_ENV% --publish=never test_script: diff --git a/aptly.sh b/aptly.sh index 3646bc189b97..1f7c08e3aaaa 100755 --- a/aptly.sh +++ b/aptly.sh @@ -1,18 +1,40 @@ #!/bin/bash -# Setup: +# Setup - creates the local repo which will be mirrored up to S3, then back-fill it. Your +# future deploys will eliminate all old versions without these backfill steps: # aptly repo create signal-desktop +# aptly mirror create -ignore-signatures backfill-mirror https://updates.signal.org/desktop/apt xenial +# aptly mirror update -ignore-signatures backfill-mirror +# aptly repo import backfill-mirror signal-desktop signal-desktop signal-desktop-beta +# aptly repo show -with-packages signal-desktop +# +# First run on a machine - uncomment the first two 'aptly publish snapshot' commands, +# comment the other two. Sets up the two publish channels, one local, one to S3. +# +# Testing - comment out the lines with s3:$ENDPOINT to publish only locally. To eliminate +# effects of testing, remove package from repo, then move back to old snapshot: +# aptly repo remove signal-desktop signal-desktop_1.0.35_amd64 +# aptly publish switch -gpg-key=57F6FB06 xenial signal-desktop_v1.0.34 # # Release: -# VERSION=X.X.X ./aptly.sh +# NAME=signal-desktop(-beta) VERSION=X.X.X ./aptly.sh + +echo "Releasing $NAME build version $VERSION" REPO=signal-desktop DISTRO=xenial ENDPOINT=signal-desktop-apt # Matches endpoint name in .aptly.conf -DEB_PATH=release SNAPSHOT=signal-desktop_v$VERSION GPG_KEYID=57F6FB06 -aptly repo add $REPO $DEB_PATH/$REPO\_$VERSION\_*.deb +aptly repo add $REPO release/$NAME\_$VERSION\_*.deb aptly snapshot create $SNAPSHOT from repo $REPO + +# run these two only on first release to a given repo from a given machine +# https://www.aptly.info/doc/aptly/publish/snapshot/ +# aptly publish snapshot -gpg-key=$GPG_KEYID $SNAPSHOT +# aptly publish snapshot -gpg-key=$GPG_KEYID -config=.aptly.conf $SNAPSHOT s3:$ENDPOINT: + +# these update already-published repos, run every time after that +# https://www.aptly.info/doc/aptly/publish/switch/ aptly publish switch -gpg-key=$GPG_KEYID $DISTRO $SNAPSHOT aptly publish switch -gpg-key=$GPG_KEYID -config=.aptly.conf $DISTRO s3:$ENDPOINT: $SNAPSHOT diff --git a/js/background.js b/js/background.js index bfcb05b31086..443df5d26763 100644 --- a/js/background.js +++ b/js/background.js @@ -14,6 +14,15 @@ var initialLoadComplete = false; window.owsDesktopApp = {}; + var title = window.config.name; + if (window.config.environment !== 'production') { + title += ' - ' + window.config.environment; + } + if (window.config.appInstance) { + title += ' - ' + window.config.appInstance; + } + window.config.title = window.document.title = title; + // start a background worker for ecc textsecure.startWorker('js/libsignal-protocol-worker.js'); Whisper.KeyChangeListener.init(textsecure.storage.protocol); diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 1d6b522d27fe..9fe2c67bb652 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -64,10 +64,10 @@ if (newUnreadCount > 0) { window.setBadgeCount(newUnreadCount); - window.document.title = "Signal (" + newUnreadCount + ")"; + window.document.title = window.config.title + " (" + newUnreadCount + ")"; } else { window.setBadgeCount(0); - window.document.title = "Signal"; + window.document.title = window.config.title; } }, startPruning: function() { diff --git a/js/logging.js b/js/logging.js index f7a687fe0a4d..b2d864f5bd41 100644 --- a/js/logging.js +++ b/js/logging.js @@ -69,7 +69,12 @@ if (window.console) { // The mechanics of preparing a log for publish function getHeader() { - return window.navigator.userAgent + ' node/' + window.config.node_version; + let header = window.navigator.userAgent; + + header += ' node/' + window.config.node_version; + header += ' env/' + window.config.environment; + + return header; } function getLevel(level) { diff --git a/main.js b/main.js index 36f7d2f42646..130b0de3f283 100644 --- a/main.js +++ b/main.js @@ -3,7 +3,7 @@ const url = require('url'); const os = require('os'); const _ = require('lodash'); -const electron = require('electron') +const electron = require('electron'); const BrowserWindow = electron.BrowserWindow; const app = electron.app; @@ -11,20 +11,26 @@ const ipc = electron.ipcMain; const Menu = electron.Menu; const shell = electron.shell; +const packageJson = require('./package.json'); const autoUpdate = require('./app/auto_update'); const windowState = require('./app/window_state'); -console.log('setting AUMID'); -app.setAppUserModelId('org.whispersystems.signal-desktop') +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 -// be closed automatically when the JavaScript object is garbage collected. +// be closed automatically when the JavaScript object is garbage collected. let mainWindow; const config = require("./app/config"); -if (config.environment === 'production' && !process.mas) { +// Very important to put before the single instance check, since it is based on the +// userData directory. +const userConfig = require('./app/user_config'); + +if (!process.mas) { console.log('making app single instance'); var shouldQuit = app.makeSingleInstance(function(commandLine, workingDirectory) { // Someone tried to run a second instance, we should focus our window @@ -36,16 +42,15 @@ if (config.environment === 'production' && !process.mas) { }); if (shouldQuit) { - console.log('quitting'); + console.log('quitting; we are the second instance'); app.quit(); return; } } -const userConfig = require('./app/user_config'); const logging = require('./app/logging'); -// this must be after we set up appPath in user_config.js +// This must be after we set up appPath in user_config.js, so we know where logs go logging.initialize(); const logger = logging.getLogger(); @@ -60,6 +65,7 @@ function prepareURL(pathSegments) { protocol: 'file:', slashes: true, query: { + name: packageJson.productName, locale: locale.name, version: app.getVersion(), buildExpiration: config.get('buildExpiration'), @@ -69,6 +75,7 @@ function prepareURL(pathSegments) { environment: config.environment, node_version: process.versions.node, hostname: os.hostname(), + appInstance: process.env.NODE_APP_INSTANCE, } }) } diff --git a/package.json b/package.json index d082d5b24871..4d6d825f6868 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "build": "build --em.environment=$SIGNAL_ENV", "dist": "npm run generate && npm run build", "pack": "npm run generate && npm run build -- --dir", + "prepare-build": "node prepare_build.js", "pack-prod": "SIGNAL_ENV=production npm run pack", "dist-prod": "SIGNAL_ENV=production npm run dist", "dist-prod-all": "SIGNAL_ENV=production npm run dist -- -mwl", @@ -51,13 +52,13 @@ "prep-release": "npm run generate && grunt prep-release && npm run build-release && npm run build-mas-release && grunt test-release", "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 && VERSION=$npm_package_version ./aptly.sh", + "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" }, "build": { "appId": "org.whispersystems.signal-desktop", "mac": { - "artifactName": "${productName}-mac-${version}.${ext}", + "artifactName": "${name}-mac-${version}.${ext}", "category": "public.app-category.social-networking", "icon": "build/icons/mac/icon.icns", "publish": [ @@ -81,7 +82,7 @@ }, "win": { "asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries", - "artifactName": "${productName}-win-${version}.${ext}", + "artifactName": "${name}-win-${version}.${ext}", "certificateSubjectName": "Signal", "publisherName": "Signal (Quiet Riddle Ventures, LLC)", "icon": "build/icons/win/icon.ico", diff --git a/prepare_build.js b/prepare_build.js new file mode 100644 index 000000000000..06d894f658e6 --- /dev/null +++ b/prepare_build.js @@ -0,0 +1,65 @@ +const fs = require('fs'); +const _ = require('lodash'); + +const packageJson = require('./package.json'); +const version = packageJson.version; +const beta = /beta/; + +// You might be wondering why this file is necessary. It comes down to our desire to allow +// side-by-side installation of production and beta builds. Electron-Builder uses +// top-level data from package.json for many things, like the executable name, the +// debian package name, the install directory under /opt on linux, etc. We tried +// adding the ${channel} macro to these values, but Electron-Builder didn't like that. + +if (!beta.test(version)) { + return; +} + +console.log('prepare_build: updating package.json for beta build'); + +// ------- + +const NAME_PATH = 'name'; +const PRODUCTION_NAME = 'signal-desktop'; +const BETA_NAME = 'signal-desktop-beta'; + +const PRODUCT_NAME_PATH = 'productName'; +const PRODUCTION_PRODUCT_NAME = 'Signal'; +const BETA_PRODUCT_NAME = 'Signal Beta'; + +const APP_ID_PATH = 'build.appId'; +const PRODUCTION_APP_ID = 'org.whispersystems.signal-desktop'; +const BETA_APP_ID = 'org.whispersystems.signal-desktop-beta'; + +const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass'; +const PRODUCTION_STARTUP_WM_CLASS = 'Signal'; +const BETA_STARTUP_WM_CLASS = 'Signal Beta'; + + + +// ------- + +function checkValue(object, objectPath, expected) { + const actual = _.get(object, objectPath) + if (actual !== expected) { + throw new Error(objectPath + ' was ' + actual + '; expected ' + expected); + } +} + +// ------ + +checkValue(packageJson, NAME_PATH, PRODUCTION_NAME); +checkValue(packageJson, PRODUCT_NAME_PATH, PRODUCTION_PRODUCT_NAME); +checkValue(packageJson, APP_ID_PATH, PRODUCTION_APP_ID); +checkValue(packageJson, STARTUP_WM_CLASS_PATH, PRODUCTION_STARTUP_WM_CLASS); + +// ------- + +_.set(packageJson, NAME_PATH, BETA_NAME); +_.set(packageJson, PRODUCT_NAME_PATH, BETA_PRODUCT_NAME); +_.set(packageJson, APP_ID_PATH, BETA_APP_ID); +_.set(packageJson, STARTUP_WM_CLASS_PATH, BETA_STARTUP_WM_CLASS); + +// ------- + +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' '));