etcher/lib/gui/app.js
Juan Cruz Viotti d3b35742a6 refactor(GUI): integrate etcher-latest-version into the main repo (#1183)
`etcher-latest-version` was kept in a separate repository in order to
re-use it with the Etcher website, however the Etcher website is not
using it at all, and we're moving towards having the website in the main
repository.

Therefore, this commit brings back the logic from
`etcher-latest-version`, but introduces it as
`lib/shared/s3-packages.js`, in order to not tie ourselves to the
AngularJS framework, and as a step towards the Etcher SDK.

As a nice little bonus, this commit adds support for an
`ETCHER_FAKE_S3_LATEST_VERSION` environment variable that can be used to
trick Etcher that there is an available update, and therefore show the
update notifier modal.

Also, this commit adds support for snapshot builds update-checks, by
checking the `resin-nightly-downloads` S3 bucket if the current version
contains a git commit hash build number.

If the version is not a production release, then the update notifier
modal doesn't present the checkbox to disable update notifications for X
days.

We also add a property called `updates.semverRange` to `package.json`,
which can be used to fine control which versions are considered as
candidates for an update notification.

This commit adds a setting called `includeUnstableChannel`, which can be
used to tweak whether unstable (beta) releases are considered or not
when checking for the latest available version.

See: https://github.com/resin-io-modules/etcher-latest-version
Fixes: https://github.com/resin-io/etcher/issues/953
Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
2017-04-26 23:52:04 -04:00

299 lines
8.7 KiB
JavaScript

/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @module Etcher
*/
'use strict';
/* eslint-disable no-var */
var angular = require('angular');
/* eslint-enable no-var */
const electron = require('electron');
const Bluebird = require('bluebird');
const semver = require('semver');
const _ = require('lodash');
const EXIT_CODES = require('../shared/exit-codes');
const messages = require('../shared/messages');
const s3Packages = require('../shared/s3-packages');
const release = require('../shared/release');
const packageJSON = require('../../package.json');
const flashState = require('./models/flash-state');
const settings = require('./models/settings');
const windowProgress = require('./os/window-progress');
const Store = require('./models/store');
const app = angular.module('Etcher', [
require('angular-ui-router'),
require('angular-ui-bootstrap'),
require('angular-if-state'),
// Etcher modules
require('./modules/analytics'),
require('./modules/error'),
require('./modules/drive-scanner'),
// Models
require('./models/selection-state'),
require('./models/drives'),
// Components
require('./components/svg-icon/svg-icon'),
require('./components/update-notifier/update-notifier'),
require('./components/warning-modal/warning-modal'),
// Pages
require('./pages/main/main'),
require('./pages/finish/finish'),
require('./pages/settings/settings'),
// OS
require('./os/open-external/open-external'),
require('./os/dropzone/dropzone'),
require('./os/dialog/dialog'),
// Utils
require('./utils/manifest-bind/manifest-bind')
]);
app.run(() => {
console.log([
' _____ _ _',
'| ___| | | |',
'| |__ | |_ ___| |__ ___ _ __',
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
'| |___| || (__| | | | __/ |',
'\\____/ \\__\\___|_| |_|\\___|_|',
'',
'Interested in joining the Etcher team?',
'Drop us a line at join+etcher@resin.io'
].join('\n'));
});
app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => {
AnalyticsService.logEvent('Application start');
const currentVersion = packageJSON.version;
const currentReleaseType = release.getReleaseType(currentVersion);
const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates({
ignoreSleepUpdateCheck: currentReleaseType !== release.RELEASE_TYPE.PRODUCTION
});
if (_.some([
!shouldCheckForUpdates,
process.env.ETCHER_DISABLE_UPDATES,
currentReleaseType === release.RELEASE_TYPE.UNKNOWN
])) {
AnalyticsService.logEvent('Not checking for updates', {
shouldCheckForUpdates,
disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES,
releaseType: currentReleaseType
});
return;
}
const updateSemverRange = packageJSON.updates.semverRange;
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel');
AnalyticsService.logEvent('Checking for updates', {
currentVersion,
releaseType: currentReleaseType,
updateSemverRange,
includeUnstableChannel
});
s3Packages.getLatestVersion(currentReleaseType, {
range: updateSemverRange,
includeUnstableChannel
}).then((latestVersion) => {
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
AnalyticsService.logEvent('Update notification skipped', {
reason: 'Latest version'
});
return Bluebird.resolve();
}
// In case the internet connection is not good and checking the
// latest published version takes too long, only show notify
// the user about the new version if he didn't start the flash
// process (e.g: selected an image), otherwise such interruption
// might be annoying.
if (SelectionStateModel.hasImage()) {
AnalyticsService.logEvent('Update notification skipped', {
reason: 'Image selected'
});
return Bluebird.resolve();
}
AnalyticsService.logEvent('Notifying update', {
latestVersion
});
return UpdateNotifierService.notify(latestVersion, {
allowSleepUpdateCheck: currentReleaseType === release.RELEASE_TYPE.PRODUCTION
});
}).catch(ErrorService.reportException);
});
app.run((AnalyticsService) => {
Store.subscribe(() => {
const currentFlashState = flashState.getFlashState();
// There is usually a short time period between the `isFlashing()`
// property being set, and the flashing actually starting, which
// might cause some non-sense flashing state logs including
// `undefined` values.
//
// We use the presence of `.eta` to determine that the actual
// writing started.
if (!flashState.isFlashing() || !currentFlashState.eta) {
return;
}
AnalyticsService.logDebug([
`Progress (${currentFlashState.type}):`,
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s`,
`(eta ${currentFlashState.eta}s)`
].join(' '));
windowProgress.set(currentFlashState.percentage);
});
});
app.run(($timeout, DriveScannerService, DrivesModel, ErrorService) => {
DriveScannerService.on('drives', (drives) => {
// Safely trigger a digest cycle.
// In some cases, AngularJS doesn't acknowledge that the
// available drives list has changed, and incorrectly
// keeps asking the user to "Connect a drive".
$timeout(() => {
DrivesModel.setDrives(drives);
});
});
DriveScannerService.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
// spamming our error reporting service.
DriveScannerService.stop();
return ErrorService.reportException(error);
});
DriveScannerService.start();
});
app.run(($window, AnalyticsService, WarningModalService, ErrorService, OSDialogService) => {
let popupExists = false;
$window.addEventListener('beforeunload', (event) => {
if (!flashState.isFlashing() || popupExists) {
AnalyticsService.logEvent('Close application', {
isFlashing: flashState.isFlashing()
});
return;
}
// Don't close window while flashing
event.returnValue = false;
// Don't open any more popups
popupExists = true;
AnalyticsService.logEvent('Close attempt while flashing');
OSDialogService.showWarning({
confirmationLabel: 'Yes, quit',
rejectionLabel: 'Cancel',
title: 'Are you sure you want to close Etcher?',
description: messages.warning.exitWhileFlashing()
}).then((confirmed) => {
if (confirmed) {
AnalyticsService.logEvent('Close confirmed while flashing', {
uuid: flashState.getFlashUuid()
});
// This circumvents the 'beforeunload' event unlike
// electron.remote.app.quit() which does not.
electron.remote.process.exit(EXIT_CODES.SUCCESS);
}
AnalyticsService.logEvent('Close rejected while flashing');
popupExists = false;
}).catch(ErrorService.reportException);
});
});
app.run(($rootScope, AnalyticsService) => {
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
// Ignore first navigation
if (!fromState.name) {
return;
}
AnalyticsService.logEvent('Navigate', {
to: toState.name,
from: fromState.name
});
});
});
app.config(($urlRouterProvider) => {
$urlRouterProvider.otherwise('/main');
});
app.config(($provide) => {
$provide.decorator('$exceptionHandler', ($delegate, $injector) => {
return (exception, cause) => {
const ErrorService = $injector.get('ErrorService');
ErrorService.reportException(exception);
$delegate(exception, cause);
};
});
});
app.controller('HeaderController', function(SelectionStateModel, OSOpenExternalService) {
/**
* @summary Open help page
* @function
* @public
*
* @description
* This application will open either the image's support url, declared
* in the archive `manifest.json`, or the default Etcher help page.
*
* @example
* HeaderController.openHelpPage();
*/
this.openHelpPage = () => {
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md';
const supportUrl = SelectionStateModel.getImageSupportUrl() || DEFAULT_SUPPORT_URL;
OSOpenExternalService.open(supportUrl);
};
});