Merge branch 'real-upgrades'

* real-upgrades:
  minor
  some progress, but needs more work
  Real update system
This commit is contained in:
Alex Grintsvayg 2017-03-17 18:23:41 -04:00
commit 0d5ef61944
6 changed files with 193 additions and 30 deletions

View file

@ -1,4 +1,4 @@
const {app, BrowserWindow, ipcMain} = require('electron'); const {app, BrowserWindow, ipcMain, shell} = require('electron');
const path = require('path'); const path = require('path');
const jayson = require('jayson'); const jayson = require('jayson');
// tree-kill has better cross-platform handling of // tree-kill has better cross-platform handling of
@ -27,7 +27,7 @@ function createWindow () {
}; };
function lauchDaemon() { function launchDaemon() {
if (subpy) { if (subpy) {
return; return;
} }
@ -78,7 +78,7 @@ function launchDaemonIfNotRunning() {
function (err, res) { function (err, res) {
if (err) { if (err) {
console.log('lbrynet daemon needs to be launched') console.log('lbrynet daemon needs to be launched')
lauchDaemon(); launchDaemon();
} else { } else {
console.log('lbrynet daemon is already running') console.log('lbrynet daemon is already running')
} }
@ -115,23 +115,49 @@ app.on('activate', () => {
}) })
function shutdownDaemon() { function shutdownDaemon(evenIfNotStartedByApp = false) {
console.log('Shutdown triggered'); if (subpy) {
if (subpy == null) { console.log('Killing lbrynet-daemon process');
// TODO: In this case, we didn't start the process so I'm hesitant kill(subpy.pid, undefined, (err) => {
// to shut it down. We might want to send a stop command console.log('Killed lbrynet-daemon process');
// though instead of just letting it run. });
console.log('Not killing lbrynet daemon because we did not start it') } else if (evenIfNotStartedByApp) {
return console.log('Killing lbrynet-daemon, even though app did not start it');
client.request('daemon_stop', []);
// TODO: If the daemon errors or times out when we make this request, find
// the process and force quit it.
} else {
console.log('Not killing lbrynet-daemon because app did not start it')
} }
// Is it safe to start the installer before the daemon finishes running?
// If not, we should wait until the daemon is closed before we start the install.
}
function shutdown() {
if (win) { if (win) {
win.loadURL(`file://${__dirname}/dist/quit.html`); win.loadURL(`file://${__dirname}/dist/quit.html`);
} }
quitting = true; quitting = true;
console.log('Killing lbrynet-daemon process'); shutdownDaemon();
kill(subpy.pid, undefined, (err) => {
console.log('Killed lbrynet-daemon process');
});
} }
ipcMain.on('shutdown', shutdownDaemon); function upgrade(event, installerPath) {
app.on('quit', () => {
shell.openItem(installerPath);
});
if (win) {
win.loadURL(`file://${__dirname}/dist/upgrade.html`);
}
quitting = true;
shutdownDaemon(true);
// wait for daemon to shut down before upgrading
// what to do if no shutdown in a long time?
console.log('Update downloaded to ', installerPath);
console.log('The app will close, and you will be prompted to install the latest version of LBRY.');
console.log('After the install is complete, please reopen the app.');
}
ipcMain.on('upgrade', upgrade);
ipcMain.on('shutdown', shutdown);

View file

@ -13,7 +13,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes * Wherever possible, use outpoints for unique IDs instead of names or SD hashes
### Changed ### Changed
* * Update process now easier and more reliable
* *
* *

37
ui/dist/upgrade.html vendored Normal file
View file

@ -0,0 +1,37 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>LBRY</title>
<link href='https://fonts.googleapis.com/css?family=Raleway:600,300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600italic,600' rel='stylesheet' type='text/css'>
<link href="./css/all.css" rel="stylesheet" type="text/css" media="screen,print" />
<link href="./js/mediaelement/mediaelementplayer.css" rel="stylesheet" type="text/css" />
<link rel="icon" type="image/png" href="./img/fav/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="./img/fav/favicon-194x194.png" sizes="194x194">
<link rel="icon" type="image/png" href="./img/fav/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="./img/fav/android-chrome-192x192.png" sizes="192x192">
<link rel="icon" type="image/png" href="./img/fav/favicon-16x16.png" sizes="16x16">
<meta name="msapplication-TileColor" content="#155B4A">
<meta name="msapplication-TileImage" content="/img/fav/mstile-144x144.png">
<meta name="theme-color" content="#155B4A">
<style>
body {
background-color: "#155b4a"
}
</style>
</head>
<div id="canvas">
<div class="load-screen" style="color: white; min-height: 100vh; min-width: 100vw; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<img src="./img/lbry-white-485x160.png" alt="LBRY">
<div style="margin-top: 24px; width: 325px; text-align: center;">
<h3>
<span>Starting LBRY Upgrade <span class="busy-indicator"></span>
</span>
</h3>
</div>
</div>
</div>
</html>

View file

@ -24,6 +24,9 @@ import {Link} from './component/link.js';
const {remote, ipcRenderer, shell} = require('electron'); const {remote, ipcRenderer, shell} = require('electron');
const {download} = remote.require('electron-dl'); const {download} = remote.require('electron-dl');
const os = require('os');
const path = require('path');
const app = require('electron').remote.app;
var App = React.createClass({ var App = React.createClass({
@ -36,6 +39,19 @@ var App = React.createClass({
data: 'Error data', data: 'Error data',
}, },
_upgradeDownloadItem: null,
_version: null,
// Temporary workaround since electron-dl throws errors when you try to get the filename
getUpgradeFilename: function() {
if (os.platform() == 'darwin') {
return `LBRY-${this._version}.dmg`;
} else if (os.platform() == 'linux') {
return `LBRY-${this._version}.deb`;
} else {
return `LBRY-${this._version}_amd64.deb`;
}
},
getInitialState: function() { getInitialState: function() {
// For now, routes are in format ?page or ?page=args // For now, routes are in format ?page or ?page=args
var match, param, val, viewingPage, var match, param, val, viewingPage,
@ -53,6 +69,7 @@ var App = React.createClass({
updateUrl: null, updateUrl: null,
isOldOSX: null, isOldOSX: null,
downloadProgress: null, downloadProgress: null,
downloadComplete: false,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
@ -79,6 +96,8 @@ var App = React.createClass({
} }
lbry.getVersionInfo((versionInfo) => { lbry.getVersionInfo((versionInfo) => {
this._version = versionInfo.lbrynet_version; // temp for building upgrade filename
var isOldOSX = false; var isOldOSX = false;
if (versionInfo.os_system == 'Darwin') { if (versionInfo.os_system == 'Darwin') {
var updateUrl = 'https://lbry.io/get/lbry.dmg'; var updateUrl = 'https://lbry.io/get/lbry.dmg';
@ -91,10 +110,7 @@ var App = React.createClass({
} else if (versionInfo.os_system == 'Linux') { } else if (versionInfo.os_system == 'Linux') {
var updateUrl = 'https://lbry.io/get/lbry.deb'; var updateUrl = 'https://lbry.io/get/lbry.deb';
} else if (versionInfo.os_system == 'Windows') { } else if (versionInfo.os_system == 'Windows') {
// A little weird, but for electron, the installer is var updateUrl = 'https://lbry.io/get/lbry.exe';
// actually an exe. Maybe a better url would
// be something like /get/windows ?
var updateUrl = 'https://lbry.io/get/lbry.msi';
} else { } else {
var updateUrl = 'https://lbry.io/get'; var updateUrl = 'https://lbry.io/get';
} }
@ -123,17 +139,51 @@ var App = React.createClass({
handleUpgradeClicked: function() { handleUpgradeClicked: function() {
// TODO: create a callback for onProgress and have the UI // TODO: create a callback for onProgress and have the UI
// show download progress // show download progress
// TODO: remove the saveAs popup. Thats just me being lazy and having
// some indication that the download is happening
// TODO: calling lbry.stop() ends up displaying the "daemon // TODO: calling lbry.stop() ends up displaying the "daemon
// unexpectedly stopped" page. Have a better way of shutting down // unexpectedly stopped" page. Have a better way of shutting down
let dir = app.getPath('temp');
let options = { let options = {
onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}), onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
} directory: dir,
};
download(remote.getCurrentWindow(), this.state.updateUrl, options) download(remote.getCurrentWindow(), this.state.updateUrl, options)
.then(dl => ipcRenderer.send('shutdown')); .then(downloadItem => {
/**
* TODO: get the download path directly from the download object. It should just be
* downloadItem.getSavePath(), but the copy on the main process is being garbage collected
* too soon.
*/
this._upgradeDownloadItem = downloadItem;
this._upgradeDownloadPath = path.join(dir, this.getUpgradeFilename());
this.setState({
downloadComplete: true
});
});
this.setState({modal: 'downloading'}); this.setState({modal: 'downloading'});
}, },
handleStartUpgradeClicked: function() {
ipcRenderer.send('upgrade', this._upgradeDownloadPath);
},
cancelUpgrade: function() {
if (this._upgradeDownloadItem) {
/*
* Right now the remote reference to the download item gets garbage collected as soon as the
* the download is over (maybe even earlier), so trying to cancel a finished download may
* throw an error.
*/
try {
this._upgradeDownloadItem.cancel();
} catch (err) {
// Do nothing
}
}
this.setState({
downloadProgress: null,
downloadComplete: false,
modal: null,
});
},
handleSkipClicked: function() { handleSkipClicked: function() {
sessionStorage.setItem('upgradeSkipped', true); sessionStorage.setItem('upgradeSkipped', true);
this.setState({ this.setState({
@ -246,8 +296,22 @@ var App = React.createClass({
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom"> <Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom">
Downloading Update: {this.state.downloadProgress}% Complete Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
<Line percent={this.state.downloadProgress} strokeWidth="4"/> <Line percent={this.state.downloadProgress} strokeWidth="4"/>
{this.state.downloadComplete ? (
<div>
<br />
<p>Click "Begin Upgrade" to start the upgrade process.</p>
<p>The app will close, and you will be prompted to install the latest version of LBRY.</p>
<p>After the install is complete, please reopen the app.</p>
</div>
) : null }
<div className="modal__buttons">
{this.state.downloadComplete
? <Link button="primary" label="Begin Upgrade" className="modal__button" onClick={this.handleStartUpgradeClicked} />
: null}
<Link button="alt" label="Cancel" className="modal__button" onClick={this.cancelUpgrade} />
</div>
</Modal> </Modal>
<ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal" <ExpandableModal isOpen={this.state.modal == 'error'} contentLabel="Error" className="error-modal"
overlayClassName="error-modal-overlay" onConfirmed={this.closeModal} overlayClassName="error-modal-overlay" onConfirmed={this.closeModal}

View file

@ -33,10 +33,10 @@ export const Modal = React.createClass({
{this.props.type == 'custom' // custom modals define their own buttons {this.props.type == 'custom' // custom modals define their own buttons
? null ? null
: <div className="modal__buttons"> : <div className="modal__buttons">
{this.props.type == 'confirm' <Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" disabled={this.props.confirmButtonDisabled} onClick={this.props.onConfirmed} />
? <Link button="alt" label={this.props.abortButtonLabel} className="modal__button" disabled={this.props.abortButtonDisabled} onClick={this.props.onAborted} /> {this.props.type == 'confirm'
: null} ? <Link button="alt" label={this.props.abortButtonLabel} className="modal__button" disabled={this.props.abortButtonDisabled} onClick={this.props.onAborted} />
<Link button="primary" label={this.props.confirmButtonLabel} className="modal__button" disabled={this.props.confirmButtonDisabled} onClick={this.props.onConfirmed} /> : null}
</div>} </div>}
</ReactModal> </ReactModal>
); );

View file

@ -1,6 +1,10 @@
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import React from 'react'; import React from 'react';
import FormField from '../component/form.js'; import FormField from '../component/form.js';
import {Link} from '../component/link.js';
const fs = require('fs');
const {ipcRenderer} = require('electron');
const DeveloperPage = React.createClass({ const DeveloperPage = React.createClass({
getInitialState: function() { getInitialState: function() {
@ -8,6 +12,7 @@ const DeveloperPage = React.createClass({
showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'), showDeveloperMenu: lbry.getClientSetting('showDeveloperMenu'),
useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'), useCustomLighthouseServers: lbry.getClientSetting('useCustomLighthouseServers'),
customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'), customLighthouseServers: lbry.getClientSetting('customLighthouseServers').join('\n'),
upgradePath: '',
}; };
}, },
handleShowDeveloperMenuChange: function(event) { handleShowDeveloperMenuChange: function(event) {
@ -23,6 +28,30 @@ const DeveloperPage = React.createClass({
useCustomLighthouseServers: event.target.checked, useCustomLighthouseServers: event.target.checked,
}); });
}, },
handleUpgradeFileChange: function(event) {
this.setState({
upgradePath: event.target.files[0].path,
});
},
handleForceUpgradeClick: function() {
let upgradeSent = false;
if (!this.state.upgradePath) {
alert('Please select a file to upgrade from');
} else {
try {
const stats = fs.lstatSync(this.state.upgradePath);
if (stats.isFile()) {
console.log('Starting upgrade using ' + this.state.upgradePath);
ipcRenderer.send('upgrade', this.state.upgradePath);
upgradeSent = true;
}
}
catch (e) {}
if (!upgradeSent) {
alert('Failed to start upgrade. Is "' + this.state.upgradePath + '" a valid path to the upgrade?');
}
}
},
render: function() { render: function() {
return ( return (
<main> <main>
@ -43,6 +72,13 @@ const DeveloperPage = React.createClass({
</div> </div>
: null} : null}
</section> </section>
<section className="card">
<div className="form-row">
<FormField name="file" ref="file" type="file" onChange={this.handleUpgradeFileChange}/>
&nbsp;
<Link label="Force Upgrade" button="alt" onClick={this.handleForceUpgradeClick} />
</div>
</section>
</main> </main>
); );
} }