From 2a7a36db15df03e774f83c13d77e9081c207f81d Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Thu, 23 Mar 2017 15:42:40 -0400 Subject: [PATCH 01/15] no faultstring in jsonrpc 2 --- ui/js/page/wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 05ee8db9e..bdd6eff0e 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -91,7 +91,7 @@ var SendToAddressSection = React.createClass({ } }, (error) => { this.setState({ - results: "Something went wrong: " + error.faultString + " " + error.faultCode + results: "Something went wrong: " + error.message }) }); }, -- 2.45.3 From fa4bb58c9a1fdcff4b2c1ba0660f6f9538c309fd Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Sat, 25 Mar 2017 10:23:11 -0400 Subject: [PATCH 02/15] update submodule --- lbry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbry b/lbry index e8e2a0d35..b4f5f2068 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit e8e2a0d353928f01746a275c38faf3cd8f072d28 +Subproject commit b4f5f2068fa40475cfbede7fd41c7fc210a118b1 -- 2.45.3 From f51b720e056b5b54c705a6aa2e33d860a8d9ad1a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 22 Mar 2017 06:47:44 -0400 Subject: [PATCH 03/15] Make upgrade process better at killing old daemons - Manually call xdg-open instead of using shell.openItem(), which doesn't reliably work from the main process - If there's a connection error or timeout when asking the daemon to close, fall back on force killing --- app/main.js | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- ui/js/app.js | 11 +++++------ 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/main.js b/app/main.js index 64deeb573..4b70973db 100644 --- a/app/main.js +++ b/app/main.js @@ -1,9 +1,10 @@ -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'); let client = jayson.client.http('http://localhost:5279/lbryapi'); @@ -37,7 +38,7 @@ function launchDaemon() { executable = path.join(__dirname, 'dist', 'lbrynet-daemon'); } console.log('Launching daemon: ' + executable) - subpy = require('child_process').spawn(executable) + subpy = 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 @@ -86,6 +87,31 @@ function launchDaemonIfNotRunning() { ); } +/* + * Last resort for killing unresponsive daemon instances. + * Looks for any processes called "lbrynet-daemon" and + * tries to force kill them. + */ +function forceKillAllDaemons() { + console.log("Attempting to force kill any running lbrynet-daemon instances..."); + + const fgrepOut = child_process.spawnSync('pgrep', ['-x', 'lbrynet-daemon'], {encoding: 'utf8'}).stdout; + const daemonPids = fgrepOut.split(/[^\d]+/).filter((pid) => pid); + if (!daemonPids) { + console.log('No lbrynet-daemon found running.'); + } else { + console.log(`Found ${daemonPids.length} running daemon instances. Attempting to force kill...`); + + for (const pid of daemonPids) { + kill(pid, 'SIGKILL', (err) => { + if (err) { + console.log(`Failed to force kill running daemon with pid ${pid}. Error message: ${err.message}`); + } + }); + } + } +} + // Quit when all windows are closed. app.on('window-all-closed', () => { @@ -122,10 +148,17 @@ function shutdownDaemon(evenIfNotStartedByApp = false) { console.log('Killed lbrynet-daemon process'); }); } 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}'); + forceKillAllDaemons(); + } + }); } else { console.log('Not killing lbrynet-daemon because app did not start it') } @@ -144,7 +177,10 @@ function shutdown() { function upgrade(event, installerPath) { app.on('quit', () => { - shell.openItem(installerPath); + // shell.openItem doesn't reliably work from the app process, so run xdg-open directly + child_process.spawn('xdg-open', [installerPath], { + stdio: 'ignore', + }); }); if (win) { win.loadURL(`file://${__dirname}/dist/upgrade.html`); @@ -160,4 +196,4 @@ function upgrade(event, installerPath) { ipcMain.on('upgrade', upgrade); -ipcMain.on('shutdown', shutdown); \ No newline at end of file +ipcMain.on('shutdown', shutdown); diff --git a/ui/js/app.js b/ui/js/app.js index 093c051cb..6ce99b9ba 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -27,6 +27,7 @@ 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({ @@ -137,16 +138,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 -- 2.45.3 From 052eef5f81556cf0f50593988b251312aa1911f4 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 22 Mar 2017 06:55:16 -0400 Subject: [PATCH 04/15] 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 --- ui/js/app.js | 59 ++++++++++++++++++++-------------------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 6ce99b9ba..906802bb5 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -43,6 +43,17 @@ var App = React.createClass({ _upgradeDownloadItem: null, _version: null, + getUpdateUrl: function() { + console.log('os.platform is', os.platform()); + switch (os.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'; // should now be msi + } + }, // Temporary workaround since electron-dl throws errors when you try to get the filename getUpgradeFilename: function() { if (os.platform() == 'darwin') { @@ -67,8 +78,6 @@ var App = React.createClass({ pageArgs: typeof val !== 'undefined' ? val : null, errorInfo: null, modal: null, - updateUrl: null, - isOldOSX: null, downloadProgress: null, downloadComplete: false, }; @@ -91,38 +100,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); @@ -288,11 +279,7 @@ var App = React.createClass({ -

Your version of LBRY is out of date and may be unreliable or insecure.

- {this.state.isOldOSX - ?

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."

- : null} - + Your version of LBRY is out of date and may be unreliable or insecure.
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null} -- 2.45.3 From 71e8d42b27aa1af2e810e24a95c70ba9a8419a86 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 22 Mar 2017 14:55:12 -0400 Subject: [PATCH 05/15] Minor style fixes and tweaks --- app/main.js | 4 ++-- ui/js/app.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/main.js b/app/main.js index 4b70973db..abbc24902 100644 --- a/app/main.js +++ b/app/main.js @@ -93,10 +93,10 @@ function launchDaemonIfNotRunning() { * tries to force kill them. */ function forceKillAllDaemons() { - console.log("Attempting to force kill any running lbrynet-daemon instances..."); + console.log('Attempting to force kill any running lbrynet-daemon instances...'); const fgrepOut = child_process.spawnSync('pgrep', ['-x', 'lbrynet-daemon'], {encoding: 'utf8'}).stdout; - const daemonPids = fgrepOut.split(/[^\d]+/).filter((pid) => pid); + const daemonPids = fgrepOut.match(/\d+/g); if (!daemonPids) { console.log('No lbrynet-daemon found running.'); } else { diff --git a/ui/js/app.js b/ui/js/app.js index 906802bb5..2826b4675 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -44,7 +44,6 @@ var App = React.createClass({ _version: null, getUpdateUrl: function() { - console.log('os.platform is', os.platform()); switch (os.platform()) { case 'darwin': return 'https://lbry.io/get/lbry.dmg'; -- 2.45.3 From dead2bdeb3b8db00e76846609644472d587939e6 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 23 Mar 2017 14:08:14 -0400 Subject: [PATCH 06/15] Add Mac and Windows installer launching Also starts a detached process so the window doesn't get held open (many GUI apps launch the interface in a new process, so xdg-open returns immediately, but it's not guaranteed). --- app/main.js | 29 +++++++++++++++++++++++++---- ui/js/app.js | 25 +++++++++++++++---------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/app/main.js b/app/main.js index abbc24902..7b8a9566a 100644 --- a/app/main.js +++ b/app/main.js @@ -16,6 +16,29 @@ let subpy // set to true when the quitting sequence has started let quitting = 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 createWindow () { win = new BrowserWindow({backgroundColor: '#155b4a'}) @@ -177,11 +200,9 @@ function shutdown() { function upgrade(event, installerPath) { app.on('quit', () => { - // shell.openItem doesn't reliably work from the app process, so run xdg-open directly - child_process.spawn('xdg-open', [installerPath], { - stdio: 'ignore', - }); + openItem(installerPath); }); + if (win) { win.loadURL(`file://${__dirname}/dist/upgrade.html`); } diff --git a/ui/js/app.js b/ui/js/app.js index 2826b4675..3a639c945 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -24,7 +24,6 @@ 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'); @@ -44,23 +43,29 @@ var App = React.createClass({ _version: null, getUpdateUrl: function() { - switch (os.platform()) { + 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'; // should now be msi + return 'https://lbry.io/get/lbry.exe'; + default: + throw 'Unknown platform'; } }, - // Temporary workaround since electron-dl throws errors when you try to get the filename + // 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() { -- 2.45.3 From 977acafb8c9555906278443564a9c2f76fb7d23a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 23 Mar 2017 19:07:08 -0400 Subject: [PATCH 07/15] Refactor shutdown process --- app/main.js | 132 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 53 deletions(-) diff --git a/app/main.js b/app/main.js index 7b8a9566a..bfb97dc73 100644 --- a/app/main.js +++ b/app/main.js @@ -5,16 +5,23 @@ const jayson = require('jayson'); // 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 kill the daemon subprocess -- +// if it dies when we haven't made a request, 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 @@ -50,49 +57,57 @@ function createWindow () { }) }; +function handleDaemonSubprocessExited() { + console.log('The daemon has exited.'); + daemonSubprocess = null; + if (!daemonSubprocessKillRequested) { + // We didn't stop down the daemon subprocess on purpose, so display a + // warning and schedule a quit. + // + // TODO: maybe it would be better to restart the daemon? + console.log('Did not display, so scheduling quit'); + if (win) { + win.loadURL(`file://${__dirname}/dist/warning.html`); + } + setTimeout(quitNow, 5000); + } +} + 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 = child_process.spawn(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, 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 @@ -115,21 +130,27 @@ function launchDaemonIfNotRunning() { * Looks for any processes called "lbrynet-daemon" and * tries to force kill them. */ -function forceKillAllDaemons() { +function forceKillAllDaemonsAndQuit() { console.log('Attempting to force kill any running lbrynet-daemon instances...'); const fgrepOut = child_process.spawnSync('pgrep', ['-x', 'lbrynet-daemon'], {encoding: 'utf8'}).stdout; const daemonPids = fgrepOut.match(/\d+/g); 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(); + } }); } } @@ -147,12 +168,15 @@ app.on('window-all-closed', () => { app.on('before-quit', (event) => { - if (subpy == null) { - return + if (!readyToQuit) { + // We need to shutdown the daemons 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', () => { @@ -161,14 +185,17 @@ 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, +// and then calls app.quit() to quit for real. +function shutdownDaemonAndQuit(shutdownEvenIfNotStartedByApp = 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('Stopping lbrynet-daemon, even though app did not start it'); @@ -179,42 +206,41 @@ function shutdownDaemon(evenIfNotStartedByApp = false) { // So try to force kill any daemons that are still running. console.log('received error when stopping lbrynet-daemon. Error message: {err.message}'); - forceKillAllDaemons(); + 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) { + console.log('top of upgrade()') + app.on('quit', () => { + 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); -- 2.45.3 From 31e3b3006ba24e983cd1182bfa55cef85f9d17b2 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Fri, 24 Mar 2017 03:04:30 -0400 Subject: [PATCH 08/15] Small bugfixes and typos --- app/main.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/app/main.js b/app/main.js index bfb97dc73..bcb617c96 100644 --- a/app/main.js +++ b/app/main.js @@ -15,8 +15,8 @@ let win; // Also keep the daemon subprocess alive let daemonSubprocess; -// This is set to true right before we try to kill the daemon subprocess -- -// if it dies when we haven't made a request, we want to alert the user. +// 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 @@ -61,15 +61,18 @@ function handleDaemonSubprocessExited() { console.log('The daemon has exited.'); daemonSubprocess = null; if (!daemonSubprocessKillRequested) { - // We didn't stop down the daemon subprocess on purpose, so display a + // 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? - console.log('Did not display, so scheduling quit'); 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(); } - setTimeout(quitNow, 5000); } } @@ -82,7 +85,7 @@ function launchDaemon() { } else { executable = path.join(__dirname, 'dist', 'lbrynet-daemon'); } - console.log('Launching daemon: ' + 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 @@ -94,8 +97,9 @@ function launchDaemon() { } /* - * Quits without any preparation (when a quit is requested, we abort the quit, try to shut down - * the daemon, and then call this to quit for real). + * 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; @@ -169,7 +173,7 @@ app.on('window-all-closed', () => { app.on('before-quit', (event) => { if (!readyToQuit) { - // We need to shutdown the daemons before we're ready to actually quit. This + // 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(); @@ -187,9 +191,9 @@ app.on('activate', () => { } }); -// When a quit is attempted, this is called, it attempts to shutdown the daemon, -// and then calls app.quit() to quit for real. -function shutdownDaemonAndQuit(shutdownEvenIfNotStartedByApp = false) { +// 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(daemonSubprocess.pid, undefined, (err) => { @@ -205,7 +209,7 @@ function shutdownDaemonAndQuit(shutdownEvenIfNotStartedByApp = false) { // 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}'); + console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}`); forceKillAllDaemonsAndQuit(); } else { console.log('Successfully stopped daemon via RPC call.') @@ -222,8 +226,6 @@ function shutdownDaemonAndQuit(shutdownEvenIfNotStartedByApp = false) { } function upgrade(event, installerPath) { - console.log('top of upgrade()') - app.on('quit', () => { console.log('Launching upgrade installer at', installerPath); // This gets triggered called after *all* other quit-related events, so -- 2.45.3 From 3e33f78235e4b0871f142c57d4c10b8c9f8d519a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Fri, 24 Mar 2017 03:06:54 -0400 Subject: [PATCH 09/15] Add code to get process IDs for daemon on Windows --- app/main.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/main.js b/app/main.js index bcb617c96..d10d250ef 100644 --- a/app/main.js +++ b/app/main.js @@ -47,6 +47,19 @@ function openItem(fullPath) { 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'}) win.maximize() @@ -137,8 +150,7 @@ function launchDaemonIfNotRunning() { function forceKillAllDaemonsAndQuit() { console.log('Attempting to force kill any running lbrynet-daemon instances...'); - const fgrepOut = child_process.spawnSync('pgrep', ['-x', 'lbrynet-daemon'], {encoding: 'utf8'}).stdout; - const daemonPids = fgrepOut.match(/\d+/g); + const daemonPids = getPidsForProcessName('lbrynet-daemon'); if (!daemonPids) { console.log('No lbrynet-daemon found running.'); quitNow(); -- 2.45.3 From e19c16c1cefb58c34d87d6e3a07348bcdc750435 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Sat, 25 Mar 2017 10:29:07 -0400 Subject: [PATCH 10/15] =?UTF-8?q?Bump=20version:=200.9.2rc1=20=E2=86=92=20?= =?UTF-8?q?0.9.2rc2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 785910217..d477ef385 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc1 +current_version = 0.9.2rc2 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index bb3ccbbc4..dc2762462 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc1", + "version": "0.9.2rc2", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index b4f5f2068..b04455f9c 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit b4f5f2068fa40475cfbede7fd41c7fc210a118b1 +Subproject commit b04455f9c733575b5191f1711d6d065ea5195ebb diff --git a/ui/package.json b/ui/package.json index e01103e7a..f4caf9b3e 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc1", + "version": "0.9.2rc2", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.3 From dc13de7160dc7a0f2ad8eb08b3d05887bd7c3175 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Sun, 26 Mar 2017 14:30:18 -0400 Subject: [PATCH 11/15] make page changes fast --- ui/CHANGELOG.md => CHANGELOG.md | 4 +++ app/main.js | 2 +- ui/js/app.js | 43 +++++++++++++++++++++-------- ui/js/component/drawer.js | 27 +++++++++++------- ui/js/component/file-actions.js | 5 +++- ui/js/component/file-tile.js | 8 +++--- ui/js/lbry.js | 43 +++++++++++++++++++++++------ ui/js/page/file-list.js | 4 +-- ui/js/page/help.js | 2 +- ui/js/page/show.js | 2 +- ui/js/page/wallet.js | 9 +++++- ui/scss/component/_load-screen.scss | 1 + 12 files changed, 110 insertions(+), 40 deletions(-) rename ui/CHANGELOG.md => CHANGELOG.md (83%) diff --git a/ui/CHANGELOG.md b/CHANGELOG.md similarity index 83% rename from ui/CHANGELOG.md rename to CHANGELOG.md index b8c82b321..c621b006e 100644 --- a/ui/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added + * The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change. + * lbry.js now offers a subscription model for wallet balance similar to file info. + * Fixed file info subscribes not being unsubscribed in unmount. + * Fixed drawer not highlighting selected page. * You can now make API calls directly on the lbry module, e.g. lbry.peer_list() * New-style API calls return promises instead of using callbacks * Wherever possible, use outpoints for unique IDs instead of names or SD hashes diff --git a/app/main.js b/app/main.js index 64deeb573..9ea2b52e8 100644 --- a/app/main.js +++ b/app/main.js @@ -17,7 +17,7 @@ let quitting = false; function createWindow () { - win = new BrowserWindow({backgroundColor: '#155b4a'}) + win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary win.maximize() //win.webContents.openDevTools() win.loadURL(`file://${__dirname}/dist/index.html`) diff --git a/ui/js/app.js b/ui/js/app.js index 093c051cb..d5a44a26b 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -40,9 +40,15 @@ var App = React.createClass({ }, _upgradeDownloadItem: null, + _isMounted: false, _version: null, // Temporary workaround since electron-dl throws errors when you try to get the filename + getDefaultProps: function() { + return { + address: window.location.search + }; + }, getUpgradeFilename: function() { if (os.platform() == 'darwin') { return `LBRY-${this._version}.dmg`; @@ -52,32 +58,35 @@ var App = React.createClass({ return `LBRY.Setup.${this._version}.exe`; } }, - getInitialState: function() { + getViewingPageAndArgs: function(address) { // For now, routes are in format ?page or ?page=args - var match, param, val, viewingPage, - drawerOpenRaw = sessionStorage.getItem('drawerOpen'); - - [match, viewingPage, val] = window.location.search.match(/\??([^=]*)(?:=(.*))?/); - - + let isMatch, viewingPage, pageArgs; + [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); return { viewingPage: viewingPage, + pageArgs: pageArgs === undefined ? null : pageArgs + }; + }, + getInitialState: function() { + var match, param, val, viewingPage, pageArgs, + drawerOpenRaw = sessionStorage.getItem('drawerOpen'); + + return Object.assign(this.getViewingPageAndArgs(this.props.address), { drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true, - pageArgs: typeof val !== 'undefined' ? val : null, errorInfo: null, modal: null, updateUrl: null, isOldOSX: null, downloadProgress: null, downloadComplete: false, - }; + }); }, componentWillMount: function() { document.addEventListener('unhandledError', (event) => { this.alertError(event.detail); }); - //open links in external browser + //open links in external browser and skip full redraw on changing page document.addEventListener('click', function(event) { var target = event.target; while (target && target !== document) { @@ -86,9 +95,15 @@ var App = React.createClass({ shell.openExternal(target.href); return; } + if (target.matches('a[href^="?"]')) { + event.preventDefault(); + if (this._isMounted) { + this.setState(this.getViewingPageAndArgs(target.getAttribute('href'))); + } + } target = target.parentNode; } - }); + }.bind(this)); lbry.checkNewVersionAvailable((isAvailable) => { if (!isAvailable || sessionStorage.getItem('upgradeSkipped')) { @@ -136,6 +151,12 @@ var App = React.createClass({ modal: null, }); }, + componentDidMount: function() { + this._isMounted = true; + }, + componentWillUnmount: function() { + this._isMounted = false; + }, handleUpgradeClicked: function() { // TODO: create a callback for onProgress and have the UI // show download progress diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js index bc8f6d666..eaf11506b 100644 --- a/ui/js/component/drawer.js +++ b/ui/js/component/drawer.js @@ -9,7 +9,7 @@ var DrawerItem = React.createClass({ }; }, render: function() { - var isSelected = (this.props.viewingPage == this.props.href.substr(2) || + var isSelected = (this.props.viewingPage == this.props.href.substr(1) || this.props.subPages.indexOf(this.props.viewingPage) != -1); return } @@ -20,9 +20,11 @@ var drawerImageStyle = { //@TODO: remove this, img should be properly scaled onc }; var Drawer = React.createClass({ + _balanceSubscribeId: null, + handleLogoClicked: function(event) { if ((event.ctrlKey || event.metaKey) && event.shiftKey) { - window.location.href = 'index.html?developer' + window.location.href = '?developer' event.preventDefault(); } }, @@ -32,25 +34,30 @@ var Drawer = React.createClass({ }; }, componentDidMount: function() { - lbry.getBalance(function(balance) { + this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) { this.setState({ balance: balance }); }.bind(this)); }, + componentWillUnmount: function() { + if (this._balanceSubscribeId) { + lbry.balanceUnsubscribe(this._balanceSubscribeId) + } + }, render: function() { return ( ); } diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 9a1d42af3..b9d831e69 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -253,7 +253,7 @@ export let FileActions = React.createClass({ if (this.isMounted) { this.setState({ fileInfo: fileInfo, - }); + }); } }, componentDidMount: function() { @@ -276,6 +276,9 @@ export let FileActions = React.createClass({ }, componentWillUnmount: function() { this._isMounted = false; + if (this._fileInfoSubscribeId) { + lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId); + } }, render: function() { const fileInfo = this.state.fileInfo; diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 4cc3166ae..5c9ce2890 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -79,7 +79,7 @@ export let FileTileStream = React.createClass({ componentDidMount: function() { this._isMounted = true; if (this.props.hideOnRemove) { - lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); + this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); } }, componentWillUnmount: function() { @@ -121,15 +121,15 @@ export let FileTileStream = React.createClass({
- +
{ !this.props.hidePrice ? : null} - +

- + {title} diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 5d41abc42..ebf1574ba 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -223,7 +223,7 @@ lbry.stopFile = function(name, callback) { lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) { this._removedFiles.push(outpoint); - this._updateSubscribedFileInfo(outpoint); + this._updateFileInfoSubscribers(outpoint); lbry.file_delete({ outpoint: outpoint, @@ -405,9 +405,11 @@ lbry.stop = function(callback) { }; lbry.fileInfo = {}; -lbry._fileInfoSubscribeIdCounter = 0; +lbry._subscribeIdCount = 0; lbry._fileInfoSubscribeCallbacks = {}; lbry._fileInfoSubscribeInterval = 5000; +lbry._balanceSubscribeCallbacks = {}; +lbry._balanceSubscribeInterval = 5000; lbry._removedFiles = []; lbry._claimIdOwnershipCache = {}; @@ -419,7 +421,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { }); }; -lbry._updateSubscribedFileInfo = function(outpoint) { +lbry._updateFileInfoSubscribers = function(outpoint) { const callSubscribedCallbacks = (outpoint, fileInfo) => { for (let [subscribeId, callback] of Object.entries(this._fileInfoSubscribeCallbacks[outpoint])) { callback(fileInfo); @@ -446,7 +448,7 @@ lbry._updateSubscribedFileInfo = function(outpoint) { if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) { setTimeout(() => { - this._updateSubscribedFileInfo(outpoint); + this._updateFileInfoSubscribers(outpoint); }, lbry._fileInfoSubscribeInterval); } } @@ -457,14 +459,39 @@ lbry.fileInfoSubscribe = function(outpoint, callback) { lbry._fileInfoSubscribeCallbacks[outpoint] = {}; } - const subscribeId = ++lbry._fileInfoSubscribeIdCounter; + const subscribeId = ++lbry._subscribeIdCount; lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback; - lbry._updateSubscribedFileInfo(outpoint); + lbry._updateFileInfoSubscribers(outpoint); return subscribeId; } -lbry.fileInfoUnsubscribe = function(name, subscribeId) { - delete lbry._fileInfoSubscribeCallbacks[name][subscribeId]; +lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) { + delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId]; +} + +lbry._updateBalanceSubscribers = function() { + lbry.get_balance().then(function(balance) { + for (let [subscribeId, callback] of Object.entries(lbry._balanceSubscribeCallbacks)) { + callback(balance); + } + }); + + if (Object.keys(lbry._balanceSubscribeCallbacks).length) { + setTimeout(() => { + lbry._updateBalanceSubscribers(); + }, lbry._balanceSubscribeInterval); + } +} + +lbry.balanceSubscribe = function(callback) { + const subscribeId = ++lbry._subscribeIdCount; + lbry._balanceSubscribeCallbacks[subscribeId] = callback; + lbry._updateBalanceSubscribers(); + return subscribeId; +} + +lbry.balanceUnsubscribe = function(subscribeId) { + delete lbry._balanceSubscribeCallbacks[subscribeId]; } lbry.showMenuIfNeeded = function() { diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 734cb75eb..8134be11f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -41,7 +41,7 @@ export let FileListDownloaded = React.createClass({ } else if (!this.state.fileInfos.length) { return (
- You haven't downloaded anything from LBRY yet. Go ! + You haven't downloaded anything from LBRY yet. Go !
); } else { @@ -90,7 +90,7 @@ export let FileListPublished = React.createClass({ else if (!this.state.fileInfos.length) { return (
- You haven't published anything to LBRY yet. Try ! + You haven't published anything to LBRY yet. Try !
); } diff --git a/ui/js/page/help.js b/ui/js/page/help.js index 6bf964205..632c3abd0 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help.js @@ -67,7 +67,7 @@ var HelpPage = React.createClass({

Report a Bug

Did you find something wrong?

-

+

Thanks! LBRY is made by its users.
{!ver ? null : diff --git a/ui/js/page/show.js b/ui/js/page/show.js index a2bb1b5f1..8f4d450c9 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -155,7 +155,7 @@ var DetailPage = React.createClass({ ) : (

No content

- There is no content available at the name lbry://{this.props.name}. If you reached this page from a link within the LBRY interface, please . Thanks! + There is no content available at the name lbry://{this.props.name}. If you reached this page from a link within the LBRY interface, please . Thanks!
)}

diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 05ee8db9e..6e6714b0a 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -243,6 +243,8 @@ var TransactionList = React.createClass({ var WalletPage = React.createClass({ + _balanceSubscribeId: null, + propTypes: { viewingPage: React.PropTypes.string, }, @@ -259,12 +261,17 @@ var WalletPage = React.createClass({ } }, componentWillMount: function() { - lbry.getBalance((results) => { + this._balanceSubscribeId = lbry.balanceSubscribe((results) => { this.setState({ balance: results, }) }); }, + componentWillUnmount: function() { + if (this._balanceSubscribeId) { + lbry.balanceUnsubscribe(this._balanceSubscribeId); + } + }, render: function() { return (
diff --git a/ui/scss/component/_load-screen.scss b/ui/scss/component/_load-screen.scss index 06540324f..e56eb12c0 100644 --- a/ui/scss/component/_load-screen.scss +++ b/ui/scss/component/_load-screen.scss @@ -2,6 +2,7 @@ .load-screen { color: white; + background: $color-primary; background-size: cover; min-height: 100vh; min-width: 100vw; -- 2.45.3 From a0903e6b4fb1a98213e605cead26e31ea73f2adf Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 27 Mar 2017 01:44:13 -0400 Subject: [PATCH 12/15] Style tweaks --- ui/js/app.js | 7 +++---- ui/js/lbry.js | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index d5a44a26b..b2f58cd14 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -60,8 +60,7 @@ var App = React.createClass({ }, getViewingPageAndArgs: function(address) { // For now, routes are in format ?page or ?page=args - let isMatch, viewingPage, pageArgs; - [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); + let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); return { viewingPage: viewingPage, pageArgs: pageArgs === undefined ? null : pageArgs @@ -87,7 +86,7 @@ var App = React.createClass({ }); //open links in external browser and skip full redraw on changing page - document.addEventListener('click', function(event) { + document.addEventListener('click', (event) => { var target = event.target; while (target && target !== document) { if (target.matches('a[href^="http"]')) { @@ -103,7 +102,7 @@ var App = React.createClass({ } target = target.parentNode; } - }.bind(this)); + }); lbry.checkNewVersionAvailable((isAvailable) => { if (!isAvailable || sessionStorage.getItem('upgradeSkipped')) { diff --git a/ui/js/lbry.js b/ui/js/lbry.js index ebf1574ba..4f5f6cbec 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -423,7 +423,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { lbry._updateFileInfoSubscribers = function(outpoint) { const callSubscribedCallbacks = (outpoint, fileInfo) => { - for (let [subscribeId, callback] of Object.entries(this._fileInfoSubscribeCallbacks[outpoint])) { + for (let callback of Object.values(this._fileInfoSubscribeCallbacks[outpoint])) { callback(fileInfo); } } @@ -471,7 +471,7 @@ lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) { lbry._updateBalanceSubscribers = function() { lbry.get_balance().then(function(balance) { - for (let [subscribeId, callback] of Object.entries(lbry._balanceSubscribeCallbacks)) { + for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) { callback(balance); } }); -- 2.45.3 From e5239069017773467544c9e1e6befd41de69c068 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 14 Mar 2017 23:05:07 -0400 Subject: [PATCH 13/15] Make publishes appear immediately in My Files Uses a client side cache to simulate entries in the file manager and claims list. Also adds new utility functions for using Local Storage. --- ui/js/lbry.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++--- ui/js/utils.js | 15 ++++ 2 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 ui/js/utils.js diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 4f5f6cbec..aeb91e12f 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,15 +1,103 @@ import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; +import {getLocal, setLocal} from './utils.js'; const {remote} = require('electron'); const menu = remote.require('./menu/main-menu'); +/** + * Records a publish attempt in local storage. Returns a dictionary with all the data needed to + * needed to make a dummy claim or file info object. + */ +function savePendingPublish(name) { + const pendingPublishes = getLocal('pendingPublishes') || []; + const newPendingPublish = { + claim_id: 'pending_claim_' + name, + txid: 'pending_' + name, + nout: 0, + outpoint: 'pending_' + name + ':0', + name: name, + time: Date.now(), + }; + setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); + return newPendingPublish; +} + +function removePendingPublish({name, outpoint}) { + setLocal('pendingPublishes', getPendingPublishes().filter( + (pub) => pub.name != name && pub.outpoint != outpoint + )); +} + +/** + * Gets the current list of pending publish attempts. Filters out any that have timed out and + * removes them from the list. + */ +function getPendingPublishes() { + const pendingPublishes = getLocal('pendingPublishes') || []; + + const newPendingPublishes = []; + for (let pendingPublish of pendingPublishes) { + if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) { + newPendingPublishes.push(pendingPublish); + } + } + setLocal('pendingPublishes', newPendingPublishes); + return newPendingPublishes +} + +/** + * Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found + * but it has timed out), returns null. + */ +function getPendingPublish({name, outpoint}) { + const pendingPublishes = getPendingPublishes(); + const pendingPublishIndex = pendingPublishes.findIndex( + ({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint + ); + const pendingPublish = pendingPublishes[pendingPublishIndex]; + + if (pendingPublishIndex == -1) { + return null; + } else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) { + // Pending publish timed out, so remove it from the stored list and don't match + + const newPendingPublishes = pendingPublishes.slice(); + newPendingPublishes.splice(pendingPublishIndex, 1); + setLocal('pendingPublishes', newPendingPublishes); + return null; + } else { + return pendingPublish; + } +} + +function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) { + return { + name: name, + outpoint: outpoint, + claim_id: claim_id, + txid: txid, + nout: nout, + }; +} + +function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { + return { + name: name, + outpoint: outpoint, + claim_id: claim_id, + metadata: "Attempting publication", + }; +} + + let lbry = { isConnected: false, rootPath: '.', daemonConnectionString: 'http://localhost:5279/lbryapi', webUiUri: 'http://localhost:5279', peerListTimeout: 6000, + pendingPublishTimeout: 20 * 60 * 1000, colors: { primary: '#155B4A' }, @@ -251,18 +339,49 @@ lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) }, () => scheduleNextCheckOrTimeout()); } +/** + * Publishes a file. The optional fileListedCallback is called when the file becomes available in + * lbry.file_list() during the publish process. + * + * This currently includes a work-around to cache the file in local storage so that the pending + * publish can appear in the UI immediately. + */ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) { - // Publishes a file. - // The optional fileListedCallback is called when the file becomes available in - // lbry.getFilesInfo() during the publish process. + lbry.call('publish', params, (result) => { + if (returnedPending) { + return; + } - // Use ES6 named arguments instead of directly passing param dict? - lbry.call('publish', params, publishedCallback, errorCallback); - if (fileListedCallback) { - lbry.getFileInfoWhenListed(params.name, function(fileInfo) { - fileListedCallback(fileInfo); - }); - } + clearTimeout(returnPendingTimeout); + publishedCallback(result); + }, (err) => { + if (returnedPending) { + return; + } + + clearTimeout(returnPendingTimeout); + errorCallback(err); + }); + + let returnedPending = false; + // Give a short grace period in case publish() returns right away or (more likely) gives an error + const returnPendingTimeout = setTimeout(() => { + returnedPending = true; + + if (publishedCallback) { + savePendingPublish(params.name); + publishedCallback(true); + } + + if (fileListedCallback) { + savePendingPublish(params.name); + fileListedCallback(true); + } + }, 2000); + + //lbry.getFileInfoWhenListed(params.name, function(fileInfo) { + // fileListedCallback(fileInfo); + //}); } lbry.getVersionInfo = function(callback) { @@ -503,6 +622,60 @@ lbry.showMenuIfNeeded = function() { sessionStorage.setItem('menuShown', chosenMenu); }; +/** + * Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs, + * these are designed to be transparent wrappers around the corresponding API methods. + */ + +/** + * Returns results from the file_list API method, plus dummy entries for pending publishes. + * (If a real publish with the same name is found, the pending publish will be ignored and removed.) + */ +lbry.file_list = function(params={}) { + return new Promise((resolve, reject) => { + const {name, outpoint} = params; + + /** + * If we're searching by outpoint, check first to see if there's a matching pending publish. + * Pending publishes use their own faux outpoints that are always unique, so we don't need + * to check if there's a real file. + */ + if (outpoint !== undefined) { + const pendingPublish = getPendingPublish({outpoint}); + if (pendingPublish) { + resolve([pendingPublishToDummyFileInfo(pendingPublish)]); + return; + } + } + + lbry.call('file_list', params, (fileInfos) => { + // Remove any pending publications that are now listed in the file manager + + const pendingPublishes = getPendingPublishes(); + for (let {name: itemName} of fileInfos) { + if (pendingPublishes.find(() => name == itemName)) { + removePendingPublish({name: name}); + } + } + const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo); + resolve([...fileInfos, ...dummyFileInfos]); + }, reject, reject); + }); +} + +lbry.claim_list_mine = function(params={}) { + return new Promise((resolve, reject) => { + lbry.call('claim_list_mine', params, (claims) => { + // Filter out pending publishes when the name is already in the file manager + const dummyClaims = getPendingPublishes().filter( + (pub) => !claims.find(({name}) => name == pub.name) + ).map(pendingPublishToDummyClaim); + + resolve([...claims, ...dummyClaims]); + }, reject, reject); + }); +} + lbry = new Proxy(lbry, { get: function(target, name) { if (name in target) { diff --git a/ui/js/utils.js b/ui/js/utils.js new file mode 100644 index 000000000..5b5cf246a --- /dev/null +++ b/ui/js/utils.js @@ -0,0 +1,15 @@ +/** + * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value + * is not set yet. + */ +export function getLocal(key) { + const itemRaw = localStorage.getItem(key); + return itemRaw === null ? undefined : JSON.parse(itemRaw); +} + +/** + * Thin wrapper around localStorage.setItem(). Converts value to JSON. + */ +export function setLocal(key, value) { + localStorage.setItem(key, JSON.stringify(value)); +} -- 2.45.3 From b8f1447b7ee5e956313d0225af1e4b4f4dd918a2 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 27 Mar 2017 02:55:51 -0400 Subject: [PATCH 14/15] Don't display Downloading bar on published files We handled this just fine until recently; not sure where the regression happened. --- ui/js/component/file-actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index b9d831e69..c8331e261 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -179,7 +179,7 @@ let FileActionsRow = React.createClass({ let linkBlock; if (this.state.fileInfo === false && !this.state.attemptingDownload) { linkBlock = ; - } else if (this.state.attemptingDownload || !this.state.fileInfo.completed) { + } else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) { const progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0, label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...', -- 2.45.3 From b2fcdd1f9a24b5d361f1c8228a2bff9a683cf1b6 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 27 Mar 2017 03:06:59 -0400 Subject: [PATCH 15/15] Add changelog entry for fast publishes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c621b006e..2fe463b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * You can now make API calls directly on the lbry module, e.g. lbry.peer_list() * New-style API calls return promises instead of using callbacks * Wherever possible, use outpoints for unique IDs instead of names or SD hashes + * New publishes now display immediately in My Files, even before they hit the lbrynet file manager. ### Changed * Update process now easier and more reliable -- 2.45.3