Merge branch 'upgrades-2'
* upgrades-2: Add code to get process IDs for daemon on Windows Small bugfixes and typos Refactor shutdown process Add Mac and Windows installer launching Minor style fixes and tweaks Light refactoring of upgrade process - Use Node instead of lbrynet to get platform info - Factor out code that picks the download URI into its own function - Remove out-of-date code for checking old versions of MacOS Make upgrade process better at killing old daemons
This commit is contained in:
commit
0d0d835746
2 changed files with 195 additions and 108 deletions
213
app/main.js
213
app/main.js
|
@ -1,20 +1,64 @@
|
|||
const {app, BrowserWindow, ipcMain, shell} = require('electron');
|
||||
const {app, BrowserWindow, ipcMain} = require('electron');
|
||||
const path = require('path');
|
||||
const jayson = require('jayson');
|
||||
// tree-kill has better cross-platform handling of
|
||||
// killing a process. child-process.kill was unreliable
|
||||
const kill = require('tree-kill');
|
||||
const child_process = require('child_process');
|
||||
const assert = require('assert');
|
||||
|
||||
|
||||
let client = jayson.client.http('http://localhost:5279/lbryapi');
|
||||
// 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.
|
||||
let win
|
||||
let win;
|
||||
// Also keep the daemon subprocess alive
|
||||
let subpy
|
||||
// set to true when the quitting sequence has started
|
||||
let quitting = false;
|
||||
let daemonSubprocess;
|
||||
|
||||
// This is set to true right before we try to shut the daemon subprocess --
|
||||
// if it dies when we didn't ask it to shut down, we want to alert the user.
|
||||
let daemonSubprocessKillRequested = false;
|
||||
|
||||
// When a quit is attempted, we cancel the quit, do some preparations, then
|
||||
// this is set to true and app.quit() is called again to quit for real.
|
||||
let readyToQuit = false;
|
||||
|
||||
/*
|
||||
* Replacement for Electron's shell.openItem. The Electron version doesn't
|
||||
* reliably work from the main process, and we need to be able to run it
|
||||
* when no windows are open.
|
||||
*/
|
||||
function openItem(fullPath) {
|
||||
const subprocOptions = {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
};
|
||||
|
||||
let child;
|
||||
if (process.platform == 'darwin') {
|
||||
child = child_process.spawn('open', [fullPath], subprocOptions);
|
||||
} else if (process.platform == 'linux') {
|
||||
child = child_process.spawn('xdg-open', [fullPath], subprocOptions);
|
||||
} else if (process.platform == 'win32') {
|
||||
child = child_process.execSync('start', [fullPath], subprocOptions);
|
||||
}
|
||||
|
||||
// Causes child process reference to be garbage collected, allowing main process to exit
|
||||
child.unref();
|
||||
}
|
||||
|
||||
function getPidsForProcessName(name) {
|
||||
if (process.platform == 'windows') {
|
||||
const tasklistOut = child_process.execSync('tasklist',
|
||||
['/fi', `Imagename eq ${name}.exe`, '/nh'],
|
||||
{encoding: 'utf8'}
|
||||
).stdout;
|
||||
return tasklistOut.match(/[^\n]+/g).map((line) => line.split(/\s+/)[1]); // Second column of every non-empty line
|
||||
} else {
|
||||
const pgrepOut = child_process.spawnSync('pgrep', ['-x', name], {encoding: 'utf8'}).stdout;
|
||||
return pgrepOut.match(/\d+/g);
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow () {
|
||||
win = new BrowserWindow({backgroundColor: '#155b4a'})
|
||||
|
@ -26,49 +70,61 @@ function createWindow () {
|
|||
})
|
||||
};
|
||||
|
||||
function handleDaemonSubprocessExited() {
|
||||
console.log('The daemon has exited.');
|
||||
daemonSubprocess = null;
|
||||
if (!daemonSubprocessKillRequested) {
|
||||
// We didn't stop the daemon subprocess on purpose, so display a
|
||||
// warning and schedule a quit.
|
||||
//
|
||||
// TODO: maybe it would be better to restart the daemon?
|
||||
if (win) {
|
||||
console.log('Did not request daemon stop, so quitting in 5 seconds.');
|
||||
win.loadURL(`file://${__dirname}/dist/warning.html`);
|
||||
setTimeout(quitNow, 5000);
|
||||
} else {
|
||||
console.log('Did not request daemon stop, so quitting.');
|
||||
quitNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function launchDaemon() {
|
||||
if (subpy) {
|
||||
return;
|
||||
}
|
||||
assert(!daemonSubprocess, 'Tried to launch daemon twice');
|
||||
|
||||
if (process.env.LBRY_DAEMON) {
|
||||
executable = process.env.LBRY_DAEMON;
|
||||
} else {
|
||||
executable = path.join(__dirname, 'dist', 'lbrynet-daemon');
|
||||
}
|
||||
console.log('Launching daemon: ' + executable)
|
||||
subpy = require('child_process').spawn(executable)
|
||||
console.log('Launching daemon:', executable)
|
||||
daemonSubprocess = child_process.spawn(executable)
|
||||
// Need to handle the data event instead of attaching to
|
||||
// process.stdout because the latter doesn't work. I believe on
|
||||
// windows it buffers stdout and we don't get any meaningful output
|
||||
subpy.stdout.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
subpy.stderr.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
subpy.on('exit', () => {
|
||||
console.log('The daemon has exited. Quitting the app');
|
||||
subpy = null;
|
||||
if (quitting) {
|
||||
// If quitting is True it means that we were expecting the daemon
|
||||
// to be shutdown so we can quit right away
|
||||
app.quit();
|
||||
} else {
|
||||
// Otherwise, this shutdown was a surprise so display a warning
|
||||
// and schedule a quit
|
||||
//
|
||||
// TODO: maybe it would be better to restart the daemon?
|
||||
win.loadURL(`file://${__dirname}/dist/warning.html`);
|
||||
setTimeout(app.quit, 5000)
|
||||
}
|
||||
});
|
||||
daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());});
|
||||
daemonSubprocess.on('exit', handleDaemonSubprocessExited);
|
||||
console.log('lbrynet daemon has launched')
|
||||
}
|
||||
|
||||
/*
|
||||
* Quits without any preparation. When a quit is requested (either through the
|
||||
* interface or through app.quit()), we abort the quit, try to shut down the daemon,
|
||||
* and then call this to quit for real.
|
||||
*/
|
||||
function quitNow() {
|
||||
readyToQuit = true;
|
||||
app.quit();
|
||||
}
|
||||
|
||||
|
||||
app.on('ready', function(){
|
||||
launchDaemonIfNotRunning();
|
||||
createWindow();
|
||||
});
|
||||
|
||||
|
||||
function launchDaemonIfNotRunning() {
|
||||
// Check if the daemon is already running. If we get
|
||||
// an error its because its not running
|
||||
|
@ -86,6 +142,36 @@ function launchDaemonIfNotRunning() {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Last resort for killing unresponsive daemon instances.
|
||||
* Looks for any processes called "lbrynet-daemon" and
|
||||
* tries to force kill them.
|
||||
*/
|
||||
function forceKillAllDaemonsAndQuit() {
|
||||
console.log('Attempting to force kill any running lbrynet-daemon instances...');
|
||||
|
||||
const daemonPids = getPidsForProcessName('lbrynet-daemon');
|
||||
if (!daemonPids) {
|
||||
console.log('No lbrynet-daemon found running.');
|
||||
quitNow();
|
||||
} else {
|
||||
console.log(`Found ${daemonPids.length} running daemon instances. Attempting to force kill...`);
|
||||
|
||||
for (const pid of daemonPids) {
|
||||
const daemonKillAttemptsComplete = 0;
|
||||
kill(pid, 'SIGKILL', (err) => {
|
||||
daemonKillAttemptsComplete++;
|
||||
if (err) {
|
||||
console.log(`Failed to force kill running daemon with pid ${pid}. Error message: ${err.message}`);
|
||||
}
|
||||
if (daemonKillAttemptsComplete >= daemonPids.length) {
|
||||
quitNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
|
@ -98,12 +184,15 @@ app.on('window-all-closed', () => {
|
|||
|
||||
|
||||
app.on('before-quit', (event) => {
|
||||
if (subpy == null) {
|
||||
return
|
||||
if (!readyToQuit) {
|
||||
// We need to shutdown the daemon before we're ready to actually quit. This
|
||||
// event will be triggered re-entrantly once preparation is done.
|
||||
event.preventDefault();
|
||||
shutdownDaemonAndQuit();
|
||||
} else {
|
||||
console.log('Quitting.')
|
||||
}
|
||||
event.preventDefault();
|
||||
shutdownDaemon();
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
app.on('activate', () => {
|
||||
|
@ -112,52 +201,60 @@ app.on('activate', () => {
|
|||
if (win === null) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
function shutdownDaemon(evenIfNotStartedByApp = false) {
|
||||
if (subpy) {
|
||||
// When a quit is attempted, this is called. It attempts to shutdown the daemon,
|
||||
// then calls quitNow() to quit for real.
|
||||
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
|
||||
if (daemonSubprocess) {
|
||||
console.log('Killing lbrynet-daemon process');
|
||||
kill(subpy.pid, undefined, (err) => {
|
||||
kill(daemonSubprocess.pid, undefined, (err) => {
|
||||
console.log('Killed lbrynet-daemon process');
|
||||
requestedDaemonSubprocessKilled = true;
|
||||
quitNow();
|
||||
});
|
||||
} else if (evenIfNotStartedByApp) {
|
||||
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.
|
||||
console.log('Stopping lbrynet-daemon, even though app did not start it');
|
||||
client.request('daemon_stop', [], (err, res) => {
|
||||
if (err) {
|
||||
// We could get an error because the daemon is already stopped (good)
|
||||
// or because it's running but not responding properly (bad).
|
||||
// So try to force kill any daemons that are still running.
|
||||
|
||||
console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}`);
|
||||
forceKillAllDaemonsAndQuit();
|
||||
} else {
|
||||
console.log('Successfully stopped daemon via RPC call.')
|
||||
quitNow();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Not killing lbrynet-daemon because app did not start it')
|
||||
console.log('Not killing lbrynet-daemon because app did not start it');
|
||||
quitNow();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
win.loadURL(`file://${__dirname}/dist/quit.html`);
|
||||
}
|
||||
quitting = true;
|
||||
shutdownDaemon();
|
||||
}
|
||||
|
||||
function upgrade(event, installerPath) {
|
||||
app.on('quit', () => {
|
||||
shell.openItem(installerPath);
|
||||
console.log('Launching upgrade installer at', installerPath);
|
||||
// This gets triggered called after *all* other quit-related events, so
|
||||
// we'll only get here if we're fully prepared and quitting for real.
|
||||
openItem(installerPath);
|
||||
});
|
||||
|
||||
if (win) {
|
||||
win.loadURL(`file://${__dirname}/dist/upgrade.html`);
|
||||
}
|
||||
quitting = true;
|
||||
shutdownDaemon(true);
|
||||
|
||||
app.quit();
|
||||
// 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('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);
|
90
ui/js/app.js
90
ui/js/app.js
|
@ -24,9 +24,9 @@ import {Link} from './component/link.js';
|
|||
|
||||
const {remote, ipcRenderer, shell} = require('electron');
|
||||
const {download} = remote.require('electron-dl');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const app = require('electron').remote.app;
|
||||
const fs = remote.require('fs');
|
||||
|
||||
|
||||
var App = React.createClass({
|
||||
|
@ -42,14 +42,30 @@ var App = React.createClass({
|
|||
_upgradeDownloadItem: null,
|
||||
_version: null,
|
||||
|
||||
// Temporary workaround since electron-dl throws errors when you try to get the filename
|
||||
getUpdateUrl: function() {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return 'https://lbry.io/get/lbry.dmg';
|
||||
case 'linux':
|
||||
return 'https://lbry.io/get/lbry.deb';
|
||||
case 'win32':
|
||||
return 'https://lbry.io/get/lbry.exe';
|
||||
default:
|
||||
throw 'Unknown platform';
|
||||
}
|
||||
},
|
||||
// Hard code the filenames as a temporary workaround, because
|
||||
// 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}_amd64.deb`;
|
||||
} else {
|
||||
return `LBRY.Setup.${this._version}.exe`;
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return `LBRY-${this._version}.dmg`;
|
||||
case 'linux':
|
||||
return `LBRY_${this._version}_amd64.deb`;
|
||||
case 'windows':
|
||||
return `LBRY.Setup.${this._version}.exe`;
|
||||
default:
|
||||
throw 'Unknown platform';
|
||||
}
|
||||
},
|
||||
getInitialState: function() {
|
||||
|
@ -66,8 +82,6 @@ var App = React.createClass({
|
|||
pageArgs: typeof val !== 'undefined' ? val : null,
|
||||
errorInfo: null,
|
||||
modal: null,
|
||||
updateUrl: null,
|
||||
isOldOSX: null,
|
||||
downloadProgress: null,
|
||||
downloadComplete: false,
|
||||
};
|
||||
|
@ -90,38 +104,20 @@ var App = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
lbry.checkNewVersionAvailable((isAvailable) => {
|
||||
if (!isAvailable || sessionStorage.getItem('upgradeSkipped')) {
|
||||
return;
|
||||
}
|
||||
|
||||
lbry.getVersionInfo((versionInfo) => {
|
||||
this._version = versionInfo.lbrynet_version; // temp for building upgrade filename
|
||||
|
||||
var isOldOSX = false;
|
||||
if (versionInfo.os_system == 'Darwin') {
|
||||
var updateUrl = 'https://lbry.io/get/lbry.dmg';
|
||||
|
||||
var maj, min, patch;
|
||||
[maj, min, patch] = versionInfo.lbrynet_version.split('.');
|
||||
if (maj == 0 && min <= 2 && patch <= 2) {
|
||||
isOldOSX = true;
|
||||
}
|
||||
} else if (versionInfo.os_system == 'Linux') {
|
||||
var updateUrl = 'https://lbry.io/get/lbry.deb';
|
||||
} else if (versionInfo.os_system == 'Windows') {
|
||||
var updateUrl = 'https://lbry.io/get/lbry.exe';
|
||||
} else {
|
||||
var updateUrl = 'https://lbry.io/get';
|
||||
if (!sessionStorage.getItem('upgradeSkipped')) {
|
||||
lbry.checkNewVersionAvailable(({isAvailable}) => {
|
||||
if (!isAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
modal: 'upgrade',
|
||||
isOldOSX: isOldOSX,
|
||||
updateUrl: updateUrl,
|
||||
})
|
||||
lbry.getVersionInfo((versionInfo) => {
|
||||
this._version = versionInfo.lbrynet_version;
|
||||
this.setState({
|
||||
modal: 'upgrade',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
openDrawer: function() {
|
||||
sessionStorage.setItem('drawerOpen', true);
|
||||
|
@ -137,16 +133,14 @@ var App = React.createClass({
|
|||
});
|
||||
},
|
||||
handleUpgradeClicked: function() {
|
||||
// TODO: create a callback for onProgress and have the UI
|
||||
// show download progress
|
||||
// TODO: calling lbry.stop() ends up displaying the "daemon
|
||||
// unexpectedly stopped" page. Have a better way of shutting down
|
||||
let dir = app.getPath('temp');
|
||||
// Make a new directory within temp directory so the filename is guaranteed to be available
|
||||
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
|
||||
|
||||
let options = {
|
||||
onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}),
|
||||
directory: dir,
|
||||
};
|
||||
download(remote.getCurrentWindow(), this.state.updateUrl, options)
|
||||
download(remote.getCurrentWindow(), this.getUpdateUrl(), options)
|
||||
.then(downloadItem => {
|
||||
/**
|
||||
* TODO: get the download path directly from the download object. It should just be
|
||||
|
@ -289,11 +283,7 @@ var App = React.createClass({
|
|||
<Modal isOpen={this.state.modal == 'upgrade'} contentLabel="Update available"
|
||||
type="confirm" confirmButtonLabel="Upgrade" abortButtonLabel="Skip"
|
||||
onConfirmed={this.handleUpgradeClicked} onAborted={this.handleSkipClicked}>
|
||||
<p>Your version of LBRY is out of date and may be unreliable or insecure.</p>
|
||||
{this.state.isOldOSX
|
||||
? <p>Before installing the new version, make sure to exit LBRY. If you started the app, click the LBRY icon in your status bar and choose "Quit."</p>
|
||||
: null}
|
||||
|
||||
Your version of LBRY is out of date and may be unreliable or insecure.
|
||||
</Modal>
|
||||
<Modal isOpen={this.state.modal == 'downloading'} contentLabel="Downloading Update" type="custom">
|
||||
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null}
|
||||
|
|
Loading…
Add table
Reference in a new issue