From d2bca75a9ba74de43f7936e11d9d573a69c22652 Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Thu, 8 Dec 2016 22:26:18 -0800 Subject: [PATCH 001/125] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..1eaf00e4 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# nbrsessionproxy +Jupyter extensions for running an RStudio rsession proxy From f9031bb26076069ae076911f18acb7ee44776cf9 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 22:44:42 -0800 Subject: [PATCH 002/125] Initial commit. --- nbrsessionproxy/__init__.py | 18 +++++++ nbrsessionproxy/handlers.py | 89 ++++++++++++++++++++++++++++++++++ nbrsessionproxy/static/main.js | 65 +++++++++++++++++++++++++ setup.py | 12 +++++ 4 files changed, 184 insertions(+) create mode 100644 nbrsessionproxy/__init__.py create mode 100644 nbrsessionproxy/handlers.py create mode 100644 nbrsessionproxy/static/main.js create mode 100644 setup.py diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py new file mode 100644 index 00000000..40633450 --- /dev/null +++ b/nbrsessionproxy/__init__.py @@ -0,0 +1,18 @@ +from nbrsessionproxy.handlers import setup_handlers + +# Jupyter Extension points +def _jupyter_server_extension_paths(): + return [{ + 'module': 'nbrsessionproxy', + }] + +def _jupyter_nbextension_paths(): + return [{ + "section": "notebook", + "dest": "nbrsessionproxy", + "src": "static", + "require": "nbrsessionproxy/main" + }] + +def load_jupyter_server_extension(nbapp): + setup_handlers(nbapp.web_app) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py new file mode 100644 index 00000000..21cc1b46 --- /dev/null +++ b/nbrsessionproxy/handlers.py @@ -0,0 +1,89 @@ +import os +import json +import logging +import subprocess as sp + +from tornado import web + +from notebook.utils import url_path_join as ujoin +from notebook.base.handlers import IPythonHandler + +logger = logging.getLogger('nbrsessionproxy') + +class RSessionProxyHandler(IPythonHandler): + + rsession_port = 8005 + rsession_path = '/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin' + rsession_ld_lib_path = '/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + + rsession_env = { + 'R_DOC_DIR':'/usr/share/R/doc', + 'R_HOME':'/usr/lib/R', + 'R_INCLUDE_DIR':'/usr/share/R/include', + 'R_SHARE_DIR':'/usr/share/R/share', + 'RSTUDIO_DEFAULT_R_VERSION':'3.3.0', + 'RSTUDIO_DEFAULT_R_VERSION_HOME':'/usr/lib/R', + 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', + 'RSTUDIO_MINIMUM_USER_ID':'500', + } + rsession_cmd = [ + '/usr/lib/rstudio-server/bin/rsession', + '--standalone=1', + '--program-mode=server', + '--log-stderr=1', + '--www-port={}'.format(rsession_port), + '--user-identity={}'.format(os.environ['USER']), + ] + + proc = None + + @web.authenticated + def post(self): + logger.info('%s request to %s', self.request.method, self.request.uri) + + server_env = os.environ.copy() + + # Seed RStudio's R and RSTUDIO variables + server_env.update(self.rsession_env) + + # Prepend RStudio's PATH and LD_LIBRARY_PATH + server_env['PATH'] = self.rsession_path + ':' + server_env['PATH'] + server_env['LD_LIBRARY_PATH'] = \ + self.rsession_ld_lib_path + ':' + server_env['LD_LIBRARY_PATH'] + + # Runs rsession in background since we do not need stdout/stderr + self.proc = sp.Popen(self.rsession_cmd, env=server_env) + + if self.proc.poll() == 0: + raise web.HTTPError(reason='rsession terminated', status_code=500) + self.finish() + + response = { + 'pid':self.proc.pid, + 'url':'{}proxy/{}/'.format(self.base_url, self.rsession_port), + } + + self.finish(json.dumps(response)) + + @web.authenticated + def get(self): + if not self.proc: + self.set_status(500) + self.write('rsession not yet started') + self.finish() + self.finish(self.proc.poll()) + + def delete(self): + logger.info('%s request to %s', self.request.method, self.request.uri) + self.proc.kill() + self.finish(self.proc.poll()) + +def setup_handlers(web_app): + host_pattern = '.*$' + route_pattern = ujoin(web_app.settings['base_url'], '/rsessionproxy/?') + web_app.add_handlers(host_pattern, [ + (route_pattern, RSessionProxyHandler) + ]) + logger.info('Added handler for route %s', route_pattern) + +# vim: set et ts=4 sw=4: diff --git a/nbrsessionproxy/static/main.js b/nbrsessionproxy/static/main.js new file mode 100644 index 00000000..e9b318b9 --- /dev/null +++ b/nbrsessionproxy/static/main.js @@ -0,0 +1,65 @@ +define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, utils) { + + var base_url = utils.get_body_data('baseUrl'); + + function open_rsession(data) { + console.log("response: " + data); + var proxy_url; + if ("url" in data) { + proxy_url = data['url']; + } else { + /* debug hack */ + proxy_url = base_url + 'proxy/8001/'; + } + var w = window.open(proxy_url, "_blank"); + w.focus(); + } + + function load() { + console.log("nbrsessionproxy loading"); + if (!namespace.notebook_list) return; + + /* the url we POST to to start rsession */ + var rsp_url = base_url + 'rsessionproxy'; + console.log("nbrsessionproxy: url: " + rsp_url); + + /* locate the right-side dropdown menu of apps and notebooks */ + var menu = $('.tree-buttons').find('.dropdown-menu'); + + /* create a divider */ + var divider = $('
  • ') + .attr('role', 'presentation') + .addClass('divider'); + + /* add the divider */ + menu.append(divider); + + /* create our list item */ + var rsession_item = $('
  • ') + .attr('role', 'presentation') + .addClass('new-rsessionproxy'); + + /* create our list item's link */ + var rsession_link = $('') + .attr('role', 'menuitem') + .attr('tabindex', '-1') + .text('RStudio Session') + .on('click', function() { + $.post(rsp_url, {}, open_rsession); + }); + + /* add the link to the item and + * the item to the menu */ + rsession_item.append(rsession_link); + menu.append(rsession_item); + } + + var load_ipython_extension = function () { + load(); + } + + return { + load_ipython_extension: load_ipython_extension, + }; + +}); diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..bc20282e --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +import setuptools + +setuptools.setup( + name="nbrsessionproxy", + version='0.0.1', + url="https://github.com/ryanlovett/nbrsessionproxy", + author="Ryan Lovett", + description="Jupyter extensions to proxy RStudio's rsession", + packages=setuptools.find_packages(), + install_requires=[ 'tornado', 'notebook' ], + package_data={'nbrsessionproxy': ['static/*']}, +) From 3b808c8095cff3c89da052e0343dd14cef61410d Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 22:45:06 -0800 Subject: [PATCH 003/125] Add installation instructions. --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1eaf00e4..cc719b84 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ # nbrsessionproxy -Jupyter extensions for running an RStudio rsession proxy +Jupyter extensions to proxy RStudio's rsession. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy). + +## Installation +Install the library: +``` +pip install git+https://github.com/ryanlovett/nbrsessionproxy +``` + +Install the extensions for the user: +``` +jupyter serverextension enable --py nbrsessionproxy +jupyter nbextension install --py nbrsessionproxy +jupyter nbextension enable --py nbrsessionproxy +``` + +Install the extensions for all users on the system: +``` +pip install git+https://github.com/ryanlovett/nbrsessionproxy +jupyter serverextension enable --py --sys-prefix --system nbrsessionproxy +jupyter nbextension install --py --sys-prefix --system nbrsessionproxy +jupyter nbextension enable --py --sys-prefix --system nbrsessionproxy +``` From 7ab7d8e07f0c7c83c3f8d7fd369d4402939d53c4 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 23:16:34 -0800 Subject: [PATCH 004/125] Account for unset USER. --- nbrsessionproxy/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 21cc1b46..ccb8cd0a 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -32,7 +32,7 @@ class RSessionProxyHandler(IPythonHandler): '--program-mode=server', '--log-stderr=1', '--www-port={}'.format(rsession_port), - '--user-identity={}'.format(os.environ['USER']), + '--user-identity={}'.format(os.environ.get('USER', '')), ] proc = None From 48960862ef5cfe34c87f5b7fdc89c3a20d7a2dce Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 23:17:34 -0800 Subject: [PATCH 005/125] Add .gitignore. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b399da28 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nbrsessionproxy/__pycache__ From 6c763a14b1fe3c84ea565168dc2ffa1b6c547cf2 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 23:57:12 -0800 Subject: [PATCH 006/125] Do not assume empty environment paths. --- nbrsessionproxy/handlers.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index ccb8cd0a..686d6c49 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -13,8 +13,10 @@ class RSessionProxyHandler(IPythonHandler): rsession_port = 8005 - rsession_path = '/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin' - rsession_ld_lib_path = '/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + rsession_paths = { + 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', + 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + } rsession_env = { 'R_DOC_DIR':'/usr/share/R/doc', @@ -46,10 +48,11 @@ def post(self): # Seed RStudio's R and RSTUDIO variables server_env.update(self.rsession_env) - # Prepend RStudio's PATH and LD_LIBRARY_PATH - server_env['PATH'] = self.rsession_path + ':' + server_env['PATH'] - server_env['LD_LIBRARY_PATH'] = \ - self.rsession_ld_lib_path + ':' + server_env['LD_LIBRARY_PATH'] + # Prepend RStudio's requisite paths + for env_var in self.rsession_paths.keys(): + path = server_env.get(env_var, '') + if path != '': path = ':' + path + server_env[env_var] = self.rsession_paths[env_var] + path # Runs rsession in background since we do not need stdout/stderr self.proc = sp.Popen(self.rsession_cmd, env=server_env) From 3bd56f706104e6ed58ab1448927d0129f2eec1bd Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 00:07:05 -0800 Subject: [PATCH 007/125] Use get_current_user. --- nbrsessionproxy/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 686d6c49..a0d90f7b 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -34,7 +34,6 @@ class RSessionProxyHandler(IPythonHandler): '--program-mode=server', '--log-stderr=1', '--www-port={}'.format(rsession_port), - '--user-identity={}'.format(os.environ.get('USER', '')), ] proc = None @@ -43,6 +42,7 @@ class RSessionProxyHandler(IPythonHandler): def post(self): logger.info('%s request to %s', self.request.method, self.request.uri) + self.rsession_cmd.append('--user-identity=' + self.current_user) server_env = os.environ.copy() # Seed RStudio's R and RSTUDIO variables From 49bac84221679fc0c9f7681088344dc2e2f71250 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 00:29:02 -0800 Subject: [PATCH 008/125] Specify user-identity once. --- nbrsessionproxy/handlers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index a0d90f7b..3bcc5d48 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -12,7 +12,7 @@ class RSessionProxyHandler(IPythonHandler): - rsession_port = 8005 + rsession_port = 8787 rsession_paths = { 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' @@ -33,7 +33,6 @@ class RSessionProxyHandler(IPythonHandler): '--standalone=1', '--program-mode=server', '--log-stderr=1', - '--www-port={}'.format(rsession_port), ] proc = None @@ -42,7 +41,11 @@ class RSessionProxyHandler(IPythonHandler): def post(self): logger.info('%s request to %s', self.request.method, self.request.uri) - self.rsession_cmd.append('--user-identity=' + self.current_user) + cmd = self.rsession_cmd + [ + '--user-identity=' + self.current_user, + '--www-port=' + self.rsession_port + ] + server_env = os.environ.copy() # Seed RStudio's R and RSTUDIO variables @@ -55,7 +58,7 @@ def post(self): server_env[env_var] = self.rsession_paths[env_var] + path # Runs rsession in background since we do not need stdout/stderr - self.proc = sp.Popen(self.rsession_cmd, env=server_env) + self.proc = sp.Popen(cmd, env=server_env) if self.proc.poll() == 0: raise web.HTTPError(reason='rsession terminated', status_code=500) From 604784e479abda5bce3b513e36fda095fb8de9d4 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 00:36:43 -0800 Subject: [PATCH 009/125] Fix port format. --- nbrsessionproxy/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 3bcc5d48..46f915f0 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -43,7 +43,7 @@ def post(self): cmd = self.rsession_cmd + [ '--user-identity=' + self.current_user, - '--www-port=' + self.rsession_port + '--www-port=' + str(self.rsession_port) ] server_env = os.environ.copy() From cabfb9775b14ac954a79339dc442987f81b1d2c3 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 00:42:44 -0800 Subject: [PATCH 010/125] Simplify error handling. --- nbrsessionproxy/handlers.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 46f915f0..da8f2d1a 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -74,9 +74,7 @@ def post(self): @web.authenticated def get(self): if not self.proc: - self.set_status(500) - self.write('rsession not yet started') - self.finish() + raise web.HTTPError(reason='rsession not yet started', status_code=500) self.finish(self.proc.poll()) def delete(self): From a0b6734ef7fe1afc3d45c01758c7f9a81743a550 Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Fri, 9 Dec 2016 11:02:35 -0800 Subject: [PATCH 011/125] Remove redundant option. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc719b84..66c2e680 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ jupyter nbextension enable --py nbrsessionproxy Install the extensions for all users on the system: ``` pip install git+https://github.com/ryanlovett/nbrsessionproxy -jupyter serverextension enable --py --sys-prefix --system nbrsessionproxy -jupyter nbextension install --py --sys-prefix --system nbrsessionproxy -jupyter nbextension enable --py --sys-prefix --system nbrsessionproxy +jupyter serverextension enable --py --sys-prefix nbrsessionproxy +jupyter nbextension install --py --sys-prefix nbrsessionproxy +jupyter nbextension enable --py --sys-prefix nbrsessionproxy ``` From 2f69dbaaeff36a9fbc741192c7bd54d984345be2 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 16:56:54 -0800 Subject: [PATCH 012/125] Adjust whitespace. --- nbrsessionproxy/static/main.js | 79 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/nbrsessionproxy/static/main.js b/nbrsessionproxy/static/main.js index e9b318b9..255afff3 100644 --- a/nbrsessionproxy/static/main.js +++ b/nbrsessionproxy/static/main.js @@ -1,65 +1,64 @@ define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, utils) { - var base_url = utils.get_body_data('baseUrl'); + var base_url = utils.get_body_data('baseUrl'); - function open_rsession(data) { - console.log("response: " + data); - var proxy_url; - if ("url" in data) { - proxy_url = data['url']; - } else { - /* debug hack */ - proxy_url = base_url + 'proxy/8001/'; - } - var w = window.open(proxy_url, "_blank"); - w.focus(); - } + function open_rsession(data) { + console.log("response: " + data); + var proxy_url; + if ("url" in data) { + proxy_url = data['url']; + } else { + /* debug hack */ + proxy_url = base_url + 'proxy/8001/'; + } + var w = window.open(proxy_url, "_blank"); + w.focus(); + } function load() { - console.log("nbrsessionproxy loading"); + console.log("nbrsessionproxy loading"); if (!namespace.notebook_list) return; - /* the url we POST to to start rsession */ + /* the url we POST to to start rsession */ var rsp_url = base_url + 'rsessionproxy'; console.log("nbrsessionproxy: url: " + rsp_url); - /* locate the right-side dropdown menu of apps and notebooks */ - var menu = $('.tree-buttons').find('.dropdown-menu'); + /* locate the right-side dropdown menu of apps and notebooks */ + var menu = $('.tree-buttons').find('.dropdown-menu'); - /* create a divider */ - var divider = $('
  • ') - .attr('role', 'presentation') - .addClass('divider'); + /* create a divider */ + var divider = $('
  • ') + .attr('role', 'presentation') + .addClass('divider'); - /* add the divider */ - menu.append(divider); + /* add the divider */ + menu.append(divider); - /* create our list item */ - var rsession_item = $('
  • ') + /* create our list item */ + var rsession_item = $('
  • ') .attr('role', 'presentation') .addClass('new-rsessionproxy'); - /* create our list item's link */ - var rsession_link = $('') - .attr('role', 'menuitem') - .attr('tabindex', '-1') - .text('RStudio Session') - .on('click', function() { - $.post(rsp_url, {}, open_rsession); - }); + /* create our list item's link */ + var rsession_link = $('') + .attr('role', 'menuitem') + .attr('tabindex', '-1') + .text('RStudio Session') + .on('click', function() { + $.post(rsp_url, {}, open_rsession); + }); - /* add the link to the item and + /* add the link to the item and * the item to the menu */ - rsession_item.append(rsession_link); - menu.append(rsession_item); + rsession_item.append(rsession_link); + menu.append(rsession_item); } - var load_ipython_extension = function () { - load(); - } + var load_ipython_extension = function () { + load(); + }; return { load_ipython_extension: load_ipython_extension, }; - }); From 422dcb282e3daf1eeecafe05f2748de27303e257 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 17:12:45 -0800 Subject: [PATCH 013/125] Use friendlier syntax. --- nbrsessionproxy/static/main.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nbrsessionproxy/static/main.js b/nbrsessionproxy/static/main.js index 255afff3..63d2983d 100644 --- a/nbrsessionproxy/static/main.js +++ b/nbrsessionproxy/static/main.js @@ -1,4 +1,7 @@ -define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, utils) { +define(function(require) { + var $ = require('jquery'); + var Jupyter = require('base/js/namespace'); + var utils = require('base/js/utils'); var base_url = utils.get_body_data('baseUrl'); @@ -17,7 +20,7 @@ define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, function load() { console.log("nbrsessionproxy loading"); - if (!namespace.notebook_list) return; + if (!Jupyter.notebook_list) return; /* the url we POST to to start rsession */ var rsp_url = base_url + 'rsessionproxy'; @@ -54,11 +57,7 @@ define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, menu.append(rsession_item); } - var load_ipython_extension = function () { - load(); - }; - return { - load_ipython_extension: load_ipython_extension, + load_ipython_extension: load }; }); From f921db5e0b822eb4833895385b2c59e48c7b7997 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 17:17:58 -0800 Subject: [PATCH 014/125] We extend the tree. --- nbrsessionproxy/__init__.py | 2 +- nbrsessionproxy/static/{main.js => tree.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename nbrsessionproxy/static/{main.js => tree.js} (100%) diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index 40633450..b2163f37 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -11,7 +11,7 @@ def _jupyter_nbextension_paths(): "section": "notebook", "dest": "nbrsessionproxy", "src": "static", - "require": "nbrsessionproxy/main" + "require": "nbrsessionproxy/tree" }] def load_jupyter_server_extension(nbapp): diff --git a/nbrsessionproxy/static/main.js b/nbrsessionproxy/static/tree.js similarity index 100% rename from nbrsessionproxy/static/main.js rename to nbrsessionproxy/static/tree.js From 78cf69848b20c5ba3e9dc96f628021ed1041de70 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 17:21:26 -0800 Subject: [PATCH 015/125] Specify tree section. --- nbrsessionproxy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index b2163f37..f940aaab 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -8,7 +8,7 @@ def _jupyter_server_extension_paths(): def _jupyter_nbextension_paths(): return [{ - "section": "notebook", + "section": "tree", "dest": "nbrsessionproxy", "src": "static", "require": "nbrsessionproxy/tree" From ebbb6a9f33fc701b49074bada33a1511e83c0f47 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 17:28:30 -0800 Subject: [PATCH 016/125] Specify dataType to post callback. --- nbrsessionproxy/static/tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index 63d2983d..1f57c068 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -48,7 +48,7 @@ define(function(require) { .attr('tabindex', '-1') .text('RStudio Session') .on('click', function() { - $.post(rsp_url, {}, open_rsession); + $.post(rsp_url, {}, open_rsession, 'json'); }); /* add the link to the item and From 7354553331df07fa00e7dba32a04ecf1556a7816 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 9 Dec 2016 17:30:22 -0800 Subject: [PATCH 017/125] Add href to our link. --- nbrsessionproxy/static/tree.js | 1 + 1 file changed, 1 insertion(+) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index 1f57c068..a1177a65 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -46,6 +46,7 @@ define(function(require) { var rsession_link = $('') .attr('role', 'menuitem') .attr('tabindex', '-1') + .attr('href', '#') .text('RStudio Session') .on('click', function() { $.post(rsp_url, {}, open_rsession, 'json'); From 1189dc9c0e0f1697ba2bfcf1a5f29c5ebabaface Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 8 Dec 2016 23:17:34 -0800 Subject: [PATCH 018/125] Add .gitignore. Do not assume empty environment paths. Use get_current_user. Specify user-identity once. Fix port format. Simplify error handling. Remove redundant option. Adjust whitespace. Use friendlier syntax. We extend the tree. Specify tree section. Specify dataType to post callback. Add href to our link. --- .gitignore | 1 + README.md | 6 ++-- nbrsessionproxy/__init__.py | 4 +-- nbrsessionproxy/handlers.py | 30 +++++++++------- nbrsessionproxy/static/main.js | 65 ---------------------------------- nbrsessionproxy/static/tree.js | 64 +++++++++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 83 deletions(-) create mode 100644 .gitignore delete mode 100644 nbrsessionproxy/static/main.js create mode 100644 nbrsessionproxy/static/tree.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b399da28 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nbrsessionproxy/__pycache__ diff --git a/README.md b/README.md index cc719b84..66c2e680 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ jupyter nbextension enable --py nbrsessionproxy Install the extensions for all users on the system: ``` pip install git+https://github.com/ryanlovett/nbrsessionproxy -jupyter serverextension enable --py --sys-prefix --system nbrsessionproxy -jupyter nbextension install --py --sys-prefix --system nbrsessionproxy -jupyter nbextension enable --py --sys-prefix --system nbrsessionproxy +jupyter serverextension enable --py --sys-prefix nbrsessionproxy +jupyter nbextension install --py --sys-prefix nbrsessionproxy +jupyter nbextension enable --py --sys-prefix nbrsessionproxy ``` diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index 40633450..f940aaab 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -8,10 +8,10 @@ def _jupyter_server_extension_paths(): def _jupyter_nbextension_paths(): return [{ - "section": "notebook", + "section": "tree", "dest": "nbrsessionproxy", "src": "static", - "require": "nbrsessionproxy/main" + "require": "nbrsessionproxy/tree" }] def load_jupyter_server_extension(nbapp): diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index ccb8cd0a..da8f2d1a 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -12,9 +12,11 @@ class RSessionProxyHandler(IPythonHandler): - rsession_port = 8005 - rsession_path = '/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin' - rsession_ld_lib_path = '/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + rsession_port = 8787 + rsession_paths = { + 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', + 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + } rsession_env = { 'R_DOC_DIR':'/usr/share/R/doc', @@ -31,8 +33,6 @@ class RSessionProxyHandler(IPythonHandler): '--standalone=1', '--program-mode=server', '--log-stderr=1', - '--www-port={}'.format(rsession_port), - '--user-identity={}'.format(os.environ.get('USER', '')), ] proc = None @@ -41,18 +41,24 @@ class RSessionProxyHandler(IPythonHandler): def post(self): logger.info('%s request to %s', self.request.method, self.request.uri) + cmd = self.rsession_cmd + [ + '--user-identity=' + self.current_user, + '--www-port=' + str(self.rsession_port) + ] + server_env = os.environ.copy() # Seed RStudio's R and RSTUDIO variables server_env.update(self.rsession_env) - # Prepend RStudio's PATH and LD_LIBRARY_PATH - server_env['PATH'] = self.rsession_path + ':' + server_env['PATH'] - server_env['LD_LIBRARY_PATH'] = \ - self.rsession_ld_lib_path + ':' + server_env['LD_LIBRARY_PATH'] + # Prepend RStudio's requisite paths + for env_var in self.rsession_paths.keys(): + path = server_env.get(env_var, '') + if path != '': path = ':' + path + server_env[env_var] = self.rsession_paths[env_var] + path # Runs rsession in background since we do not need stdout/stderr - self.proc = sp.Popen(self.rsession_cmd, env=server_env) + self.proc = sp.Popen(cmd, env=server_env) if self.proc.poll() == 0: raise web.HTTPError(reason='rsession terminated', status_code=500) @@ -68,9 +74,7 @@ def post(self): @web.authenticated def get(self): if not self.proc: - self.set_status(500) - self.write('rsession not yet started') - self.finish() + raise web.HTTPError(reason='rsession not yet started', status_code=500) self.finish(self.proc.poll()) def delete(self): diff --git a/nbrsessionproxy/static/main.js b/nbrsessionproxy/static/main.js deleted file mode 100644 index e9b318b9..00000000 --- a/nbrsessionproxy/static/main.js +++ /dev/null @@ -1,65 +0,0 @@ -define(['jquery', 'base/js/namespace', 'base/js/utils'], function($, namespace, utils) { - - var base_url = utils.get_body_data('baseUrl'); - - function open_rsession(data) { - console.log("response: " + data); - var proxy_url; - if ("url" in data) { - proxy_url = data['url']; - } else { - /* debug hack */ - proxy_url = base_url + 'proxy/8001/'; - } - var w = window.open(proxy_url, "_blank"); - w.focus(); - } - - function load() { - console.log("nbrsessionproxy loading"); - if (!namespace.notebook_list) return; - - /* the url we POST to to start rsession */ - var rsp_url = base_url + 'rsessionproxy'; - console.log("nbrsessionproxy: url: " + rsp_url); - - /* locate the right-side dropdown menu of apps and notebooks */ - var menu = $('.tree-buttons').find('.dropdown-menu'); - - /* create a divider */ - var divider = $('
  • ') - .attr('role', 'presentation') - .addClass('divider'); - - /* add the divider */ - menu.append(divider); - - /* create our list item */ - var rsession_item = $('
  • ') - .attr('role', 'presentation') - .addClass('new-rsessionproxy'); - - /* create our list item's link */ - var rsession_link = $('') - .attr('role', 'menuitem') - .attr('tabindex', '-1') - .text('RStudio Session') - .on('click', function() { - $.post(rsp_url, {}, open_rsession); - }); - - /* add the link to the item and - * the item to the menu */ - rsession_item.append(rsession_link); - menu.append(rsession_item); - } - - var load_ipython_extension = function () { - load(); - } - - return { - load_ipython_extension: load_ipython_extension, - }; - -}); diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js new file mode 100644 index 00000000..a1177a65 --- /dev/null +++ b/nbrsessionproxy/static/tree.js @@ -0,0 +1,64 @@ +define(function(require) { + var $ = require('jquery'); + var Jupyter = require('base/js/namespace'); + var utils = require('base/js/utils'); + + var base_url = utils.get_body_data('baseUrl'); + + function open_rsession(data) { + console.log("response: " + data); + var proxy_url; + if ("url" in data) { + proxy_url = data['url']; + } else { + /* debug hack */ + proxy_url = base_url + 'proxy/8001/'; + } + var w = window.open(proxy_url, "_blank"); + w.focus(); + } + + function load() { + console.log("nbrsessionproxy loading"); + if (!Jupyter.notebook_list) return; + + /* the url we POST to to start rsession */ + var rsp_url = base_url + 'rsessionproxy'; + console.log("nbrsessionproxy: url: " + rsp_url); + + /* locate the right-side dropdown menu of apps and notebooks */ + var menu = $('.tree-buttons').find('.dropdown-menu'); + + /* create a divider */ + var divider = $('
  • ') + .attr('role', 'presentation') + .addClass('divider'); + + /* add the divider */ + menu.append(divider); + + /* create our list item */ + var rsession_item = $('
  • ') + .attr('role', 'presentation') + .addClass('new-rsessionproxy'); + + /* create our list item's link */ + var rsession_link = $('') + .attr('role', 'menuitem') + .attr('tabindex', '-1') + .attr('href', '#') + .text('RStudio Session') + .on('click', function() { + $.post(rsp_url, {}, open_rsession, 'json'); + }); + + /* add the link to the item and + * the item to the menu */ + rsession_item.append(rsession_link); + menu.append(rsession_item); + } + + return { + load_ipython_extension: load + }; +}); From 4af28cb747473c510af0db7590d005fd34c723f9 Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Fri, 9 Dec 2016 18:06:41 -0800 Subject: [PATCH 019/125] Add screenshot. --- screenshot.png | Bin 0 -> 25448 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshot.png diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7d336a17be1f096f0726e472140a8a320c9fbb GIT binary patch literal 25448 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 SC+m6Ou#%j*Y^AhC$bSGyB((?t literal 0 HcmV?d00001 From 36f0eb13d95131512eaa0dbb5f4f5de9e691df1e Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Fri, 9 Dec 2016 18:08:25 -0800 Subject: [PATCH 020/125] Add screenshot. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 66c2e680..284d379d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # nbrsessionproxy -Jupyter extensions to proxy RStudio's rsession. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy). +![Screenshot](screenshot.png) + +Jupyter extensions to proxy RStudio's rsession. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy) and currently assumes an Ubuntu environment. ## Installation Install the library: From 048c9ca8ec21d437890ca0f205911dc3289613a9 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 12 Dec 2016 10:27:28 -0800 Subject: [PATCH 021/125] Use a random port. --- nbrsessionproxy/handlers.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index da8f2d1a..68779c92 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,6 +1,7 @@ import os import json import logging +import socket import subprocess as sp from tornado import web @@ -10,9 +11,17 @@ logger = logging.getLogger('nbrsessionproxy') +# from jupyterhub.utils +def random_port(): + """get a single random port""" + sock = socket.socket() + sock.bind(('', 0)) + port = sock.getsockname()[1] + sock.close() + return port + class RSessionProxyHandler(IPythonHandler): - rsession_port = 8787 rsession_paths = { 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' @@ -35,6 +44,8 @@ class RSessionProxyHandler(IPythonHandler): '--log-stderr=1', ] + port = random_port() + proc = None @web.authenticated @@ -43,7 +54,7 @@ def post(self): cmd = self.rsession_cmd + [ '--user-identity=' + self.current_user, - '--www-port=' + str(self.rsession_port) + '--www-port=' + str(self.port) ] server_env = os.environ.copy() @@ -66,7 +77,7 @@ def post(self): response = { 'pid':self.proc.pid, - 'url':'{}proxy/{}/'.format(self.base_url, self.rsession_port), + 'url':'{}proxy/{}/'.format(self.base_url, self.port), } self.finish(json.dumps(response)) From a9d252d774b56083130dedc6340787c2cc179bce Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 12 Dec 2016 10:37:10 -0800 Subject: [PATCH 022/125] Use traitlets. Unset session timeout. --- nbrsessionproxy/handlers.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 68779c92..c4621d30 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -6,6 +6,8 @@ from tornado import web +from traitlets import List, Dict + from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler @@ -21,13 +23,17 @@ def random_port(): return port class RSessionProxyHandler(IPythonHandler): + '''Manage an RStudio rsession instance.''' + + # rsession's environment will vary depending on how it was compiled. + # Configure the env and cmd as required; values here work on Ubuntu. - rsession_paths = { + rsession_paths = Dict({ 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' - } + }) - rsession_env = { + rsession_env = Dict({ 'R_DOC_DIR':'/usr/share/R/doc', 'R_HOME':'/usr/lib/R', 'R_INCLUDE_DIR':'/usr/share/R/include', @@ -36,13 +42,16 @@ class RSessionProxyHandler(IPythonHandler): 'RSTUDIO_DEFAULT_R_VERSION_HOME':'/usr/lib/R', 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', 'RSTUDIO_MINIMUM_USER_ID':'500', - } - rsession_cmd = [ + }) + + # This command will be added to later on POST + rsession_cmd = List([ '/usr/lib/rstudio-server/bin/rsession', '--standalone=1', '--program-mode=server', '--log-stderr=1', - ] + '--session-timeout-minutes=0', + ]) port = random_port() From 204cd2ffce680de52a886be277dd89a6c7b0f6bc Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 12 Dec 2016 16:42:00 -0800 Subject: [PATCH 023/125] Preserve process information. --- nbrsessionproxy/handlers.py | 75 +++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index c4621d30..66b5be0a 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,18 +1,16 @@ import os import json -import logging import socket import subprocess as sp from tornado import web from traitlets import List, Dict +from traitlets.config import Configurable from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler -logger = logging.getLogger('nbrsessionproxy') - # from jupyterhub.utils def random_port(): """get a single random port""" @@ -22,18 +20,20 @@ def random_port(): sock.close() return port -class RSessionProxyHandler(IPythonHandler): - '''Manage an RStudio rsession instance.''' +# Data shared between handler requests +state_data = dict() + +class RSessionContext(Configurable): # rsession's environment will vary depending on how it was compiled. # Configure the env and cmd as required; values here work on Ubuntu. - rsession_paths = Dict({ + paths = Dict({ 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' - }) + }, help="Executable and dynamic linker paths required by rsession.") - rsession_env = Dict({ + env = Dict({ 'R_DOC_DIR':'/usr/share/R/doc', 'R_HOME':'/usr/lib/R', 'R_INCLUDE_DIR':'/usr/share/R/include', @@ -42,72 +42,83 @@ class RSessionProxyHandler(IPythonHandler): 'RSTUDIO_DEFAULT_R_VERSION_HOME':'/usr/lib/R', 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', 'RSTUDIO_MINIMUM_USER_ID':'500', - }) + }, help="R and RStudio environment variables required by rsession.") - # This command will be added to later on POST - rsession_cmd = List([ + cmd = List([ '/usr/lib/rstudio-server/bin/rsession', '--standalone=1', '--program-mode=server', '--log-stderr=1', '--session-timeout-minutes=0', - ]) + ], help="rsession command. Augmented with user-identity and www-port") - port = random_port() +class RSessionProxyHandler(IPythonHandler): + '''Manage an RStudio rsession instance.''' + + rsession_context = RSessionContext() - proc = None + def initialize(self, state): + self.state = state @web.authenticated def post(self): - logger.info('%s request to %s', self.request.method, self.request.uri) + self.log.debug('%s request to %s', self.request.method, self.request.uri) + + port = random_port() - cmd = self.rsession_cmd + [ + cmd = self.rsession_context.cmd + [ '--user-identity=' + self.current_user, - '--www-port=' + str(self.port) + '--www-port=' + str(port) ] server_env = os.environ.copy() # Seed RStudio's R and RSTUDIO variables - server_env.update(self.rsession_env) + server_env.update(self.rsession_context.env) # Prepend RStudio's requisite paths - for env_var in self.rsession_paths.keys(): + for env_var in self.rsession_context.paths.keys(): path = server_env.get(env_var, '') if path != '': path = ':' + path - server_env[env_var] = self.rsession_paths[env_var] + path + server_env[env_var] = self.rsession_context.paths[env_var] + path # Runs rsession in background since we do not need stdout/stderr - self.proc = sp.Popen(cmd, env=server_env) + proc = sp.Popen(cmd, env=server_env) - if self.proc.poll() == 0: + if proc.poll() == 0: raise web.HTTPError(reason='rsession terminated', status_code=500) self.finish() response = { - 'pid':self.proc.pid, - 'url':'{}proxy/{}/'.format(self.base_url, self.port), + 'pid':proc.pid, + 'url':'{}proxy/{}/'.format(self.base_url, port), } + # Store our process + self.state['proc'] = proc + self.finish(json.dumps(response)) @web.authenticated def get(self): - if not self.proc: - raise web.HTTPError(reason='rsession not yet started', status_code=500) - self.finish(self.proc.poll()) + if 'proc' not in self.state: + raise web.HTTPError(reason='no rsession running', status_code=500) + proc = self.state['proc'] + self.finish(str(proc.pid)) + @web.authenticated def delete(self): - logger.info('%s request to %s', self.request.method, self.request.uri) - self.proc.kill() - self.finish(self.proc.poll()) + if 'proc' not in self.state: + raise web.HTTPError(reason='no rsession running', status_code=500) + proc = self.state['proc'] + proc.kill() + self.finish() def setup_handlers(web_app): host_pattern = '.*$' route_pattern = ujoin(web_app.settings['base_url'], '/rsessionproxy/?') web_app.add_handlers(host_pattern, [ - (route_pattern, RSessionProxyHandler) + (route_pattern, RSessionProxyHandler, dict(state=state_data)) ]) - logger.info('Added handler for route %s', route_pattern) # vim: set et ts=4 sw=4: From 3b9e77e8274b37e01ed1b35341bdffa7f1060642 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 12 Dec 2016 17:53:08 -0800 Subject: [PATCH 024/125] Notice when process is still running. --- nbrsessionproxy/handlers.py | 56 ++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 66b5be0a..da05250e 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -60,10 +60,54 @@ class RSessionProxyHandler(IPythonHandler): def initialize(self, state): self.state = state + def gen_response(self, proc, port): + response = { + 'pid': proc.pid, + 'url':'{}proxy/{}/'.format(self.base_url, port), + } + return response + + def still_running(self): + '''Check if our proxied process is still running.''' + + if 'proc' not in self.state: + return False + + # Check if the process is still around + proc = self.state['proc'] + if proc.poll() == 0: + del(self.state['proc']) + self.log.debug('Cannot poll on process.') + return False + + # Check if it is still bound to the port + port = self.state['port'] + sock = socket.socket() + try: + self.log.debug('Binding on port {}.'.format(port)) + sock.bind(('', port)) + except OSError as e: + self.log.debug('Bind error: {}'.format(str(e))) + return True + else: + sock.close() + del(self.state['port']) + + return False + @web.authenticated def post(self): - self.log.debug('%s request to %s', self.request.method, self.request.uri) + '''Start a new rsession.''' + + if self.still_running(): + proc = self.state['proc'] + port = self.state['port'] + self.log.info('Resuming process on port {}'.format(port)) + response = self.gen_response(proc, port) + self.finish(json.dumps(response)) + return + self.log.debug('No existing process') port = random_port() cmd = self.rsession_context.cmd + [ @@ -73,7 +117,7 @@ def post(self): server_env = os.environ.copy() - # Seed RStudio's R and RSTUDIO variables + # Seed RStudio's R and RSTUDIO env variables server_env.update(self.rsession_context.env) # Prepend RStudio's requisite paths @@ -82,20 +126,18 @@ def post(self): if path != '': path = ':' + path server_env[env_var] = self.rsession_context.paths[env_var] + path - # Runs rsession in background since we do not need stdout/stderr + # Runs rsession in background proc = sp.Popen(cmd, env=server_env) if proc.poll() == 0: raise web.HTTPError(reason='rsession terminated', status_code=500) self.finish() - response = { - 'pid':proc.pid, - 'url':'{}proxy/{}/'.format(self.base_url, port), - } + response = self.gen_response(proc, port) # Store our process self.state['proc'] = proc + self.state['port'] = port self.finish(json.dumps(response)) From ff6dce181ef5e8e373f418d10d301901311df05c Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Tue, 13 Dec 2016 13:13:06 -0800 Subject: [PATCH 025/125] Add link to RStudio Server Pro. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 284d379d..e8dd8786 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # nbrsessionproxy ![Screenshot](screenshot.png) -Jupyter extensions to proxy RStudio's rsession. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy) and currently assumes an Ubuntu environment. +Jupyter server and notebook extensions to proxy RStudio's rsession. This is useful if you have deployed JupyterHub and would like to take advantage of its existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy). + +Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter. ## Installation Install the library: From 8b932b67beac0955171a709f8c3bdee7025f3d72 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Wed, 11 Jan 2017 17:18:44 -0800 Subject: [PATCH 026/125] Add method to retrieve rsession's client id. --- nbrsessionproxy/handlers.py | 85 ++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 15 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index da05250e..f4136a0b 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -60,14 +60,51 @@ class RSessionProxyHandler(IPythonHandler): def initialize(self, state): self.state = state - def gen_response(self, proc, port): + def rsession_uri(self): + return '{}proxy/{}/'.format(self.base_url, self.state['port']) + + def gen_response(self, proc): response = { 'pid': proc.pid, - 'url':'{}proxy/{}/'.format(self.base_url, port), + 'url':self.rsession_uri(), } return response - def still_running(self): + def get_client_id(self): + '''Returns either None or the value of 'active-client-id' from + ~/.rstudio/session-persistent-state.''' + + client_id = None + + # Assume the contents manager is local + #root_dir = self.settings['contents_manager'].root_dir + #root_dir = self.config.FileContentsManager.root_dir + root_dir = os.getcwd() + + self.log.debug('client_id: root_dir: {}'.format(root_dir)) + path = os.path.join(root_dir, '.rstudio', 'session-persistent-state') + if not os.path.exists(path): + self.log.debug('client_id: No such file: {}'.format(path)) + return client_id + + try: + buf = open(path).read() + except Exception as e: + self.log.debug("client_id: could not read {}: {}".format(path, e)) + return client_id + + self.log.debug("client_id: read {} bytes".format(len(buf))) + config_key = 'active-client-id' + for line in buf.split(): + if line.startswith(config_key + '='): + # remove the key, '=', and leading and trailing quotes + client_id = line[len(config_key)+1+1:-1] + self.log.debug('client_id: read: {}'.format(client_id)) + break + + return client_id + + def is_running(self): '''Check if our proxied process is still running.''' if 'proc' not in self.state: @@ -88,22 +125,34 @@ def still_running(self): sock.bind(('', port)) except OSError as e: self.log.debug('Bind error: {}'.format(str(e))) - return True - else: - sock.close() + if e.strerror != 'Address already in use': + return False + + sock.close() del(self.state['port']) - return False + return True + + def is_available(self): + pass + + def rpc(self, path): + clientid = self.get_client_id() + if not clientid: + return False + uri = self.rsession_uri() + + @web.authenticated def post(self): '''Start a new rsession.''' - if self.still_running(): + if self.is_running(): proc = self.state['proc'] port = self.state['port'] self.log.info('Resuming process on port {}'.format(port)) - response = self.gen_response(proc, port) + response = self.gen_response(proc) self.finish(json.dumps(response)) return @@ -133,20 +182,26 @@ def post(self): raise web.HTTPError(reason='rsession terminated', status_code=500) self.finish() - response = self.gen_response(proc, port) - # Store our process self.state['proc'] = proc self.state['port'] = port + response = self.gen_response(proc) + + client_id = self.get_client_id() + self.log.debug('post: client_id: {}'.format(client_id)) self.finish(json.dumps(response)) @web.authenticated def get(self): - if 'proc' not in self.state: - raise web.HTTPError(reason='no rsession running', status_code=500) - proc = self.state['proc'] - self.finish(str(proc.pid)) + if self.is_running(): + proc = self.state['proc'] + port = self.state['port'] + self.log.info('Process exists on port {}'.format(port)) + response = self.gen_response(proc) + self.finish(json.dumps(response)) + return + self.finish(json.dumps({})) @web.authenticated def delete(self): From 80992b3295943360ea7288a991f0662639860900 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Wed, 11 Jan 2017 17:44:17 -0800 Subject: [PATCH 027/125] Send _xsrf cookie with POST. --- nbrsessionproxy/static/tree.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index a1177a65..8468074a 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -5,6 +5,12 @@ define(function(require) { var base_url = utils.get_body_data('baseUrl'); + /* http://www.tornadoweb.org/en/stable/guide/security.html */ + function getCookie(name) { + var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); + return r ? r[1] : undefined; + } + function open_rsession(data) { console.log("response: " + data); var proxy_url; @@ -23,6 +29,7 @@ define(function(require) { if (!Jupyter.notebook_list) return; /* the url we POST to to start rsession */ + var xsrf_cookie = getCookie("_xsrf"); var rsp_url = base_url + 'rsessionproxy'; console.log("nbrsessionproxy: url: " + rsp_url); @@ -49,7 +56,7 @@ define(function(require) { .attr('href', '#') .text('RStudio Session') .on('click', function() { - $.post(rsp_url, {}, open_rsession, 'json'); + $.post(rsp_url, { "_xsrf":xsrf_cookie }, open_rsession, 'json'); }); /* add the link to the item and From 23a7f0c2f037b3e656e424ea248c9053ea80d1e7 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 12 Jan 2017 16:34:31 -0800 Subject: [PATCH 028/125] Send _xsrf cookie. --- nbrsessionproxy/static/tree.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index 8468074a..7d3e7e17 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -3,13 +3,9 @@ define(function(require) { var Jupyter = require('base/js/namespace'); var utils = require('base/js/utils'); - var base_url = utils.get_body_data('baseUrl'); + var ajax = utils.ajax || $.ajax; - /* http://www.tornadoweb.org/en/stable/guide/security.html */ - function getCookie(name) { - var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); - return r ? r[1] : undefined; - } + var base_url = utils.get_body_data('baseUrl'); function open_rsession(data) { console.log("response: " + data); @@ -17,8 +13,8 @@ define(function(require) { if ("url" in data) { proxy_url = data['url']; } else { - /* debug hack */ - proxy_url = base_url + 'proxy/8001/'; + /* FIXME: visit some template */ + return; } var w = window.open(proxy_url, "_blank"); w.focus(); @@ -29,7 +25,6 @@ define(function(require) { if (!Jupyter.notebook_list) return; /* the url we POST to to start rsession */ - var xsrf_cookie = getCookie("_xsrf"); var rsp_url = base_url + 'rsessionproxy'; console.log("nbrsessionproxy: url: " + rsp_url); @@ -49,6 +44,15 @@ define(function(require) { .attr('role', 'presentation') .addClass('new-rsessionproxy'); + /* prepare ajax */ + var settings = { + type: "POST", + data: {}, + dataType: "json", + success: open_rsession, + error : utils.log_ajax_error, + } + /* create our list item's link */ var rsession_link = $('') .attr('role', 'menuitem') @@ -56,7 +60,7 @@ define(function(require) { .attr('href', '#') .text('RStudio Session') .on('click', function() { - $.post(rsp_url, { "_xsrf":xsrf_cookie }, open_rsession, 'json'); + ajax(rsp_url, settings); }); /* add the link to the item and From 8576043ef835b083d07ccf5a3ef39fbb56e4f692 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 12 Jan 2017 17:44:39 -0800 Subject: [PATCH 029/125] Get username from environment. Fall back to jovyan. --- nbrsessionproxy/handlers.py | 4 +++- setup.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index f4136a0b..0351ec74 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -159,8 +159,10 @@ def post(self): self.log.debug('No existing process') port = random_port() + username = os.environ.get('JPY_USER', default='jovyan') + cmd = self.rsession_context.cmd + [ - '--user-identity=' + self.current_user, + '--user-identity=' + username, '--www-port=' + str(port) ] diff --git a/setup.py b/setup.py index bc20282e..12f83678 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.0.1', + version='0.1.1', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From 17d2dcf0047a29c4eaf09c5bfb19ed4af6b63f74 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 12 Jan 2017 23:26:07 -0800 Subject: [PATCH 030/125] Check for missing port. --- nbrsessionproxy/handlers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 0351ec74..d28c55da 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -107,9 +107,8 @@ def get_client_id(self): def is_running(self): '''Check if our proxied process is still running.''' - if 'proc' not in self.state: + if 'proc' not in self.state or 'port' not in self.state: return False - # Check if the process is still around proc = self.state['proc'] if proc.poll() == 0: From 02b3cc6fda9828d148dd00ca799e5e41aea2a195 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 12 Jan 2017 23:43:54 -0800 Subject: [PATCH 031/125] Version 0.1.2. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 12f83678..e6690e58 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.1', + version='0.1.2', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From 5fa12e47b82349378dde8bece6c934b0d6f0d141 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 12 Jan 2017 23:45:33 -0800 Subject: [PATCH 032/125] Version 0.1.3. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e6690e58..d4a44afe 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.2', + version='0.1.3', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From 2008fd25e453daac7a4c504cd20bd97a2fbc205a Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 13 Jan 2017 00:15:49 -0800 Subject: [PATCH 033/125] Do not remove port from state. --- nbrsessionproxy/handlers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index d28c55da..d8ff71d4 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -107,8 +107,11 @@ def get_client_id(self): def is_running(self): '''Check if our proxied process is still running.''' - if 'proc' not in self.state or 'port' not in self.state: + if 'proc' not in self.state: + return False + elif 'port' not in self.state: return False + # Check if the process is still around proc = self.state['proc'] if proc.poll() == 0: @@ -128,7 +131,6 @@ def is_running(self): return False sock.close() - del(self.state['port']) return True From 50ef1ba4de700504a0a946b001934bab85f8ac90 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 13 Jan 2017 00:16:35 -0800 Subject: [PATCH 034/125] Version 0.1.4. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d4a44afe..50e91efb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.3', + version='0.1.4', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From d1813498ec2278fd871e510f06994b33c909ae9f Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 13 Jan 2017 01:24:14 -0800 Subject: [PATCH 035/125] Wait for rsession to start. --- nbrsessionproxy/handlers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index d8ff71d4..b3f1defc 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,6 +1,7 @@ import os import json import socket +import time import subprocess as sp from tornado import web @@ -185,6 +186,18 @@ def post(self): raise web.HTTPError(reason='rsession terminated', status_code=500) self.finish() + # Wait for rsession to be available + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + rsession_attempts = 0 + while rsession_attempts < 5: + try: + sock.connect(('', port)) + break + except socket.error as e: + print('sleeping: {}'.format(e)) + time.sleep(2) + rsession_attempts += 1 + # Store our process self.state['proc'] = proc self.state['port'] = port From 16ca085facde54f6963e80c5122ef143328919aa Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 13 Jan 2017 01:24:45 -0800 Subject: [PATCH 036/125] Version 0.1.5. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 50e91efb..3a812d93 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.4', + version='0.1.5', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From c100541ae36f8165634815616941fcfe7c8450cc Mon Sep 17 00:00:00 2001 From: Ryan Lovett Date: Mon, 16 Jan 2017 20:59:29 -0800 Subject: [PATCH 037/125] Add conda's R library path --- nbrsessionproxy/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index b3f1defc..6f9c99ef 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -31,7 +31,7 @@ class RSessionContext(Configurable): paths = Dict({ 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', - 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server' + 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server:/opt/conda/lib/R/lib' }, help="Executable and dynamic linker paths required by rsession.") env = Dict({ From 3da7639afc824c2f35ea6879af8fcd53ab99bedd Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 16 Jan 2017 21:00:48 -0800 Subject: [PATCH 038/125] Version 0.1.6. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3a812d93..61d29bad 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.5', + version='0.1.6', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From 7fa2421b96fb2a6f46646ef71d24f718405930fa Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Tue, 4 Apr 2017 08:22:53 -0700 Subject: [PATCH 039/125] Update link Update link that was redirecting to itself. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8dd8786..83ffd3e1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # nbrsessionproxy ![Screenshot](screenshot.png) -Jupyter server and notebook extensions to proxy RStudio's rsession. This is useful if you have deployed JupyterHub and would like to take advantage of its existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/ryanlovett/nbrsessionproxy). +Jupyter server and notebook extensions to proxy RStudio's rsession. This is useful if you have deployed JupyterHub and would like to take advantage of its existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter. From 2a0e75248f83f250cda627b4f0ba620b71e56d80 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 13 Apr 2017 17:39:39 -0700 Subject: [PATCH 040/125] Open window right after onClick. Do not behave like a popup window. --- nbrsessionproxy/static/tree.js | 45 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index 7d3e7e17..d04ae245 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -7,27 +7,32 @@ define(function(require) { var base_url = utils.get_body_data('baseUrl'); - function open_rsession(data) { - console.log("response: " + data); - var proxy_url; - if ("url" in data) { - proxy_url = data['url']; - } else { - /* FIXME: visit some template */ - return; + function open_rsession(w) { + /* the url we POST to to start rsession */ + var rsp_url = base_url + 'rsessionproxy'; + + /* prepare ajax */ + var settings = { + type: "POST", + data: {}, + dataType: "json", + success: function(data) { + if (!("url" in data)) { + /* FIXME: visit some template */ + return; + } + w.location = data['url']; + }, + error : utils.log_ajax_error, } - var w = window.open(proxy_url, "_blank"); - w.focus(); + + ajax(rsp_url, settings); } function load() { console.log("nbrsessionproxy loading"); if (!Jupyter.notebook_list) return; - /* the url we POST to to start rsession */ - var rsp_url = base_url + 'rsessionproxy'; - console.log("nbrsessionproxy: url: " + rsp_url); - /* locate the right-side dropdown menu of apps and notebooks */ var menu = $('.tree-buttons').find('.dropdown-menu'); @@ -44,15 +49,6 @@ define(function(require) { .attr('role', 'presentation') .addClass('new-rsessionproxy'); - /* prepare ajax */ - var settings = { - type: "POST", - data: {}, - dataType: "json", - success: open_rsession, - error : utils.log_ajax_error, - } - /* create our list item's link */ var rsession_link = $('') .attr('role', 'menuitem') @@ -60,7 +56,8 @@ define(function(require) { .attr('href', '#') .text('RStudio Session') .on('click', function() { - ajax(rsp_url, settings); + var w = window.open(undefined, Jupyter._target); + open_rsession(w); }); /* add the link to the item and From ce67452a5d8163fa7784f244b5b8d354752adda3 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 13 Apr 2017 17:42:02 -0700 Subject: [PATCH 041/125] Update URL in README. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 83ffd3e1..24b4e792 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-p ## Installation Install the library: ``` -pip install git+https://github.com/ryanlovett/nbrsessionproxy +pip install git+https://github.com/jupyterhub/nbrsessionproxy ``` Install the extensions for the user: @@ -20,7 +20,6 @@ jupyter nbextension enable --py nbrsessionproxy Install the extensions for all users on the system: ``` -pip install git+https://github.com/ryanlovett/nbrsessionproxy jupyter serverextension enable --py --sys-prefix nbrsessionproxy jupyter nbextension install --py --sys-prefix nbrsessionproxy jupyter nbextension enable --py --sys-prefix nbrsessionproxy From 9b0265e51863a34cba09f6874347490f870d9d56 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 13 Apr 2017 17:49:28 -0700 Subject: [PATCH 042/125] Prepare for 0.2.0. --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 61d29bad..edacf1fd 100644 --- a/setup.py +++ b/setup.py @@ -2,11 +2,13 @@ setuptools.setup( name="nbrsessionproxy", - version='0.1.6', + version='0.2.0', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", packages=setuptools.find_packages(), - install_requires=[ 'tornado', 'notebook' ], + keywords=['Jupyter'], + classifiers=['Framework :: Jupyter'], + install_requires=[ 'tornado', 'notebook', 'nbserverproxy' ], package_data={'nbrsessionproxy': ['static/*']}, ) From 974b2824f849fe4602fe5a0d81759d34bf0b3f20 Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 14 Apr 2017 11:24:37 -0700 Subject: [PATCH 043/125] Move brief description before image Minor edits to text. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 24b4e792..c0d5e160 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # nbrsessionproxy + +**nbrsessionproxy** provides Jupyter server and notebook extensions to proxy an RStudio rsession. + ![Screenshot](screenshot.png) -Jupyter server and notebook extensions to proxy RStudio's rsession. This is useful if you have deployed JupyterHub and would like to take advantage of its existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). +If you have a JupyterHub deployment, nbrsessionproxy is useful to take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). -Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter. +Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter's. ## Installation + Install the library: ``` pip install git+https://github.com/jupyterhub/nbrsessionproxy From 093373b0b1e67544b0746e0db37bdc17f6153577 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 17 Apr 2017 19:59:41 -0700 Subject: [PATCH 044/125] Use rstudio desktop. --- nbrsessionproxy/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 6f9c99ef..c2027062 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -46,7 +46,7 @@ class RSessionContext(Configurable): }, help="R and RStudio environment variables required by rsession.") cmd = List([ - '/usr/lib/rstudio-server/bin/rsession', + '/usr/lib/rstudio/bin/rsession', '--standalone=1', '--program-mode=server', '--log-stderr=1', From 7e1a322dacd0e8ee83ac70876de4fc499654fc54 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 21 Apr 2017 13:38:37 -0700 Subject: [PATCH 045/125] Detect R env vars, rather than pre-configure. --- nbrsessionproxy/handlers.py | 78 ++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index c2027062..ca3ba663 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -6,12 +6,34 @@ from tornado import web -from traitlets import List, Dict -from traitlets.config import Configurable - from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler + +def detectR(): + '''Detect R's version, R_HOME, and various other directories that rsession + requires. + + Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp''' + + cmd = ['R', '--slave', '--vanilla', '-e', + 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] + + p = sp.run(cmd, check=True, stdout=sp.PIPE, stderr=sp.PIPE) + if p.returncode != 0: + raise Exception('Error detecting R') + R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ + p.stdout.decode().split(':') + + return { + 'R_DOC_DIR': R_DOC_DIR, + 'R_HOME': R_HOME, + 'R_INCLUDE_DIR': R_INCLUDE_DIR, + 'R_SHARE_DIR': R_SHARE_DIR, + 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, + 'RSTUDIO_DEFAULT_R_VERSION': version, + } + # from jupyterhub.utils def random_port(): """get a single random port""" @@ -24,39 +46,24 @@ def random_port(): # Data shared between handler requests state_data = dict() -class RSessionContext(Configurable): - - # rsession's environment will vary depending on how it was compiled. - # Configure the env and cmd as required; values here work on Ubuntu. - - paths = Dict({ - 'PATH':'/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin', - 'LD_LIBRARY_PATH':'/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server:/opt/conda/lib/R/lib' - }, help="Executable and dynamic linker paths required by rsession.") +class RSessionProxyHandler(IPythonHandler): + '''Manage an RStudio rsession instance.''' - env = Dict({ - 'R_DOC_DIR':'/usr/share/R/doc', - 'R_HOME':'/usr/lib/R', - 'R_INCLUDE_DIR':'/usr/share/R/include', - 'R_SHARE_DIR':'/usr/share/R/share', - 'RSTUDIO_DEFAULT_R_VERSION':'3.3.0', - 'RSTUDIO_DEFAULT_R_VERSION_HOME':'/usr/lib/R', + # R and RStudio environment variables required by rsession. + env = { 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', 'RSTUDIO_MINIMUM_USER_ID':'500', - }, help="R and RStudio environment variables required by rsession.") + } - cmd = List([ - '/usr/lib/rstudio/bin/rsession', + # rsession command. Augmented with user-identity and www-port. + cmd = [ + 'rsession', '--standalone=1', '--program-mode=server', '--log-stderr=1', '--session-timeout-minutes=0', - ], help="rsession command. Augmented with user-identity and www-port") - -class RSessionProxyHandler(IPythonHandler): - '''Manage an RStudio rsession instance.''' + ] - rsession_context = RSessionContext() def initialize(self, state): self.state = state @@ -159,11 +166,11 @@ def post(self): return self.log.debug('No existing process') - port = random_port() username = os.environ.get('JPY_USER', default='jovyan') + port = random_port() - cmd = self.rsession_context.cmd + [ + cmd = self.cmd + [ '--user-identity=' + username, '--www-port=' + str(port) ] @@ -171,13 +178,14 @@ def post(self): server_env = os.environ.copy() # Seed RStudio's R and RSTUDIO env variables - server_env.update(self.rsession_context.env) + server_env.update(self.env) - # Prepend RStudio's requisite paths - for env_var in self.rsession_context.paths.keys(): - path = server_env.get(env_var, '') - if path != '': path = ':' + path - server_env[env_var] = self.rsession_context.paths[env_var] + path + try: + r_vars = detectR() + server_env.update(r_vars) + except: + raise web.HTTPError(reason='could not detect R', status_code=500) + self.finish() # Runs rsession in background proc = sp.Popen(cmd, env=server_env) From 5cc04e79524d9a8adf1429ae93279970a0c558b4 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 21 Apr 2017 13:50:05 -0700 Subject: [PATCH 046/125] Add a Dockerfile. --- Dockerfile | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..bc615eb0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +FROM jupyter/r-notebook + +USER root + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + libapparmor1 \ + libedit2 \ + lsb-release \ + ; + +# You can use rsession from rstudio's desktop package as well. +ENV RSTUDIO_PKG=rstudio-server-1.0.136-amd64.deb + +RUN wget -q http://download2.rstudio.org/${RSTUDIO_PKG} +RUN dpkg -i ${RSTUDIO_PKG} +RUN rm ${RSTUDIO_PKG} + +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +USER $NB_USER + +RUN pip install git+https://github.com/jupyterhub/nbserverproxy.git +RUN jupyter serverextension enable --sys-prefix --py nbserverproxy + +RUN pip install git+https://github.com/jupyterhub/nbrsessionproxy.git@detect_r +RUN jupyter serverextension enable --sys-prefix --py nbrsessionproxy +RUN jupyter nbextension install --sys-prefix --py nbrsessionproxy +RUN jupyter nbextension enable --sys-prefix --py nbrsessionproxy + +# The desktop package uses /usr/lib/rstudio/bin +ENV PATH="${PATH}:/usr/lib/rstudio-server/bin" +ENV LD_LIBRARY_PATH="/usr/lib/R/lib:/lib:/usr/lib/x86_64-linux-gnu:/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/amd64/server:/opt/conda/lib/R/lib" From e0cc72f45b4463ca6b0bd29432f39733fd8f4a27 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 21 Apr 2017 13:56:03 -0700 Subject: [PATCH 047/125] Document Dockerfile. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0d5e160..768aa44a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Screenshot](screenshot.png) -If you have a JupyterHub deployment, nbrsessionproxy is useful to take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). +If you have a JupyterHub deployment, nbrsessionproxy can take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter's. @@ -15,16 +15,18 @@ Install the library: pip install git+https://github.com/jupyterhub/nbrsessionproxy ``` -Install the extensions for the user: +Either install the extensions for the user: ``` jupyter serverextension enable --py nbrsessionproxy jupyter nbextension install --py nbrsessionproxy jupyter nbextension enable --py nbrsessionproxy ``` -Install the extensions for all users on the system: +Or install the extensions for all users on the system: ``` jupyter serverextension enable --py --sys-prefix nbrsessionproxy jupyter nbextension install --py --sys-prefix nbrsessionproxy jupyter nbextension enable --py --sys-prefix nbrsessionproxy ``` + +The Dockerfile contains an example installation on top of [jupyter/r-notebook](https://github.com/jupyter/docker-stacks/tree/master/r-notebook). From 1c74c626195e11ceb3205f4f0b2b41336c8955a9 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 21 Apr 2017 13:56:59 -0700 Subject: [PATCH 048/125] Do not specify git branch. --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index bc615eb0..97db0ae3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ USER $NB_USER RUN pip install git+https://github.com/jupyterhub/nbserverproxy.git RUN jupyter serverextension enable --sys-prefix --py nbserverproxy -RUN pip install git+https://github.com/jupyterhub/nbrsessionproxy.git@detect_r +RUN pip install git+https://github.com/jupyterhub/nbrsessionproxy.git RUN jupyter serverextension enable --sys-prefix --py nbrsessionproxy RUN jupyter nbextension install --sys-prefix --py nbrsessionproxy RUN jupyter nbextension enable --sys-prefix --py nbrsessionproxy From 79f564bb0468fa8ffb133a2668687c7d4e30edd8 Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Fri, 16 Jun 2017 14:37:08 -0700 Subject: [PATCH 049/125] Adding jupyterlab extension --- README.md | 9 + jupyterlab-rsessionproxy/package.json | 52 ++++++ jupyterlab-rsessionproxy/src/index.ts | 166 ++++++++++++++++++ .../style/images/rstudio.svg | 1 + jupyterlab-rsessionproxy/style/index.css | 29 +++ jupyterlab-rsessionproxy/tsconfig.json | 16 ++ 6 files changed, 273 insertions(+) create mode 100644 jupyterlab-rsessionproxy/package.json create mode 100644 jupyterlab-rsessionproxy/src/index.ts create mode 100644 jupyterlab-rsessionproxy/style/images/rstudio.svg create mode 100644 jupyterlab-rsessionproxy/style/index.css create mode 100644 jupyterlab-rsessionproxy/tsconfig.json diff --git a/README.md b/README.md index 768aa44a..d97af7a0 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,13 @@ jupyter nbextension install --py --sys-prefix nbrsessionproxy jupyter nbextension enable --py --sys-prefix nbrsessionproxy ``` +For JupyterLab first clone this repository to a known location and +install from the directory. +``` +git clone https://github.com/jupyterhub/nbserverproxy /opt/nbserverproxy +pip install -e /opt/nbserverproxy +juptyer serverextension enable --py nbrsessionproxy +jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy +``` + The Dockerfile contains an example installation on top of [jupyter/r-notebook](https://github.com/jupyter/docker-stacks/tree/master/r-notebook). diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json new file mode 100644 index 00000000..aa2305e2 --- /dev/null +++ b/jupyterlab-rsessionproxy/package.json @@ -0,0 +1,52 @@ +{ + "name": "@jupyterlab/rsessionproxy-extension", + "version": "0.1.2", + "description": "JupyterLab - RSession Proxy Extension", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "files": [ + "lib/*.d.ts", + "lib/*.js", + "style/images/*.*", + "style/*.css" + ], + "directories": { + "lib": "lib/" + }, + "jupyterlab": { + "extension": true + }, + "dependencies": { + "@jupyterlab/application": "^0.7.0", + "@jupyterlab/apputils": "^0.7.0", + "@jupyterlab/coreutils": "^0.7.0", + "@jupyterlab/launcher": "^0.7.0", + "@jupyterlab/services": "^0.46.0", + "@phosphor/messaging": "^1.2.1", + "@phosphor/widgets": "^1.3.0" + }, + "devDependencies": { + "rimraf": "^2.5.2", + "typescript": "^2.2.1" + }, + "keywords": [ + "jupyter", + "jupyterlab" + ], + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "prepublish": "npm run build", + "watch": "tsc -w" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterhub/nbrsessionproxy.git" + }, + "author": "Project Jupyter", + "license": "BSD-3-Clause", + "bugs": { + "url": "https://github.com/jupyterhub/nbrsessionproxy/issues" + }, + "homepage": "https://github.com/jupyterhub/nbrsessionproxy" +} diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts new file mode 100644 index 00000000..d6342851 --- /dev/null +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -0,0 +1,166 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + ILayoutRestorer, JupyterLab, JupyterLabPlugin +} from '@jupyterlab/application'; + +import { + ICommandPalette/*, IFrame, InstanceTracker*/ +} from '@jupyterlab/apputils'; + +import { + ServerConnection +} from '@jupyterlab/services'; + +import { + ILauncher +} from '@jupyterlab/launcher'; + +import { + Message +} from '@phosphor/messaging'; + +import '../style/index.css'; + +/** + * The command IDs used by the rstudio plugin. + */ +namespace CommandIDs { + export + const launch = 'rsession:launch'; +}; + +/** + * The class name for the rstudio icon + */ +const RSTUDIO_ICON_CLASS = 'jp-RStudioIcon'; + + +/** + * A flag denoting whether the application is loaded over HTTPS. + */ +// const LAB_IS_SECURE = window.location.protocol === 'https:'; + +/** + * The class name added to the help widget. + */ +// const RSESSION_CLASS = 'jp-Rsession'; + +/** + * A list of help resources. + */ + +const RESOURCES = [ + { + text: 'RStudio Session', + url: '/' + } +]; + +RESOURCES.sort((a: any, b: any) => { + return a.text.localeCompare(b.text); +}); + + +/** + * The rsession handler extension. + */ +const plugin: JupyterLabPlugin = { + activate, + id: 'jupyter.extensions.rsessionproxy', + requires: [ICommandPalette, ILayoutRestorer], + optional: [ILauncher], + autoStart: true +}; + + +/** + * Export the plugin as default. + */ +export default plugin; + +/* + * An IFrame the disposes itself when closed. + * + * This is needed to clear the state restoration db when IFrames are closed. + */ +// class ClosableIFrame extends IFrame { +// +// /** +// * Dispose of the IFrame when closing. +// */ +// protected onCloseRequest(msg: Message): void { +// this.dispose(); +// } +// } + + +/** + * Activate the rsession extension. + */ +function activate(app: JupyterLab, palette: ICommandPalette, + restorer: ILayoutRestorer, launcher: ILauncher | null): void { + let counter = 0; + const category = 'RStudio'; + const namespace = 'rsession-proxy'; + const command = CommandIDs.launch; + const { commands, shell } = app; + // const tracker = new InstanceTracker({ namespace }); + + // Handle state restoration. + // restorer.restore(tracker, { + // command, + // args: widget => ({ url: widget.url, text: widget.title.label }), + // name: widget => widget.url + // }); + + /** + * Create a new ClosableIFrame widget. + */ + // function newClosableIFrame(url: string, text: string): ClosableIFrame { + // let iframe = new ClosableIFrame(); + // iframe.addClass(RSESSION_CLASS); + // iframe.title.label = text; + // iframe.title.closable = true; + // iframe.id = `${namespace}-${++counter}`; + // iframe.url = url; + // tracker.add(iframe); + // return iframe; + // } + + commands.addCommand(command, { + label: 'New Rstudio Session', + caption: 'Start a new Rstudio Session', + execute: () => { + // Start up the rserver + let settings = ServerConnection.makeSettings(); + let req = { + url: settings.baseUrl + 'rsessionproxy', + method: 'POST', + data: {} + }; + ServerConnection.makeRequest(req, settings).then(resp => { + console.log("Started RStudio... ", resp.data.url); + window.open(resp.data.url, 'RStudio Session'); + // let iframe = newClosableIFrame(resp.data.url, 'Rstudio Session'); + // shell.addToMainArea(iframe); + // shell.activateById(iframe.id); + }); + } + }); + + // Add a launcher item if the launcher is available. + if (launcher) { + launcher.add({ + displayName: 'RStudio', + iconClass: RSTUDIO_ICON_CLASS, + callback: () => { + return commands.execute(command); + } + }); + } + + palette.addItem({ command, category }); +} + diff --git a/jupyterlab-rsessionproxy/style/images/rstudio.svg b/jupyterlab-rsessionproxy/style/images/rstudio.svg new file mode 100644 index 00000000..3fb496e1 --- /dev/null +++ b/jupyterlab-rsessionproxy/style/images/rstudio.svg @@ -0,0 +1 @@ + diff --git a/jupyterlab-rsessionproxy/style/index.css b/jupyterlab-rsessionproxy/style/index.css new file mode 100644 index 00000000..1a46ba13 --- /dev/null +++ b/jupyterlab-rsessionproxy/style/index.css @@ -0,0 +1,29 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) Jupyter Development Team. +| Distributed under the terms of the Modified BSD License. +|----------------------------------------------------------------------------*/ + + +.jp-RStudioIcon { + background-image: url(images/rstudio.svg); +} +.jp-Rsession { + min-width: 480px; + background: white; +} + + +.jp-Rsession::before { + content: ''; + display: block; + height: var(--jp-toolbar-micro-height); + background: var(--jp-toolbar-background); + border-bottom: 1px solid var(--jp-toolbar-border-color); + box-shadow: var(--jp-toolbar-box-shadow); + z-index: 1; +} + + +.jp-Rsession > iframe { + border: none; +} diff --git a/jupyterlab-rsessionproxy/tsconfig.json b/jupyterlab-rsessionproxy/tsconfig.json new file mode 100644 index 00000000..0a6ecc07 --- /dev/null +++ b/jupyterlab-rsessionproxy/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "declaration": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": false, + "module": "commonjs", + "moduleResolution": "node", + "target": "ES5", + "outDir": "./lib", + "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"], + "types": [] + }, + "include": ["src/*"] +} + From ef04d1c3d0494e7c37ffd4d4a19f4315f56fc1d0 Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Sat, 17 Jun 2017 14:12:46 -0700 Subject: [PATCH 050/125] Fixing typo in readme. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d97af7a0..00f64d7f 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ install from the directory. ``` git clone https://github.com/jupyterhub/nbserverproxy /opt/nbserverproxy pip install -e /opt/nbserverproxy -juptyer serverextension enable --py nbrsessionproxy +jupyter serverextension enable --py nbrsessionproxy jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy ``` From 48f222c9f666eb162868b5fd499c332c85a251f3 Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Wed, 5 Jul 2017 17:07:06 -0700 Subject: [PATCH 051/125] Pinning typescript compiler --- jupyterlab-rsessionproxy/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index aa2305e2..0f4f095c 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -27,7 +27,7 @@ }, "devDependencies": { "rimraf": "^2.5.2", - "typescript": "^2.2.1" + "typescript": "~2.2.0" }, "keywords": [ "jupyter", From 40e25b419736c712b08d932ce6326327f3d7693e Mon Sep 17 00:00:00 2001 From: Carol Willing Date: Fri, 4 Aug 2017 04:16:37 -0700 Subject: [PATCH 052/125] Create CONTRIBUTING.md --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..815a9af1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). From a5d3bb236b29d11a2951f0771f42e1a3e1d06d69 Mon Sep 17 00:00:00 2001 From: Joseph Nelson Date: Sat, 26 Aug 2017 09:45:09 -0400 Subject: [PATCH 053/125] adding license --- LICENSE.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..919c332a --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,35 @@ +BSD 3-Clause License + + + Copyright (c) 2017, Project Jupyter Contributors + All rights reserved. + + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 36e27b325cd1f3e8205e5ae775500855fe60793a Mon Sep 17 00:00:00 2001 From: Fred Mitchell Date: Sat, 26 Aug 2017 10:04:19 -0400 Subject: [PATCH 054/125] Fixes #11 --- LICENSE | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0dc89813 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2017, Project Jupyter Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From b1fb52a5460a700cfec48d29948ca12893ded300 Mon Sep 17 00:00:00 2001 From: iagomez Date: Tue, 24 Oct 2017 06:05:51 -0700 Subject: [PATCH 055/125] Add preqs installation steps to readme Extend README and include installation steps for rstudio and nbserverproxy --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 00f64d7f..b1f6c401 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,27 @@ Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-p ## Installation +### Pre-reqs +#### Install [nbserverproxy](https://github.com/jupyterhub/nbserverproxy) +``` +pip install git+https://github.com/jupyterhub/nbserverproxy +``` + +Either install the nbserverproxy extensions for the user: +``` +jupyter serverextension enable --py nbserverproxy +``` + +Or install the nbserverproxy extensions for all users on the system: +``` +jupyter serverextension enable --py --sys-prefix nbserverproxy +``` +#### Install rstudio +``` +conda install rstudio +``` + +### Install nbrsessionproxy Install the library: ``` pip install git+https://github.com/jupyterhub/nbrsessionproxy From 3e56d87b2b1bdbacf768966356a1265a7b397fed Mon Sep 17 00:00:00 2001 From: iagomez Date: Thu, 26 Oct 2017 10:28:48 -0700 Subject: [PATCH 056/125] Include Rstudio distribution package --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b1f6c401..a9def75f 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,7 @@ Or install the nbserverproxy extensions for all users on the system: jupyter serverextension enable --py --sys-prefix nbserverproxy ``` #### Install rstudio -``` -conda install rstudio -``` +Use conda `conda install rstudio` or [download](https://www.rstudio.com/products/rstudio/download-server/) the corresponding package for your platform ### Install nbrsessionproxy Install the library: From 4224ce32e16171edfae516e21cd0cfa61908d7ef Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 13:15:56 -0700 Subject: [PATCH 057/125] Pick a static port to run on This simplifies the code, and works great in pretty much all situations. Also well known ports are better for firewalling and diagnosis than random ports each time. We do need to make this more configurable however, and also possibly try different ports automatically when the given port is taken. --- nbrsessionproxy/handlers.py | 48 +++++++++---------------------------- 1 file changed, 11 insertions(+), 37 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index ca3ba663..01ffe961 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -34,15 +34,6 @@ def detectR(): 'RSTUDIO_DEFAULT_R_VERSION': version, } -# from jupyterhub.utils -def random_port(): - """get a single random port""" - sock = socket.socket() - sock.bind(('', 0)) - port = sock.getsockname()[1] - sock.close() - return port - # Data shared between handler requests state_data = dict() @@ -67,9 +58,10 @@ class RSessionProxyHandler(IPythonHandler): def initialize(self, state): self.state = state + self.port = 9797 def rsession_uri(self): - return '{}proxy/{}/'.format(self.base_url, self.state['port']) + return '{}proxy/{}/'.format(self.base_url, self.port) def gen_response(self, proc): response = { @@ -77,7 +69,7 @@ def gen_response(self, proc): 'url':self.rsession_uri(), } return response - + def get_client_id(self): '''Returns either None or the value of 'active-client-id' from ~/.rstudio/session-persistent-state.''' @@ -110,15 +102,12 @@ def get_client_id(self): self.log.debug('client_id: read: {}'.format(client_id)) break - return client_id - + return client_id def is_running(self): '''Check if our proxied process is still running.''' if 'proc' not in self.state: return False - elif 'port' not in self.state: - return False # Check if the process is still around proc = self.state['proc'] @@ -128,11 +117,10 @@ def is_running(self): return False # Check if it is still bound to the port - port = self.state['port'] sock = socket.socket() try: - self.log.debug('Binding on port {}.'.format(port)) - sock.bind(('', port)) + self.log.debug('Binding on port {}.'.format(self.port)) + sock.bind(('', self.port)) except OSError as e: self.log.debug('Bind error: {}'.format(str(e))) if e.strerror != 'Address already in use': @@ -142,37 +130,25 @@ def is_running(self): return True - def is_available(self): - pass - def rpc(self, path): - clientid = self.get_client_id() - if not clientid: - return False - - uri = self.rsession_uri() - - @web.authenticated def post(self): '''Start a new rsession.''' if self.is_running(): proc = self.state['proc'] - port = self.state['port'] - self.log.info('Resuming process on port {}'.format(port)) + self.log.info('Resuming process on port {}'.format(self.port)) response = self.gen_response(proc) self.finish(json.dumps(response)) return self.log.debug('No existing process') - username = os.environ.get('JPY_USER', default='jovyan') - port = random_port() + username = os.environ.get('USER', default='jovyan') cmd = self.cmd + [ '--user-identity=' + username, - '--www-port=' + str(port) + '--www-port=' + str(self.port) ] server_env = os.environ.copy() @@ -199,7 +175,7 @@ def post(self): rsession_attempts = 0 while rsession_attempts < 5: try: - sock.connect(('', port)) + sock.connect(('', self.port)) break except socket.error as e: print('sleeping: {}'.format(e)) @@ -208,7 +184,6 @@ def post(self): # Store our process self.state['proc'] = proc - self.state['port'] = port response = self.gen_response(proc) @@ -220,8 +195,7 @@ def post(self): def get(self): if self.is_running(): proc = self.state['proc'] - port = self.state['port'] - self.log.info('Process exists on port {}'.format(port)) + self.log.info('Process exists on port {}'.format(self.port)) response = self.gen_response(proc) self.finish(json.dumps(response)) return From 12a2ef0c154bf653295d0ecacd54ee46ceaef49a Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 13:30:08 -0700 Subject: [PATCH 058/125] Simplify starting rsession - No AJAX needed. Visiting /rsessionproxy now starts rsession & redirects you to appropriate URL - Don't require a POST for starting rsession - a GET is good enough. If it's already started, we redirect user to it. If not, we start it. - Remove DELETE verb handler, since it is never used --- nbrsessionproxy/handlers.py | 70 ++-------------------------------- nbrsessionproxy/static/tree.js | 32 ++-------------- 2 files changed, 6 insertions(+), 96 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 01ffe961..4cbf0689 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -63,46 +63,6 @@ def initialize(self, state): def rsession_uri(self): return '{}proxy/{}/'.format(self.base_url, self.port) - def gen_response(self, proc): - response = { - 'pid': proc.pid, - 'url':self.rsession_uri(), - } - return response - - def get_client_id(self): - '''Returns either None or the value of 'active-client-id' from - ~/.rstudio/session-persistent-state.''' - - client_id = None - - # Assume the contents manager is local - #root_dir = self.settings['contents_manager'].root_dir - #root_dir = self.config.FileContentsManager.root_dir - root_dir = os.getcwd() - - self.log.debug('client_id: root_dir: {}'.format(root_dir)) - path = os.path.join(root_dir, '.rstudio', 'session-persistent-state') - if not os.path.exists(path): - self.log.debug('client_id: No such file: {}'.format(path)) - return client_id - - try: - buf = open(path).read() - except Exception as e: - self.log.debug("client_id: could not read {}: {}".format(path, e)) - return client_id - - self.log.debug("client_id: read {} bytes".format(len(buf))) - config_key = 'active-client-id' - for line in buf.split(): - if line.startswith(config_key + '='): - # remove the key, '=', and leading and trailing quotes - client_id = line[len(config_key)+1+1:-1] - self.log.debug('client_id: read: {}'.format(client_id)) - break - - return client_id def is_running(self): '''Check if our proxied process is still running.''' @@ -132,15 +92,13 @@ def is_running(self): @web.authenticated - def post(self): + def get(self): '''Start a new rsession.''' if self.is_running(): proc = self.state['proc'] self.log.info('Resuming process on port {}'.format(self.port)) - response = self.gen_response(proc) - self.finish(json.dumps(response)) - return + return self.redirect(self.rsession_uri()) self.log.debug('No existing process') @@ -185,29 +143,7 @@ def post(self): # Store our process self.state['proc'] = proc - response = self.gen_response(proc) - - client_id = self.get_client_id() - self.log.debug('post: client_id: {}'.format(client_id)) - self.finish(json.dumps(response)) - - @web.authenticated - def get(self): - if self.is_running(): - proc = self.state['proc'] - self.log.info('Process exists on port {}'.format(self.port)) - response = self.gen_response(proc) - self.finish(json.dumps(response)) - return - self.finish(json.dumps({})) - - @web.authenticated - def delete(self): - if 'proc' not in self.state: - raise web.HTTPError(reason='no rsession running', status_code=500) - proc = self.state['proc'] - proc.kill() - self.finish() + return self.redirect(self.rsession_uri()) def setup_handlers(web_app): host_pattern = '.*$' diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index d04ae245..10243841 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -3,31 +3,8 @@ define(function(require) { var Jupyter = require('base/js/namespace'); var utils = require('base/js/utils'); - var ajax = utils.ajax || $.ajax; - var base_url = utils.get_body_data('baseUrl'); - function open_rsession(w) { - /* the url we POST to to start rsession */ - var rsp_url = base_url + 'rsessionproxy'; - - /* prepare ajax */ - var settings = { - type: "POST", - data: {}, - dataType: "json", - success: function(data) { - if (!("url" in data)) { - /* FIXME: visit some template */ - return; - } - w.location = data['url']; - }, - error : utils.log_ajax_error, - } - - ajax(rsp_url, settings); - } function load() { console.log("nbrsessionproxy loading"); @@ -53,12 +30,9 @@ define(function(require) { var rsession_link = $('') .attr('role', 'menuitem') .attr('tabindex', '-1') - .attr('href', '#') - .text('RStudio Session') - .on('click', function() { - var w = window.open(undefined, Jupyter._target); - open_rsession(w); - }); + .attr('href', base_url + 'rsessionproxy') + .attr('target', '_blank') + .text('RStudio Session'); /* add the link to the item and * the item to the menu */ From fbe8f39f65101fc77a440c65644010c50477b551 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 13:39:06 -0700 Subject: [PATCH 059/125] Use getpass to find out user name Does a bunch of heuristics, including looking in the USER variable. We should not depend on JPY_USER being set, since recent versions of JupyterHub do not set it anymore. --- nbrsessionproxy/handlers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 4cbf0689..153864f0 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,5 +1,6 @@ import os import json +import getpass import socket import time import subprocess as sp @@ -75,7 +76,7 @@ def is_running(self): del(self.state['proc']) self.log.debug('Cannot poll on process.') return False - + # Check if it is still bound to the port sock = socket.socket() try: @@ -102,10 +103,8 @@ def get(self): self.log.debug('No existing process') - username = os.environ.get('USER', default='jovyan') - cmd = self.cmd + [ - '--user-identity=' + username, + '--user-identity=' + getpass.getuser(), '--www-port=' + str(self.port) ] From 8a15e07604082461525d74a77f529b010abbb54e Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 14:18:58 -0700 Subject: [PATCH 060/125] Clean up how we health check if rsession is up - Use a http check rather than binding to the socket. This is more reliable - Do exponential backoff with max of 5s wait between checking for rstudio server uptime - Co-routine-ize more of the code (even though we are still using popen) --- nbrsessionproxy/handlers.py | 58 ++++++++++++++----------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 153864f0..fbe3baf6 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,11 +1,8 @@ import os -import json import getpass -import socket -import time import subprocess as sp -from tornado import web +from tornado import web, gen, httpclient from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler @@ -64,6 +61,7 @@ def initialize(self, state): def rsession_uri(self): return '{}proxy/{}/'.format(self.base_url, self.port) + @gen.coroutine def is_running(self): '''Check if our proxied process is still running.''' @@ -73,32 +71,28 @@ def is_running(self): # Check if the process is still around proc = self.state['proc'] if proc.poll() == 0: - del(self.state['proc']) self.log.debug('Cannot poll on process.') return False - # Check if it is still bound to the port - sock = socket.socket() - try: - self.log.debug('Binding on port {}.'.format(self.port)) - sock.bind(('', self.port)) - except OSError as e: - self.log.debug('Bind error: {}'.format(str(e))) - if e.strerror != 'Address already in use': - return False + client = httpclient.AsyncHTTPClient() + req = httpclient.HTTPRequest('http://localhost:{}'.format(self.port)) - sock.close() + try: + yield client.fetch(req) + self.log.debug('Got positive response from rstudio server') + except: + return False return True + @gen.coroutine @web.authenticated def get(self): '''Start a new rsession.''' - if self.is_running(): - proc = self.state['proc'] - self.log.info('Resuming process on port {}'.format(self.port)) + if (yield self.is_running()): + self.log.info('R process on port {}'.format(self.port)) return self.redirect(self.rsession_uri()) self.log.debug('No existing process') @@ -118,29 +112,21 @@ def get(self): server_env.update(r_vars) except: raise web.HTTPError(reason='could not detect R', status_code=500) - self.finish() # Runs rsession in background proc = sp.Popen(cmd, env=server_env) + self.state['proc'] = proc - if proc.poll() == 0: - raise web.HTTPError(reason='rsession terminated', status_code=500) - self.finish() - - # Wait for rsession to be available - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - rsession_attempts = 0 - while rsession_attempts < 5: - try: - sock.connect(('', self.port)) + for i in range(5): + if (yield self.is_running()): + self.log.info('rsession startup complete') break - except socket.error as e: - print('sleeping: {}'.format(e)) - time.sleep(2) - rsession_attempts += 1 - - # Store our process - self.state['proc'] = proc + # Simple exponential backoff + wait_time = max(1.4 ** i, 5) + self.log.debug('Waiting {} before checking if rstudio is up'.format(wait_time)) + yield gen.sleep(wait_time) + else: + raise web.HTTPError('could not start rsession in time', status_code=500) return self.redirect(self.rsession_uri()) From beb71899314162559b99b31aac1d1054a546d265 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 14:24:04 -0700 Subject: [PATCH 061/125] Simplify setting up shared state a little more --- nbrsessionproxy/handlers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index fbe3baf6..d944787c 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -32,8 +32,6 @@ def detectR(): 'RSTUDIO_DEFAULT_R_VERSION': version, } -# Data shared between handler requests -state_data = dict() class RSessionProxyHandler(IPythonHandler): '''Manage an RStudio rsession instance.''' @@ -131,10 +129,9 @@ def get(self): return self.redirect(self.rsession_uri()) def setup_handlers(web_app): - host_pattern = '.*$' route_pattern = ujoin(web_app.settings['base_url'], '/rsessionproxy/?') - web_app.add_handlers(host_pattern, [ - (route_pattern, RSessionProxyHandler, dict(state=state_data)) + web_app.add_handlers('.*', [ + (route_pattern, RSessionProxyHandler, dict(state={})) ]) # vim: set et ts=4 sw=4: From 8415c602e413863c03276cd2cdd237c0cae1dcd6 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Nov 2017 14:25:43 -0700 Subject: [PATCH 062/125] Call the URL /rstudio rather than /rsessionproxy This is useful for binder, where you can just request that binder start in /rstudio! --- nbrsessionproxy/handlers.py | 2 +- nbrsessionproxy/static/tree.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index d944787c..c25d70a0 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -129,7 +129,7 @@ def get(self): return self.redirect(self.rsession_uri()) def setup_handlers(web_app): - route_pattern = ujoin(web_app.settings['base_url'], '/rsessionproxy/?') + route_pattern = ujoin(web_app.settings['base_url'], '/rstudio/?') web_app.add_handlers('.*', [ (route_pattern, RSessionProxyHandler, dict(state={})) ]) diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index 10243841..a5bbe136 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -7,7 +7,6 @@ define(function(require) { function load() { - console.log("nbrsessionproxy loading"); if (!Jupyter.notebook_list) return; /* locate the right-side dropdown menu of apps and notebooks */ @@ -24,13 +23,13 @@ define(function(require) { /* create our list item */ var rsession_item = $('
  • ') .attr('role', 'presentation') - .addClass('new-rsessionproxy'); + .addClass('new-rstudio'); /* create our list item's link */ var rsession_link = $('') .attr('role', 'menuitem') .attr('tabindex', '-1') - .attr('href', base_url + 'rsessionproxy') + .attr('href', base_url + 'rstudio') .attr('target', '_blank') .text('RStudio Session'); From 11cd9c188f401820bfb1a0a245ebcdf57b6f539d Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Mon, 6 Nov 2017 13:17:24 -0600 Subject: [PATCH 063/125] Make port binding dynamic again This allows easier use by multiple users on the same machine --- nbrsessionproxy/handlers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index c25d70a0..54d52caa 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,5 +1,6 @@ import os import getpass +import socket import subprocess as sp from tornado import web, gen, httpclient @@ -54,7 +55,18 @@ class RSessionProxyHandler(IPythonHandler): def initialize(self, state): self.state = state - self.port = 9797 + + @property + def port(self): + """ + Allocate a random empty port for use by rstudio + """ + if not hasattr(self, '_port'): + sock = socket.socket() + sock.bind(('', 0)) + self._port = sock.getsockname()[1] + sock.close() + return self._port def rsession_uri(self): return '{}proxy/{}/'.format(self.base_url, self.port) From 89c6c4ca0823cb9b7a06243440aed070021c34cf Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 6 Nov 2017 12:54:32 -0800 Subject: [PATCH 064/125] Update to v0.3.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index edacf1fd..1ec272d2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.2.0', + version='0.3.0', url="https://github.com/ryanlovett/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extensions to proxy RStudio's rsession", From f843be2b1965d4007b809004c04dc041e0ef72b2 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 6 Nov 2017 13:01:22 -0800 Subject: [PATCH 065/125] Update repo URL. --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1ec272d2..dfb1f857 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,10 @@ setuptools.setup( name="nbrsessionproxy", - version='0.3.0', - url="https://github.com/ryanlovett/nbrsessionproxy", + version='0.3.1', + url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", - description="Jupyter extensions to proxy RStudio's rsession", + description="Jupyter extension to proxy RStudio's rsession", packages=setuptools.find_packages(), keywords=['Jupyter'], classifiers=['Framework :: Jupyter'], From 23c1f1c53e9f8b22b2a0a3025b9ffd7b4f15f11e Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 8 Nov 2017 20:08:30 -0600 Subject: [PATCH 066/125] Supervise the rsession process, restarting it as required rsession restarts itself to change cwd when switching projects, so we need this! --- nbrsessionproxy/handlers.py | 46 +++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 54d52caa..d8961c1c 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,9 +1,9 @@ import os import getpass import socket -import subprocess as sp +import subprocess -from tornado import web, gen, httpclient +from tornado import web, gen, httpclient, process, ioloop from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler @@ -18,7 +18,7 @@ def detectR(): cmd = ['R', '--slave', '--vanilla', '-e', 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] - p = sp.run(cmd, check=True, stdout=sp.PIPE, stderr=sp.PIPE) + p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if p.returncode != 0: raise Exception('Error detecting R') R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ @@ -80,7 +80,7 @@ def is_running(self): # Check if the process is still around proc = self.state['proc'] - if proc.poll() == 0: + if proc.proc.poll() == 0: self.log.debug('Cannot poll on process.') return False @@ -97,16 +97,10 @@ def is_running(self): @gen.coroutine - @web.authenticated - def get(self): - '''Start a new rsession.''' - - if (yield self.is_running()): - self.log.info('R process on port {}'.format(self.port)) - return self.redirect(self.rsession_uri()) - - self.log.debug('No existing process') - + def start_process(self): + """ + Start the rstudio process + """ cmd = self.cmd + [ '--user-identity=' + getpass.getuser(), '--www-port=' + str(self.port) @@ -123,9 +117,16 @@ def get(self): except: raise web.HTTPError(reason='could not detect R', status_code=500) + def exit_callback(code): + self.log.info('rsession process died with code {}, restarting...'.format(code)) + if code != 0: + ioloop.IOLoop.current().add_callback(self.start_process) + # Runs rsession in background - proc = sp.Popen(cmd, env=server_env) + proc = process.Subprocess(cmd, env=server_env) + self.log.info('Starting rsession process...') self.state['proc'] = proc + proc.set_exit_callback(exit_callback) for i in range(5): if (yield self.is_running()): @@ -138,8 +139,23 @@ def get(self): else: raise web.HTTPError('could not start rsession in time', status_code=500) + + @gen.coroutine + @web.authenticated + def get(self): + '''Start a new rsession.''' + + if (yield self.is_running()): + self.log.info('R process on port {}'.format(self.port)) + return self.redirect(self.rsession_uri()) + + self.log.debug('No existing process') + + yield self.start_process() + return self.redirect(self.rsession_uri()) + def setup_handlers(web_app): route_pattern = ujoin(web_app.settings['base_url'], '/rstudio/?') web_app.add_handlers('.*', [ From 79c364b11e04e76ccf405b40936b1c07f38db0ea Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 8 Nov 2017 21:08:24 -0600 Subject: [PATCH 067/125] Proxy rstudio through /rstudio This provides a number of benefits: - Users never see a 'Connection Refused' error, since we check to make sure the process is up before forwarding them through - Consistent URL rather than exposing internal detail of what port is being used! - Much better experience when switching projects - you get a small message that says 'switching projects', then a spinner then it exists! - Make sure that port is shared across requests, rather than a new one being allocated per request! This worked before by accident :) - Use trailing slash when accessing rstudio (so rstudio/ than rstudio), to make relative URLs used by rstudio work properly. This too worked before by accident - Don't make a HTTP request to check before each GET. This is not necessary, since we know the process is up (since we watch for SIGCHLD and delete the proc key if it is dead) --- nbrsessionproxy/handlers.py | 59 +++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index d8961c1c..42392eb9 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -8,6 +8,8 @@ from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler +from nbserverproxy.handlers import LocalProxyHandler + def detectR(): '''Detect R's version, R_HOME, and various other directories that rsession @@ -34,7 +36,7 @@ def detectR(): } -class RSessionProxyHandler(IPythonHandler): +class RSessionProxyHandler(LocalProxyHandler): '''Manage an RStudio rsession instance.''' # R and RStudio environment variables required by rsession. @@ -61,12 +63,12 @@ def port(self): """ Allocate a random empty port for use by rstudio """ - if not hasattr(self, '_port'): + if 'port' not in self.state: sock = socket.socket() sock.bind(('', 0)) - self._port = sock.getsockname()[1] + self.state['port'] = sock.getsockname()[1] sock.close() - return self._port + return self.state['port'] def rsession_uri(self): return '{}proxy/{}/'.format(self.base_url, self.port) @@ -81,7 +83,7 @@ def is_running(self): # Check if the process is still around proc = self.state['proc'] if proc.proc.poll() == 0: - self.log.debug('Cannot poll on process.') + self.log.info('Cannot poll on process.') return False client = httpclient.AsyncHTTPClient() @@ -91,6 +93,7 @@ def is_running(self): yield client.fetch(req) self.log.debug('Got positive response from rstudio server') except: + self.log.debug('Got negative response from rstudio server') return False return True @@ -117,10 +120,15 @@ def start_process(self): except: raise web.HTTPError(reason='could not detect R', status_code=500) + @gen.coroutine def exit_callback(code): - self.log.info('rsession process died with code {}, restarting...'.format(code)) + """ + Callback when the rsessionproxy dies + """ + self.log.info('rsession process died with code {}'.format(code)) + del self.state['proc'] if code != 0: - ioloop.IOLoop.current().add_callback(self.start_process) + yield self.start_process() # Runs rsession in background proc = process.Subprocess(cmd, env=server_env) @@ -140,24 +148,43 @@ def exit_callback(code): raise web.HTTPError('could not start rsession in time', status_code=500) + @gen.coroutine @web.authenticated - def get(self): - '''Start a new rsession.''' + def proxy(self, port, path): + if not path.startswith('/'): + path = '/' + path + + # FIXME: try to not start multiple processes at a time with some locking here + if 'proc' not in self.state: + self.log.info('No existing process rsession process found') + yield self.start_process() + + return (yield super().proxy(self.port, path)) + + def get(self, path): + return self.proxy(self.port, path) + + def post(self, path): + return self.proxy(self.port, path) - if (yield self.is_running()): - self.log.info('R process on port {}'.format(self.port)) - return self.redirect(self.rsession_uri()) + def put(self, path): + return self.proxy(self.port, path) - self.log.debug('No existing process') + def delete(self, path): + return self.proxy(self.port, path) - yield self.start_process() + def head(self, path): + return self.proxy(self.port, path) - return self.redirect(self.rsession_uri()) + def patch(self, path): + return self.proxy(self.port, path) + def options(self, path): + return self.proxy(self.port, path) def setup_handlers(web_app): - route_pattern = ujoin(web_app.settings['base_url'], '/rstudio/?') + route_pattern = ujoin(web_app.settings['base_url'], 'rstudio/(.*)') web_app.add_handlers('.*', [ (route_pattern, RSessionProxyHandler, dict(state={})) ]) From 96198c02a52adb871c287eafc2f0f308634a9eda Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 8 Nov 2017 21:39:33 -0600 Subject: [PATCH 068/125] Redirect 'rstudio' to 'rstudio/' The trailing slash is required for the relative URLs in rstudio to work properly --- nbrsessionproxy/handlers.py | 12 ++++++++++-- nbrsessionproxy/static/tree.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 42392eb9..4197be24 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -2,6 +2,7 @@ import getpass import socket import subprocess +from urllib.parse import urlunparse, urlparse from tornado import web, gen, httpclient, process, ioloop @@ -35,6 +36,13 @@ def detectR(): 'RSTUDIO_DEFAULT_R_VERSION': version, } +class AddSlashHandler(IPythonHandler): + """Handler for adding trailing slash to URLs that need them""" + @web.authenticated + def get(self, *args): + src = urlparse(self.request.uri) + dest = src._replace(path=src.path + '/') + self.redirect(urlunparse(dest)) class RSessionProxyHandler(LocalProxyHandler): '''Manage an RStudio rsession instance.''' @@ -184,9 +192,9 @@ def options(self, path): return self.proxy(self.port, path) def setup_handlers(web_app): - route_pattern = ujoin(web_app.settings['base_url'], 'rstudio/(.*)') web_app.add_handlers('.*', [ - (route_pattern, RSessionProxyHandler, dict(state={})) + (ujoin(web_app.settings['base_url'], 'rstudio/(.*)'), RSessionProxyHandler, dict(state={})), + (ujoin(web_app.settings['base_url'], 'rstudio'), AddSlashHandler) ]) # vim: set et ts=4 sw=4: diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js index a5bbe136..f1a207a9 100644 --- a/nbrsessionproxy/static/tree.js +++ b/nbrsessionproxy/static/tree.js @@ -29,7 +29,7 @@ define(function(require) { var rsession_link = $('') .attr('role', 'menuitem') .attr('tabindex', '-1') - .attr('href', base_url + 'rstudio') + .attr('href', base_url + 'rstudio/') .attr('target', '_blank') .text('RStudio Session'); From 04b781f49eaa00d1a840859350dade0fc471e087 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 8 Nov 2017 21:41:56 -0600 Subject: [PATCH 069/125] Modify README to not require manually installing nbserverproxy It's in install_requires, and we don't have to enable the serverextension anymore --- README.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/README.md b/README.md index a9def75f..3f6ac8c9 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,7 @@ Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-p ## Installation ### Pre-reqs -#### Install [nbserverproxy](https://github.com/jupyterhub/nbserverproxy) -``` -pip install git+https://github.com/jupyterhub/nbserverproxy -``` -Either install the nbserverproxy extensions for the user: -``` -jupyter serverextension enable --py nbserverproxy -``` - -Or install the nbserverproxy extensions for all users on the system: -``` -jupyter serverextension enable --py --sys-prefix nbserverproxy -``` #### Install rstudio Use conda `conda install rstudio` or [download](https://www.rstudio.com/products/rstudio/download-server/) the corresponding package for your platform From 6d0f853e1cda324a02235e3ecca9a2d6a8c4f5bc Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 8 Nov 2017 23:09:49 -0600 Subject: [PATCH 070/125] Make sure we don't race process creation before proxying Simple state flags are good enough to make sure that if we are in the process of waiting for the rsession process to come up, we do not proxy requests through --- nbrsessionproxy/handlers.py | 91 +++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 4197be24..303ba86f 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -112,41 +112,64 @@ def start_process(self): """ Start the rstudio process """ - cmd = self.cmd + [ - '--user-identity=' + getpass.getuser(), - '--www-port=' + str(self.port) - ] - server_env = os.environ.copy() + self.state['starting'] = True + try: + cmd = self.cmd + [ + '--user-identity=' + getpass.getuser(), + '--www-port=' + str(self.port) + ] + + server_env = os.environ.copy() + + # Seed RStudio's R and RSTUDIO env variables + server_env.update(self.env) + + try: + r_vars = detectR() + server_env.update(r_vars) + except: + raise web.HTTPError(reason='could not detect R', status_code=500) + + @gen.coroutine + def exit_callback(code): + """ + Callback when the rsessionproxy dies + """ + self.log.info('rsession process died with code {}'.format(code)) + del self.state['proc'] + if code != 0: + yield self.start_process() + + # Runs rsession in background + proc = process.Subprocess(cmd, env=server_env) + self.log.info('Starting rsession process...') + self.state['proc'] = proc + proc.set_exit_callback(exit_callback) + + for i in range(5): + if (yield self.is_running()): + self.log.info('rsession startup complete') + break + # Simple exponential backoff + wait_time = max(1.4 ** i, 5) + self.log.debug('Waiting {} before checking if rstudio is up'.format(wait_time)) + yield gen.sleep(wait_time) + else: + raise web.HTTPError('could not start rsession in time', status_code=500) + finally: + self.state['starting'] = False - # Seed RStudio's R and RSTUDIO env variables - server_env.update(self.env) - try: - r_vars = detectR() - server_env.update(r_vars) - except: - raise web.HTTPError(reason='could not detect R', status_code=500) - - @gen.coroutine - def exit_callback(code): - """ - Callback when the rsessionproxy dies - """ - self.log.info('rsession process died with code {}'.format(code)) - del self.state['proc'] - if code != 0: - yield self.start_process() - - # Runs rsession in background - proc = process.Subprocess(cmd, env=server_env) - self.log.info('Starting rsession process...') - self.state['proc'] = proc - proc.set_exit_callback(exit_callback) + @gen.coroutine + @web.authenticated + def proxy(self, port, path): + if not path.startswith('/'): + path = '/' + path + # if we're in 'starting' let's wait a while for i in range(5): - if (yield self.is_running()): - self.log.info('rsession startup complete') + if not self.state.get('starting', False): break # Simple exponential backoff wait_time = max(1.4 ** i, 5) @@ -155,14 +178,6 @@ def exit_callback(code): else: raise web.HTTPError('could not start rsession in time', status_code=500) - - - @gen.coroutine - @web.authenticated - def proxy(self, port, path): - if not path.startswith('/'): - path = '/' + path - # FIXME: try to not start multiple processes at a time with some locking here if 'proc' not in self.state: self.log.info('No existing process rsession process found') From 793f67f9bb14c3b44a4de72443a8ddde6fe5c337 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Thu, 9 Nov 2017 10:55:18 -0800 Subject: [PATCH 071/125] Bump version to v0.4.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dfb1f857..b2893118 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.3.1', + version='0.4.0', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", From 9fce06a904c1fc91c292b9801794290967e1119a Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 9 Nov 2017 15:52:48 -0600 Subject: [PATCH 072/125] Bump version + nbserverproxy required version Also remove tornado as an explicit dependency. It is depended on by notebook --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b2893118..d612b9bc 100644 --- a/setup.py +++ b/setup.py @@ -2,13 +2,16 @@ setuptools.setup( name="nbrsessionproxy", - version='0.4.0', + version='0.4.1', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", packages=setuptools.find_packages(), keywords=['Jupyter'], classifiers=['Framework :: Jupyter'], - install_requires=[ 'tornado', 'notebook', 'nbserverproxy' ], + install_requires=[ + 'notebook', + 'nbserverproxy >= 0.3.2' + ], package_data={'nbrsessionproxy': ['static/*']}, ) From 7102d4c5efa9a30305c1a974c671dc2ba78f33fc Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 2 Dec 2017 13:24:13 -0800 Subject: [PATCH 073/125] Abstract process supervision to nbserverproxy Removes a lot of code and bumps nbserverproxy version! --- nbrsessionproxy/handlers.py | 182 ++++++------------------------------ setup.py | 2 +- 2 files changed, 27 insertions(+), 157 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 303ba86f..faadaba7 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -9,7 +9,7 @@ from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler -from nbserverproxy.handlers import LocalProxyHandler +from nbserverproxy.handlers import SuperviseAndProxyHandler def detectR(): @@ -44,167 +44,37 @@ def get(self, *args): dest = src._replace(path=src.path + '/') self.redirect(urlunparse(dest)) -class RSessionProxyHandler(LocalProxyHandler): + +class RSessionProxyHandler(SuperviseAndProxyHandler): '''Manage an RStudio rsession instance.''' - # R and RStudio environment variables required by rsession. - env = { - 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', - 'RSTUDIO_MINIMUM_USER_ID':'500', - } + name = 'rsession' - # rsession command. Augmented with user-identity and www-port. - cmd = [ - 'rsession', - '--standalone=1', - '--program-mode=server', - '--log-stderr=1', - '--session-timeout-minutes=0', - ] - - - def initialize(self, state): - self.state = state - - @property - def port(self): - """ - Allocate a random empty port for use by rstudio - """ - if 'port' not in self.state: - sock = socket.socket() - sock.bind(('', 0)) - self.state['port'] = sock.getsockname()[1] - sock.close() - return self.state['port'] - - def rsession_uri(self): - return '{}proxy/{}/'.format(self.base_url, self.port) - - @gen.coroutine - def is_running(self): - '''Check if our proxied process is still running.''' - - if 'proc' not in self.state: - return False - - # Check if the process is still around - proc = self.state['proc'] - if proc.proc.poll() == 0: - self.log.info('Cannot poll on process.') - return False - - client = httpclient.AsyncHTTPClient() - req = httpclient.HTTPRequest('http://localhost:{}'.format(self.port)) + def get_env(self): + env = { + 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', + 'RSTUDIO_MINIMUM_USER_ID':'500', + } try: - yield client.fetch(req) - self.log.debug('Got positive response from rstudio server') + r_vars = detectR() + env.update(r_vars) except: - self.log.debug('Got negative response from rstudio server') - return False - - return True - - - @gen.coroutine - def start_process(self): - """ - Start the rstudio process - """ - - self.state['starting'] = True - try: - cmd = self.cmd + [ - '--user-identity=' + getpass.getuser(), - '--www-port=' + str(self.port) - ] - - server_env = os.environ.copy() - - # Seed RStudio's R and RSTUDIO env variables - server_env.update(self.env) - - try: - r_vars = detectR() - server_env.update(r_vars) - except: - raise web.HTTPError(reason='could not detect R', status_code=500) - - @gen.coroutine - def exit_callback(code): - """ - Callback when the rsessionproxy dies - """ - self.log.info('rsession process died with code {}'.format(code)) - del self.state['proc'] - if code != 0: - yield self.start_process() - - # Runs rsession in background - proc = process.Subprocess(cmd, env=server_env) - self.log.info('Starting rsession process...') - self.state['proc'] = proc - proc.set_exit_callback(exit_callback) - - for i in range(5): - if (yield self.is_running()): - self.log.info('rsession startup complete') - break - # Simple exponential backoff - wait_time = max(1.4 ** i, 5) - self.log.debug('Waiting {} before checking if rstudio is up'.format(wait_time)) - yield gen.sleep(wait_time) - else: - raise web.HTTPError('could not start rsession in time', status_code=500) - finally: - self.state['starting'] = False - - - @gen.coroutine - @web.authenticated - def proxy(self, port, path): - if not path.startswith('/'): - path = '/' + path - - # if we're in 'starting' let's wait a while - for i in range(5): - if not self.state.get('starting', False): - break - # Simple exponential backoff - wait_time = max(1.4 ** i, 5) - self.log.debug('Waiting {} before checking if rstudio is up'.format(wait_time)) - yield gen.sleep(wait_time) - else: - raise web.HTTPError('could not start rsession in time', status_code=500) - - # FIXME: try to not start multiple processes at a time with some locking here - if 'proc' not in self.state: - self.log.info('No existing process rsession process found') - yield self.start_process() - - return (yield super().proxy(self.port, path)) - - def get(self, path): - return self.proxy(self.port, path) - - def post(self, path): - return self.proxy(self.port, path) - - def put(self, path): - return self.proxy(self.port, path) - - def delete(self, path): - return self.proxy(self.port, path) - - def head(self, path): - return self.proxy(self.port, path) - - def patch(self, path): - return self.proxy(self.port, path) - - def options(self, path): - return self.proxy(self.port, path) + raise web.HTTPError(reason='could not detect R', status_code=500) + + return env + + def get_cmd(self): + # rsession command. Augmented with user-identity and www-port. + return [ + 'rsession', + '--standalone=1', + '--program-mode=server', + '--log-stderr=1', + '--session-timeout-minutes=0', + '--user-identity=' + getpass.getuser(), + '--www-port=' + str(self.port) + ] def setup_handlers(web_app): web_app.add_handlers('.*', [ diff --git a/setup.py b/setup.py index d612b9bc..dbd839ad 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ classifiers=['Framework :: Jupyter'], install_requires=[ 'notebook', - 'nbserverproxy >= 0.3.2' + 'nbserverproxy >= 0.4' ], package_data={'nbrsessionproxy': ['static/*']}, ) From b222353ed0562e003e626c79787694e769989d0e Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 2 Dec 2017 13:24:43 -0800 Subject: [PATCH 074/125] Bump version to v0.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dbd839ad..e327c1fa 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.4.1', + version='0.5', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", From 4e0a64e5f34d0b3c7aacdbebd131c259f5a37d7e Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 25 Jan 2018 12:47:48 -0800 Subject: [PATCH 075/125] Switch to using rserver instead of rsession This seems to handle process supervision better than we do, and also does prefixing properly to get shiny apps to work Shoutout to @cmd-ntrf for https://github.com/jupyterhub/nbrsessionproxy/pull/7, which is what inspired this change --- nbrsessionproxy/handlers.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index faadaba7..b28ed330 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -67,12 +67,8 @@ def get_env(self): def get_cmd(self): # rsession command. Augmented with user-identity and www-port. return [ - 'rsession', - '--standalone=1', - '--program-mode=server', - '--log-stderr=1', - '--session-timeout-minutes=0', - '--user-identity=' + getpass.getuser(), + 'rserver', + '--server-user=' + getpass.getuser(), '--www-port=' + str(self.port) ] From d35d21b2eb3e58f0fd3c5bacadd9439a12718d39 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 25 Jan 2018 18:30:48 -0800 Subject: [PATCH 076/125] Cleanup code + explicitly set USER environment variable - RStudio needs USER to be set, otherwise it'll push you out into an authentication window! - Since we're using rserver now rather than rsession, we do not need to do any R detection work. --- nbrsessionproxy/handlers.py | 44 ++++--------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index b28ed330..7de17068 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,10 +1,8 @@ import os import getpass -import socket -import subprocess from urllib.parse import urlunparse, urlparse -from tornado import web, gen, httpclient, process, ioloop +from tornado import web from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler @@ -12,30 +10,6 @@ from nbserverproxy.handlers import SuperviseAndProxyHandler -def detectR(): - '''Detect R's version, R_HOME, and various other directories that rsession - requires. - - Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp''' - - cmd = ['R', '--slave', '--vanilla', '-e', - 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] - - p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if p.returncode != 0: - raise Exception('Error detecting R') - R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ - p.stdout.decode().split(':') - - return { - 'R_DOC_DIR': R_DOC_DIR, - 'R_HOME': R_HOME, - 'R_INCLUDE_DIR': R_INCLUDE_DIR, - 'R_SHARE_DIR': R_SHARE_DIR, - 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, - 'RSTUDIO_DEFAULT_R_VERSION': version, - } - class AddSlashHandler(IPythonHandler): """Handler for adding trailing slash to URLs that need them""" @web.authenticated @@ -51,16 +25,9 @@ class RSessionProxyHandler(SuperviseAndProxyHandler): name = 'rsession' def get_env(self): - env = { - 'RSTUDIO_LIMIT_RPC_CLIENT_UID':'998', - 'RSTUDIO_MINIMUM_USER_ID':'500', - } - - try: - r_vars = detectR() - env.update(r_vars) - except: - raise web.HTTPError(reason='could not detect R', status_code=500) + env = {} + if 'USER' not in os.environ: + env['USER'] = getpass.getuser() return env @@ -68,7 +35,6 @@ def get_cmd(self): # rsession command. Augmented with user-identity and www-port. return [ 'rserver', - '--server-user=' + getpass.getuser(), '--www-port=' + str(self.port) ] @@ -77,5 +43,3 @@ def setup_handlers(web_app): (ujoin(web_app.settings['base_url'], 'rstudio/(.*)'), RSessionProxyHandler, dict(state={})), (ujoin(web_app.settings['base_url'], 'rstudio'), AddSlashHandler) ]) - -# vim: set et ts=4 sw=4: From 66f40d763740096452a88bdc303dda8572d74f65 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 25 Jan 2018 18:44:44 -0800 Subject: [PATCH 077/125] Deal with USER being present but empty --- nbrsessionproxy/handlers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 7de17068..a26d596f 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -26,7 +26,10 @@ class RSessionProxyHandler(SuperviseAndProxyHandler): def get_env(self): env = {} - if 'USER' not in os.environ: + + # rserver needs USER to be set to something sensible, + # otherwise it'll throw up an authentication page + if not os.environ.get('USER', ''): env['USER'] = getpass.getuser() return env From 9413046d5c542fabccce14b0b6510048fa66b091 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 31 Jan 2018 22:03:12 -0800 Subject: [PATCH 078/125] Update README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f6ac8c9..dd990e5e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # nbrsessionproxy -**nbrsessionproxy** provides Jupyter server and notebook extensions to proxy an RStudio rsession. +**nbrsessionproxy** provides Jupyter server and notebook extensions to proxy RStudio. ![Screenshot](screenshot.png) -If you have a JupyterHub deployment, nbrsessionproxy can take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Requires [nbserverproxy](https://github.com/jupyterhub/nbserverproxy). - +If you have a JupyterHub deployment, nbrsessionproxy can take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter's. ## Installation @@ -15,6 +14,8 @@ Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-p #### Install rstudio Use conda `conda install rstudio` or [download](https://www.rstudio.com/products/rstudio/download-server/) the corresponding package for your platform +Note that rstudio server is needed to work with this extension. + ### Install nbrsessionproxy Install the library: ``` From 7cb9df56b59e3a0dba555b12d040f9fc77349d4d Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 1 Feb 2018 21:44:47 -0800 Subject: [PATCH 079/125] Bump version to v0.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e327c1fa..9c739b4c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.5', + version='0.6', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", From 8807bf3f44a51ba86fb2b227bdb7adf4f9d51055 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 1 Feb 2018 21:45:55 -0800 Subject: [PATCH 080/125] Bump minimum version of nbrsessionproxy required --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9c739b4c..ff132fcb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.6', + version='0.6.1', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", @@ -11,7 +11,7 @@ classifiers=['Framework :: Jupyter'], install_requires=[ 'notebook', - 'nbserverproxy >= 0.4' + 'nbserverproxy >= 0.5.1' ], package_data={'nbrsessionproxy': ['static/*']}, ) From 031c8ae301e3fc44aa13d3643ca1314f72d7dbfd Mon Sep 17 00:00:00 2001 From: Landung Setiawan Date: Fri, 9 Feb 2018 07:40:03 -0800 Subject: [PATCH 081/125] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index dd990e5e..dc3af4dc 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Install the library: ``` pip install git+https://github.com/jupyterhub/nbrsessionproxy ``` +or +``` +conda install -c conda-forge nbrsessionproxy +``` Either install the extensions for the user: ``` From 41da4c0e5de2fa8013e3b436f344e2dfb3c1d289 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 12 Feb 2018 14:13:54 -0800 Subject: [PATCH 082/125] Add shiny proxy handler. --- nbrsessionproxy/handlers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index a26d596f..c6d5ec25 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -41,8 +41,20 @@ def get_cmd(self): '--www-port=' + str(self.port) ] +class ShinyProxyHandler(SuperviseAndProxyHandler): + '''Manage a Shiny instance.''' + + name = 'shiny' + port = '3838' + + def get_cmd(self): + # rsession command. Augmented with user-identity and www-port. + return [ 'shiny-server' ] + def setup_handlers(web_app): web_app.add_handlers('.*', [ (ujoin(web_app.settings['base_url'], 'rstudio/(.*)'), RSessionProxyHandler, dict(state={})), - (ujoin(web_app.settings['base_url'], 'rstudio'), AddSlashHandler) + (ujoin(web_app.settings['base_url'], 'shiny/(.*)'), ShinyProxyHandler, dict(state={})), + (ujoin(web_app.settings['base_url'], 'rstudio'), AddSlashHandler), + (ujoin(web_app.settings['base_url'], 'shiny'), AddSlashHandler) ]) From 432262cdcc3630b647846656f820526a8a1bea9c Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Tue, 13 Feb 2018 12:50:48 -0800 Subject: [PATCH 083/125] Provide get_env stub. --- nbrsessionproxy/handlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index c6d5ec25..717bc12a 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -47,6 +47,9 @@ class ShinyProxyHandler(SuperviseAndProxyHandler): name = 'shiny' port = '3838' + def get_env(self): + return {} + def get_cmd(self): # rsession command. Augmented with user-identity and www-port. return [ 'shiny-server' ] From fa10919672b93e3f2797abb350b936d487c48247 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Wed, 21 Feb 2018 12:43:33 -0800 Subject: [PATCH 084/125] Create the shiny config ourselves. Assume shiny apps are in the user home directory. --- nbrsessionproxy/handlers.py | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 717bc12a..90a58f2b 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,5 +1,8 @@ import os import getpass +import pwd +import tempfile + from urllib.parse import urlunparse, urlparse from tornado import web @@ -45,14 +48,35 @@ class ShinyProxyHandler(SuperviseAndProxyHandler): '''Manage a Shiny instance.''' name = 'shiny' - port = '3838' + conf_tmpl = """run_as {user}; +server {{ + listen {port}; + location / {{ + site_dir {site_dir}; + log_dir {site_dir}/logs; + directory_index on; + }} +}} +""" + + def write_conf(self, user, port, site_dir): + '''Create a configuration file and return its name.''' + conf = self.conf_tmpl.format(user=user, port=port, site_dir=site_dir) + f = tempfile.NamedTemporaryFile(mode='w', delete=False) + f.write(conf) + f.close() + return f.name def get_env(self): return {} def get_cmd(self): - # rsession command. Augmented with user-identity and www-port. - return [ 'shiny-server' ] + user = getpass.getuser() + site_dir = pwd.getpwnam(user).pw_dir + filename = self.write_conf(user, self.port, site_dir) + + # shiny command. + return [ 'shiny-server', filename ] def setup_handlers(web_app): web_app.add_handlers('.*', [ From e653f57245e28ee54b2a5bbda6cf7c7c0bfb9966 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Wed, 21 Feb 2018 13:06:52 -0800 Subject: [PATCH 085/125] Remove tabs. --- nbrsessionproxy/handlers.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 90a58f2b..7c50ac06 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -1,3 +1,4 @@ +# vim: set et sw=4 ts=4: import os import getpass import pwd @@ -48,7 +49,7 @@ class ShinyProxyHandler(SuperviseAndProxyHandler): '''Manage a Shiny instance.''' name = 'shiny' - conf_tmpl = """run_as {user}; + conf_tmpl = """run_as {user}; server {{ listen {port}; location / {{ @@ -59,21 +60,21 @@ class ShinyProxyHandler(SuperviseAndProxyHandler): }} """ - def write_conf(self, user, port, site_dir): - '''Create a configuration file and return its name.''' - conf = self.conf_tmpl.format(user=user, port=port, site_dir=site_dir) - f = tempfile.NamedTemporaryFile(mode='w', delete=False) - f.write(conf) - f.close() - return f.name + def write_conf(self, user, port, site_dir): + '''Create a configuration file and return its name.''' + conf = self.conf_tmpl.format(user=user, port=port, site_dir=site_dir) + f = tempfile.NamedTemporaryFile(mode='w', delete=False) + f.write(conf) + f.close() + return f.name def get_env(self): return {} def get_cmd(self): - user = getpass.getuser() - site_dir = pwd.getpwnam(user).pw_dir - filename = self.write_conf(user, self.port, site_dir) + user = getpass.getuser() + site_dir = pwd.getpwnam(user).pw_dir + filename = self.write_conf(user, self.port, site_dir) # shiny command. return [ 'shiny-server', filename ] From ddabdf6d35dc63cf3eb7fbf56290f3a87eee46d8 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Wed, 2 May 2018 10:38:24 -0700 Subject: [PATCH 086/125] Bump version to 0.7.0. Contains support for running shiny apps when shiny-server is installed. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ff132fcb..89fbe14b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.6.1', + version='0.7.0', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", From df03d10f83ffdd2021aecd5ca346434ffe056700 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Fri, 15 Jun 2018 00:28:29 -0700 Subject: [PATCH 087/125] Fix pip install instructions --sys-prefix is not actually system-wide. --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dc3af4dc..733275c3 100644 --- a/README.md +++ b/README.md @@ -19,21 +19,15 @@ Note that rstudio server is needed to work with this extension. ### Install nbrsessionproxy Install the library: ``` -pip install git+https://github.com/jupyterhub/nbrsessionproxy +pip install nbrsessionproxy ``` or ``` conda install -c conda-forge nbrsessionproxy ``` -Either install the extensions for the user: -``` -jupyter serverextension enable --py nbrsessionproxy -jupyter nbextension install --py nbrsessionproxy -jupyter nbextension enable --py nbrsessionproxy -``` +If installing via pip, you need to enable the extension. -Or install the extensions for all users on the system: ``` jupyter serverextension enable --py --sys-prefix nbrsessionproxy jupyter nbextension install --py --sys-prefix nbrsessionproxy From 242b9accc6e662bbad74891ff8d86b8ce7a0bb8d Mon Sep 17 00:00:00 2001 From: Yuval Kalugny Date: Tue, 3 Jul 2018 21:07:20 +0300 Subject: [PATCH 088/125] Update README.md Fixed: Instructions for installing in Jupyterlab referred to the wrong repo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 733275c3..165d4f76 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ jupyter nbextension enable --py --sys-prefix nbrsessionproxy For JupyterLab first clone this repository to a known location and install from the directory. ``` -git clone https://github.com/jupyterhub/nbserverproxy /opt/nbserverproxy -pip install -e /opt/nbserverproxy +git clone https://github.com/jupyterhub/nbserverproxy /opt/nbrsessionproxy +pip install -e /opt/nbrsessionproxy jupyter serverextension enable --py nbrsessionproxy jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy ``` From 95fab0e89fd5ec876dda7ae29a6102e36c094fb1 Mon Sep 17 00:00:00 2001 From: Anton Khodak Date: Wed, 5 Sep 2018 10:14:58 +0100 Subject: [PATCH 089/125] Add missing psmisc and libssl dependencies --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 97db0ae3..e995d493 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,8 @@ RUN apt-get update && \ libapparmor1 \ libedit2 \ lsb-release \ + psmisc \ + libssl1.0.0 \ ; # You can use rsession from rstudio's desktop package as well. From d6d263f92d8c3294fce89a7b66284734316d6f9b Mon Sep 17 00:00:00 2001 From: Anton Khodak Date: Wed, 5 Sep 2018 10:57:45 +0100 Subject: [PATCH 090/125] Correct `nbserverproxy` to `nbsessionproxy` This causes errors since a different repository is pulled --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 165d4f76..b67ad8c5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ jupyter nbextension enable --py --sys-prefix nbrsessionproxy For JupyterLab first clone this repository to a known location and install from the directory. ``` -git clone https://github.com/jupyterhub/nbserverproxy /opt/nbrsessionproxy +git clone https://github.com/jupyterhub/nbrsessionproxy /opt/nbrsessionproxy pip install -e /opt/nbrsessionproxy jupyter serverextension enable --py nbrsessionproxy jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy From 482fadc84b477e7b0c5fc50295308c202e4331c7 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Fri, 7 Sep 2018 00:06:22 -0700 Subject: [PATCH 091/125] Document considerations for multiuser environments. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index b67ad8c5..821c36e2 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,10 @@ jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy ``` The Dockerfile contains an example installation on top of [jupyter/r-notebook](https://github.com/jupyter/docker-stacks/tree/master/r-notebook). + + +### Multiuser Considerations + +This extension launches an rstudio server process from the jupyter notebook server. This is fine in JupyterHub deployments where user servers are containerized since other users cannot connect to the rstudio server port. In non-containerized JupyterHub deployments, for example on multiuser systems running LocalSpawner or BatchSpawner, this not secure. Any user may connect to rstudio server and run arbitrary code. + +Additionally, rstudio-server expects to write to `/tmp/rstudio-server/secure-cookie-key`, which means without separate mount namespaces for /tmp, only one user can run rstudio server at a time. From cc6c3dfaed9866b7b53a7262a42ed099496df75d Mon Sep 17 00:00:00 2001 From: "Lei (Ricky) Jin" Date: Tue, 11 Jul 2017 22:16:30 -0700 Subject: [PATCH 092/125] - updated deps for latest jupyterlab 0.25.2 --- jupyterlab-rsessionproxy/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 0f4f095c..0e8b7341 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,17 +17,17 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.7.0", - "@jupyterlab/apputils": "^0.7.0", - "@jupyterlab/coreutils": "^0.7.0", - "@jupyterlab/launcher": "^0.7.0", - "@jupyterlab/services": "^0.46.0", + "@jupyterlab/application": "^0.8.0", + "@jupyterlab/apputils": "^0.8.0", + "@jupyterlab/coreutils": "^0.8.0", + "@jupyterlab/launcher": "^0.8.0", + "@jupyterlab/services": "^0.47.0", "@phosphor/messaging": "^1.2.1", "@phosphor/widgets": "^1.3.0" }, "devDependencies": { "rimraf": "^2.5.2", - "typescript": "~2.2.0" + "typescript": "~2.3.0" }, "keywords": [ "jupyter", From 0c52a164cd5e6512e6251a7797558057747bd19e Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Sat, 22 Jul 2017 17:05:03 -0700 Subject: [PATCH 093/125] Updating packages for 0.26.3 release --- jupyterlab-rsessionproxy/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 0e8b7341..7b2e6eda 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,11 +17,11 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.8.0", - "@jupyterlab/apputils": "^0.8.0", - "@jupyterlab/coreutils": "^0.8.0", - "@jupyterlab/launcher": "^0.8.0", - "@jupyterlab/services": "^0.47.0", + "@jupyterlab/application": "^0.9.0", + "@jupyterlab/apputils": "^0.9.0", + "@jupyterlab/coreutils": "^0.9.0", + "@jupyterlab/launcher": "^0.9.0", + "@jupyterlab/services": "^0.48.0", "@phosphor/messaging": "^1.2.1", "@phosphor/widgets": "^1.3.0" }, From 5fc4c22c4f3d3e18b578684a4f9ece6b6c5f8af6 Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Tue, 12 Sep 2017 21:58:49 -0700 Subject: [PATCH 094/125] Updates for jupyterlab 0.27 --- jupyterlab-rsessionproxy/package.json | 12 ++++++------ jupyterlab-rsessionproxy/src/index.ts | 3 +-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 7b2e6eda..438a83b4 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,17 +17,17 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.9.0", - "@jupyterlab/apputils": "^0.9.0", - "@jupyterlab/coreutils": "^0.9.0", - "@jupyterlab/launcher": "^0.9.0", - "@jupyterlab/services": "^0.48.0", + "@jupyterlab/application": "^0.10.0", + "@jupyterlab/apputils": "^0.10.0", + "@jupyterlab/coreutils": "^0.10.0", + "@jupyterlab/launcher": "^0.10.0", + "@jupyterlab/services": "^0.49.0", "@phosphor/messaging": "^1.2.1", "@phosphor/widgets": "^1.3.0" }, "devDependencies": { "rimraf": "^2.5.2", - "typescript": "~2.3.0" + "typescript": "~2.4.1" }, "keywords": [ "jupyter", diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index d6342851..b5f11123 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -138,9 +138,8 @@ function activate(app: JupyterLab, palette: ICommandPalette, let req = { url: settings.baseUrl + 'rsessionproxy', method: 'POST', - data: {} }; - ServerConnection.makeRequest(req, settings).then(resp => { + ServerConnection.makeRequest(req, settings).then((resp:ServerConnection.IResponse) => { console.log("Started RStudio... ", resp.data.url); window.open(resp.data.url, 'RStudio Session'); // let iframe = newClosableIFrame(resp.data.url, 'Rstudio Session'); From a40e038283544151c0df540cff2562dacab1230d Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Thu, 2 Nov 2017 17:35:25 -0700 Subject: [PATCH 095/125] Updating for jupyterlab 0.28.12 --- jupyterlab-rsessionproxy/package.json | 10 +-- jupyterlab-rsessionproxy/src/index.ts | 121 ++++++-------------------- 2 files changed, 30 insertions(+), 101 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 438a83b4..fb0e0cfa 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,11 +17,11 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.10.0", - "@jupyterlab/apputils": "^0.10.0", - "@jupyterlab/coreutils": "^0.10.0", - "@jupyterlab/launcher": "^0.10.0", - "@jupyterlab/services": "^0.49.0", + "@jupyterlab/application": "^0.11.0", + "@jupyterlab/apputils": "^0.11.0", + "@jupyterlab/coreutils": "^0.11.0", + "@jupyterlab/launcher": "^0.11.0", + "@jupyterlab/services": "^0.50.0", "@phosphor/messaging": "^1.2.1", "@phosphor/widgets": "^1.3.0" }, diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index b5f11123..3f9df421 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -2,7 +2,7 @@ // Distributed under the terms of the Modified BSD License. import { - ILayoutRestorer, JupyterLab, JupyterLabPlugin + JupyterLab, JupyterLabPlugin } from '@jupyterlab/application'; import { @@ -37,97 +37,15 @@ namespace CommandIDs { const RSTUDIO_ICON_CLASS = 'jp-RStudioIcon'; -/** - * A flag denoting whether the application is loaded over HTTPS. - */ -// const LAB_IS_SECURE = window.location.protocol === 'https:'; - -/** - * The class name added to the help widget. - */ -// const RSESSION_CLASS = 'jp-Rsession'; - -/** - * A list of help resources. - */ - -const RESOURCES = [ - { - text: 'RStudio Session', - url: '/' - } -]; - -RESOURCES.sort((a: any, b: any) => { - return a.text.localeCompare(b.text); -}); - - -/** - * The rsession handler extension. - */ -const plugin: JupyterLabPlugin = { - activate, - id: 'jupyter.extensions.rsessionproxy', - requires: [ICommandPalette, ILayoutRestorer], - optional: [ILauncher], - autoStart: true -}; - - -/** - * Export the plugin as default. - */ -export default plugin; - -/* - * An IFrame the disposes itself when closed. - * - * This is needed to clear the state restoration db when IFrames are closed. - */ -// class ClosableIFrame extends IFrame { -// -// /** -// * Dispose of the IFrame when closing. -// */ -// protected onCloseRequest(msg: Message): void { -// this.dispose(); -// } -// } - - /** * Activate the rsession extension. */ -function activate(app: JupyterLab, palette: ICommandPalette, - restorer: ILayoutRestorer, launcher: ILauncher | null): void { +function activate(app: JupyterLab, palette: ICommandPalette, launcher: ILauncher): void { let counter = 0; const category = 'RStudio'; const namespace = 'rsession-proxy'; const command = CommandIDs.launch; const { commands, shell } = app; - // const tracker = new InstanceTracker({ namespace }); - - // Handle state restoration. - // restorer.restore(tracker, { - // command, - // args: widget => ({ url: widget.url, text: widget.title.label }), - // name: widget => widget.url - // }); - - /** - * Create a new ClosableIFrame widget. - */ - // function newClosableIFrame(url: string, text: string): ClosableIFrame { - // let iframe = new ClosableIFrame(); - // iframe.addClass(RSESSION_CLASS); - // iframe.title.label = text; - // iframe.title.closable = true; - // iframe.id = `${namespace}-${++counter}`; - // iframe.url = url; - // tracker.add(iframe); - // return iframe; - // } commands.addCommand(command, { label: 'New Rstudio Session', @@ -142,24 +60,35 @@ function activate(app: JupyterLab, palette: ICommandPalette, ServerConnection.makeRequest(req, settings).then((resp:ServerConnection.IResponse) => { console.log("Started RStudio... ", resp.data.url); window.open(resp.data.url, 'RStudio Session'); - // let iframe = newClosableIFrame(resp.data.url, 'Rstudio Session'); - // shell.addToMainArea(iframe); - // shell.activateById(iframe.id); }); } }); // Add a launcher item if the launcher is available. - if (launcher) { - launcher.add({ - displayName: 'RStudio', - iconClass: RSTUDIO_ICON_CLASS, - callback: () => { - return commands.execute(command); - } - }); - } + launcher.add({ + displayName: 'RStudio', + iconClass: RSTUDIO_ICON_CLASS, + callback: () => { + return commands.execute(command); + } + }); palette.addItem({ command, category }); } +/** + * The rsession handler extension. + */ +const plugin: JupyterLabPlugin = { + id: 'jupyterlab_rsessionproxy', + autoStart: true, + requires: [ICommandPalette, ILauncher], + activate: activate, +}; + + +/** + * Export the plugin as default. + */ +export default plugin; + From effab4039412f92fd0da9c6852f9fb8464a095ae Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Thu, 9 Nov 2017 17:26:24 -0800 Subject: [PATCH 096/125] Updating to jupyterlab 0.29.x --- jupyterlab-rsessionproxy/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index fb0e0cfa..3b94bb0e 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,11 +17,11 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.11.0", - "@jupyterlab/apputils": "^0.11.0", - "@jupyterlab/coreutils": "^0.11.0", - "@jupyterlab/launcher": "^0.11.0", - "@jupyterlab/services": "^0.50.0", + "@jupyterlab/application": "^0.12.0", + "@jupyterlab/apputils": "^0.12.0", + "@jupyterlab/coreutils": "^0.12.0", + "@jupyterlab/launcher": "^0.12.0", + "@jupyterlab/services": "^0.51.0", "@phosphor/messaging": "^1.2.1", "@phosphor/widgets": "^1.3.0" }, From 458b187dfc50764cbc8be3e96789d8ab0203a392 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Thu, 16 Nov 2017 08:33:12 -0800 Subject: [PATCH 097/125] Updated post request to match new code base --- jupyterlab-rsessionproxy/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index 3f9df421..7dfea66d 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -54,7 +54,7 @@ function activate(app: JupyterLab, palette: ICommandPalette, launcher: ILauncher // Start up the rserver let settings = ServerConnection.makeSettings(); let req = { - url: settings.baseUrl + 'rsessionproxy', + url: settings.baseUrl + 'rstudio', method: 'POST', }; ServerConnection.makeRequest(req, settings).then((resp:ServerConnection.IResponse) => { From bf81fa1ef57b77880de1081ad624be32419c902c Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Mon, 20 Nov 2017 16:48:26 -0800 Subject: [PATCH 098/125] Updated to match new code base/moved to menu bar to get rid of undefined id issues --- jupyterlab-rsessionproxy/src/index.ts | 49 +++++++++++---------------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index 7dfea66d..c5963d74 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -6,20 +6,20 @@ import { } from '@jupyterlab/application'; import { - ICommandPalette/*, IFrame, InstanceTracker*/ + ICommandPalette, IMainMenu/*, IFrame, InstanceTracker*/ } from '@jupyterlab/apputils'; import { - ServerConnection -} from '@jupyterlab/services'; + Message +} from '@phosphor/messaging'; import { - ILauncher -} from '@jupyterlab/launcher'; + Menu +} from '@phosphor/widgets'; import { - Message -} from '@phosphor/messaging'; + PageConfig, URLExt +} from '@jupyterlab/coreutils'; import '../style/index.css'; @@ -40,7 +40,7 @@ const RSTUDIO_ICON_CLASS = 'jp-RStudioIcon'; /** * Activate the rsession extension. */ -function activate(app: JupyterLab, palette: ICommandPalette, launcher: ILauncher): void { +function activate(app: JupyterLab, palette: ICommandPalette, mainMenu: IMainMenu): void { let counter = 0; const category = 'RStudio'; const namespace = 'rsession-proxy'; @@ -48,32 +48,23 @@ function activate(app: JupyterLab, palette: ICommandPalette, launcher: ILauncher const { commands, shell } = app; commands.addCommand(command, { - label: 'New Rstudio Session', + label: 'Launch RStudio', caption: 'Start a new Rstudio Session', execute: () => { - // Start up the rserver - let settings = ServerConnection.makeSettings(); - let req = { - url: settings.baseUrl + 'rstudio', - method: 'POST', - }; - ServerConnection.makeRequest(req, settings).then((resp:ServerConnection.IResponse) => { - console.log("Started RStudio... ", resp.data.url); - window.open(resp.data.url, 'RStudio Session'); - }); + window.open(PageConfig.getBaseUrl() + 'rstudio/', 'RStudio Session'); } }); - // Add a launcher item if the launcher is available. - launcher.add({ - displayName: 'RStudio', - iconClass: RSTUDIO_ICON_CLASS, - callback: () => { - return commands.execute(command); - } + // Add commands and menu itmes. + let menu = new Menu({ commands }); + menu.title.label = category; + [ + CommandIDs.launch, + ].forEach(command => { + palette.addItem({ command, category }); + menu.addItem({ command }); }); - - palette.addItem({ command, category }); + mainMenu.addMenu(menu, {rank: 98}); } /** @@ -82,7 +73,7 @@ function activate(app: JupyterLab, palette: ICommandPalette, launcher: ILauncher const plugin: JupyterLabPlugin = { id: 'jupyterlab_rsessionproxy', autoStart: true, - requires: [ICommandPalette, ILauncher], + requires: [ICommandPalette, IMainMenu], activate: activate, }; From b749c0e73bb1a931e0dc7ff9f957e1fa00fa4052 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Thu, 21 Dec 2017 15:09:07 -0800 Subject: [PATCH 099/125] Updated for new version of jupyterlab/hub --- jupyterlab-rsessionproxy/package.json | 15 ++++++++------- jupyterlab-rsessionproxy/src/index.ts | 6 +++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 3b94bb0e..f3668da0 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,13 +17,14 @@ "extension": true }, "dependencies": { - "@jupyterlab/application": "^0.12.0", - "@jupyterlab/apputils": "^0.12.0", - "@jupyterlab/coreutils": "^0.12.0", - "@jupyterlab/launcher": "^0.12.0", - "@jupyterlab/services": "^0.51.0", - "@phosphor/messaging": "^1.2.1", - "@phosphor/widgets": "^1.3.0" + "@jupyterlab/apputils": "^0.13.0", + "@jupyterlab/application": "^0.13.1", + "@jupyterlab/mainmenu": "^0.2.0", + "@jupyterlab/coreutils": "^0.13.0", + "@jupyterlab/launcher": "^0.13.2", + "@jupyterlab/services": "^0.52.0", + "@phosphor/messaging": "^1.2.2", + "@phosphor/widgets": "^1.5.0" }, "devDependencies": { "rimraf": "^2.5.2", diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index c5963d74..403c19dc 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -6,9 +6,13 @@ import { } from '@jupyterlab/application'; import { - ICommandPalette, IMainMenu/*, IFrame, InstanceTracker*/ + ICommandPalette } from '@jupyterlab/apputils'; +import { + IMainMenu +} from '@jupyterlab/mainmenu'; + import { Message } from '@phosphor/messaging'; From 3cb0820611781198bc0622a0b6295645e1f634e8 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Mon, 12 Feb 2018 17:16:15 -0800 Subject: [PATCH 100/125] Updated for beta release of jupyterlab --- jupyterlab-rsessionproxy/package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index f3668da0..092f9b66 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -1,6 +1,6 @@ { "name": "@jupyterlab/rsessionproxy-extension", - "version": "0.1.2", + "version": "0.1.3", "description": "JupyterLab - RSession Proxy Extension", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -17,18 +17,18 @@ "extension": true }, "dependencies": { - "@jupyterlab/apputils": "^0.13.0", - "@jupyterlab/application": "^0.13.1", - "@jupyterlab/mainmenu": "^0.2.0", - "@jupyterlab/coreutils": "^0.13.0", - "@jupyterlab/launcher": "^0.13.2", - "@jupyterlab/services": "^0.52.0", + "@jupyterlab/apputils": "^0.15.4", + "@jupyterlab/application": "^0.15.4", + "@jupyterlab/mainmenu": "^0.4.4", + "@jupyterlab/coreutils": "^1.0.6", + "@jupyterlab/launcher": "^0.15.4", + "@jupyterlab/services": "^1.1.4", "@phosphor/messaging": "^1.2.2", "@phosphor/widgets": "^1.5.0" }, "devDependencies": { - "rimraf": "^2.5.2", - "typescript": "~2.4.1" + "rimraf": "^2.6.2", + "typescript": "~2.6.2" }, "keywords": [ "jupyter", From 65310a7afc8ab819b64932f6c710a19047bac548 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Fri, 27 Jul 2018 15:15:45 -0700 Subject: [PATCH 101/125] 0.33.0 --- jupyterlab-rsessionproxy/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 092f9b66..20b59ab1 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,18 +17,18 @@ "extension": true }, "dependencies": { - "@jupyterlab/apputils": "^0.15.4", - "@jupyterlab/application": "^0.15.4", - "@jupyterlab/mainmenu": "^0.4.4", - "@jupyterlab/coreutils": "^1.0.6", - "@jupyterlab/launcher": "^0.15.4", - "@jupyterlab/services": "^1.1.4", + "@jupyterlab/apputils": "^0.17.0", + "@jupyterlab/application": "^0.17.0", + "@jupyterlab/mainmenu": "^0.6.2", + "@jupyterlab/coreutils": "^2.0.2", + "@jupyterlab/launcher": "^0.17.0", + "@jupyterlab/services": "^3.0.0", "@phosphor/messaging": "^1.2.2", "@phosphor/widgets": "^1.5.0" }, "devDependencies": { "rimraf": "^2.6.2", - "typescript": "~2.6.2" + "typescript": "~2.9.2" }, "keywords": [ "jupyter", From a18942a6fac82cc6a81e2eec6d113e713901098d Mon Sep 17 00:00:00 2001 From: Tony Kinsley Date: Tue, 12 Sep 2017 21:58:49 -0700 Subject: [PATCH 102/125] Updates for jupyterlab 0.27 --- jupyterlab-rsessionproxy/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index 403c19dc..778ac9c0 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -86,4 +86,3 @@ const plugin: JupyterLabPlugin = { * Export the plugin as default. */ export default plugin; - From 110afec054ee3b0770f0ad79a429ad834e546493 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Mon, 20 Nov 2017 16:48:26 -0800 Subject: [PATCH 103/125] Updated to match new code base/moved to menu bar to get rid of undefined id issues --- jupyterlab-rsessionproxy/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index 778ac9c0..9704e6ef 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -14,8 +14,8 @@ import { } from '@jupyterlab/mainmenu'; import { - Message -} from '@phosphor/messaging'; + PageConfig, URLExt +} from '@jupyterlab/coreutils'; import { Menu From 2738c4d195f8970c502b8da5bf6e3294a7448fb9 Mon Sep 17 00:00:00 2001 From: Derek Heldt-Werle Date: Thu, 6 Sep 2018 16:23:54 -0700 Subject: [PATCH 104/125] Fixed for 0.34.0 --- jupyterlab-rsessionproxy/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 20b59ab1..9f1f5bb6 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,14 +17,14 @@ "extension": true }, "dependencies": { - "@jupyterlab/apputils": "^0.17.0", - "@jupyterlab/application": "^0.17.0", - "@jupyterlab/mainmenu": "^0.6.2", - "@jupyterlab/coreutils": "^2.0.2", - "@jupyterlab/launcher": "^0.17.0", - "@jupyterlab/services": "^3.0.0", + "@jupyterlab/apputils": "^0.18.4", + "@jupyterlab/application": "^0.18.4", + "@jupyterlab/mainmenu": "^0.7.4", + "@jupyterlab/coreutils": "^2.1.4", + "@jupyterlab/launcher": "^0.18.4", + "@jupyterlab/services": "^3.1.4", "@phosphor/messaging": "^1.2.2", - "@phosphor/widgets": "^1.5.0" + "@phosphor/widgets": "^1.6.0" }, "devDependencies": { "rimraf": "^2.6.2", From cb7cc3daa3404c1a1210969444ffc18904cf658f Mon Sep 17 00:00:00 2001 From: Kalvin Chau Date: Thu, 18 Oct 2018 09:43:45 -0700 Subject: [PATCH 105/125] fix for 0.35 --- jupyterlab-rsessionproxy/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json index 9f1f5bb6..8721890e 100644 --- a/jupyterlab-rsessionproxy/package.json +++ b/jupyterlab-rsessionproxy/package.json @@ -17,12 +17,12 @@ "extension": true }, "dependencies": { - "@jupyterlab/apputils": "^0.18.4", - "@jupyterlab/application": "^0.18.4", - "@jupyterlab/mainmenu": "^0.7.4", - "@jupyterlab/coreutils": "^2.1.4", - "@jupyterlab/launcher": "^0.18.4", - "@jupyterlab/services": "^3.1.4", + "@jupyterlab/apputils": "^0.19.1", + "@jupyterlab/application": "^0.19.1", + "@jupyterlab/mainmenu": "^0.8.1", + "@jupyterlab/coreutils": "^2.2.1", + "@jupyterlab/launcher": "^0.19.1", + "@jupyterlab/services": "^3.2.1", "@phosphor/messaging": "^1.2.2", "@phosphor/widgets": "^1.6.0" }, From 9537649b6cebcf8a45d971d658768620d342feae Mon Sep 17 00:00:00 2001 From: Kalvin Chau Date: Thu, 18 Oct 2018 10:05:12 -0700 Subject: [PATCH 106/125] remove duplicate lines from merge --- jupyterlab-rsessionproxy/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts index 9704e6ef..778ac9c0 100644 --- a/jupyterlab-rsessionproxy/src/index.ts +++ b/jupyterlab-rsessionproxy/src/index.ts @@ -14,8 +14,8 @@ import { } from '@jupyterlab/mainmenu'; import { - PageConfig, URLExt -} from '@jupyterlab/coreutils'; + Message +} from '@phosphor/messaging'; import { Menu From b939841459f4d0c64b98020a9792d646e892ee84 Mon Sep 17 00:00:00 2001 From: Tim Head Date: Mon, 10 Dec 2018 16:12:53 +0100 Subject: [PATCH 107/125] Bump version Increase the version of `nbserverproxy` required as the previous version of it had a bug which prevents nbrsessionproxy from working. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 89fbe14b..c0fbac85 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setuptools.setup( name="nbrsessionproxy", - version='0.7.0', + version='0.8.0', url="https://github.com/jupyterhub/nbrsessionproxy", author="Ryan Lovett", description="Jupyter extension to proxy RStudio's rsession", @@ -11,7 +11,7 @@ classifiers=['Framework :: Jupyter'], install_requires=[ 'notebook', - 'nbserverproxy >= 0.5.1' + 'nbserverproxy >= 0.8.8' ], package_data={'nbrsessionproxy': ['static/*']}, ) From 81f3bb64b1286f4547c54f3de6ebff0f3d249a85 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Mon, 10 Dec 2018 10:05:01 -0800 Subject: [PATCH 108/125] Upload to pypi from travis upon tagging. --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9d8edbd9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python +python: +- 3.5 +script: +- true +deploy: + provider: pypi + user: nbrsessionproxy + password: + secure: JilUEN99KU/dZbBtStWLvJ7EdU6gud8/9C58ZhA2p8B7XgGF0GRG9/Sved9wxab1J5AL1GiweazxSn7WJVleBelNaKRmImVyukHcPIw5hS01+eE1KAWMcimPPB3Mp5JxfmupO2vaWJu5C2Lp5VM5MLuRHtxLTqYNW2qUNOmh0fpNrUIF+ATRKaDXaGE5o68FB/UZ9J91FoTK2GUcFFjmTwC4qz10udfHDMTE0YzsgyDBiX8RdeGdDzVLSP2kjdm0k/zORNhTPnzDhUX9wLAaZh5pRxYrS2Wy0C2u55CvcV/YxZiSOq9FY6Eblm8SJSOl+kaQw8sj20J7DFIwVKBpimTa/k2Byh8JrjeCO3wyZ/UfPycmRC73V5nxRBrDcOvr83j8SYhOcD6/6OwN+dnkoYJYak2YGs8EM6d78cuZ5CEl6k0nPsdr6nd3thefBrOGYZN9AehnF9fHFRwAHzKvObfJ0UxB/uyh8VIvSRqYHVxgyBj6U1erXrhkjqWTVcAAvv9mjLnSQG7YePtmaYj3qPreZ5mAj9QaTO8XE1e6oiv+5MDrTXfK7kKocXNijs30+SQBk9M/euuaxzFcXrCe98hKIKMqzzNrC9S/NTQ/13WdgxQL6At/7Tcsxep+rqDHL9T7lsJ6t1ydvXMk4AnZQaA77FZQvq2RCDPmyktYmxI= + on: + tags: true + repo: jupyterhub/nbrsessionproxy + distributions: sdist bdist_wheel From 53db34106c780732ad9c71a18a4cebb157039ffc Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 28 Dec 2018 14:03:31 -0800 Subject: [PATCH 109/125] Detect appropriate R to use Required for calling rsession directly Reverts d35d21b2eb3e58f0fd3c5bacadd9439a12718d39 --- nbrsessionproxy/handlers.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 7c50ac06..0873a483 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -7,6 +7,7 @@ from urllib.parse import urlunparse, urlparse from tornado import web +import subprocess from notebook.utils import url_path_join as ujoin from notebook.base.handlers import IPythonHandler @@ -22,6 +23,31 @@ def get(self, *args): dest = src._replace(path=src.path + '/') self.redirect(urlunparse(dest)) +def detectR(): + '''Detect R's version, R_HOME, and various other directories that rsession + requires. + + Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp''' + + cmd = ['R', '--slave', '--vanilla', '-e', + 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] + + p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if p.returncode != 0: + raise Exception('Error detecting R') + R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ + p.stdout.decode().split(':') + + return { + 'R_DOC_DIR': R_DOC_DIR, + 'R_HOME': R_HOME, + 'R_INCLUDE_DIR': R_INCLUDE_DIR, + 'R_SHARE_DIR': R_SHARE_DIR, + 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, + 'RSTUDIO_DEFAULT_R_VERSION': version, + } + + class RSessionProxyHandler(SuperviseAndProxyHandler): '''Manage an RStudio rsession instance.''' @@ -30,6 +56,11 @@ class RSessionProxyHandler(SuperviseAndProxyHandler): def get_env(self): env = {} + try: + r_vars = detectR() + env.update(r_vars) + except: + raise web.HTTPError(reason='could not detect R', status_code=500) # rserver needs USER to be set to something sensible, # otherwise it'll throw up an authentication page From 6db4a162bfa2e35e65e6b9f06193a4a0f778a118 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 28 Dec 2018 23:46:50 -0800 Subject: [PATCH 110/125] Revert "Switch to using rserver instead of rsession" This reverts commit 4e0a64e5f34d0b3c7aacdbebd131c259f5a37d7e. --- nbrsessionproxy/handlers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py index 0873a483..d3cb8394 100644 --- a/nbrsessionproxy/handlers.py +++ b/nbrsessionproxy/handlers.py @@ -72,7 +72,12 @@ def get_env(self): def get_cmd(self): # rsession command. Augmented with user-identity and www-port. return [ - 'rserver', + 'rsession', + '--standalone=1', + '--program-mode=server', + '--log-stderr=1', + '--session-timeout-minutes=0', + '--user-identity=' + getpass.getuser(), '--www-port=' + str(self.port) ] From a3204d7181e68d5cf43654920670646584272cfd Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sun, 30 Dec 2018 11:32:46 -0800 Subject: [PATCH 111/125] Use entrypoints to extend nbserverproxy Instead of creating our own handlers, we instead declare ways of figuring out the commands we want run to start our processes, and let nbserverproxy do the actual work. Requires https://github.com/jupyterhub/nbserverproxy/pull/65 --- nbrsessionproxy/__init__.py | 85 ++++++++++++++++++------ nbrsessionproxy/handlers.py | 124 ------------------------------------ setup.py | 6 ++ 3 files changed, 73 insertions(+), 142 deletions(-) delete mode 100644 nbrsessionproxy/handlers.py diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index f940aaab..155fb1ef 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -1,18 +1,67 @@ -from nbrsessionproxy.handlers import setup_handlers - -# Jupyter Extension points -def _jupyter_server_extension_paths(): - return [{ - 'module': 'nbrsessionproxy', - }] - -def _jupyter_nbextension_paths(): - return [{ - "section": "tree", - "dest": "nbrsessionproxy", - "src": "static", - "require": "nbrsessionproxy/tree" - }] - -def load_jupyter_server_extension(nbapp): - setup_handlers(nbapp.web_app) +import os +import tempfile +import subprocess +import getpass +from textwrap import dedent + +def setup_shiny(): + '''Manage a Shiny instance.''' + + name = 'shiny' + def _get_shiny_cmd(port): + conf = dedent(""" + run_as {user}; + server {{ + listen {port}; + location / {{ + site_dir {site_dir}; + log_dir {site_dir}/logs; + directory_index on; + }} + }} + """).format( + user=getpass.getuser(), + port=str(port), + site_dir=os.getcwd() + ) + + f = tempfile.NamedTemporaryFile(mode='w', delete=False) + f.write(conf) + f.close() + return ['shiny-server', f.name] + + return { + 'command': _get_shiny_cmd + } + +def setup_rstudio(): + # Detect various environment variables rsession requires to run + # Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp + cmd = ['R', '--slave', '--vanilla', '-e', + 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] + + r_output = subprocess.check_output(cmd) + R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ + r_output.decode().split(':') + + environment = { + 'R_DOC_DIR': R_DOC_DIR, + 'R_HOME': R_HOME, + 'R_INCLUDE_DIR': R_INCLUDE_DIR, + 'R_SHARE_DIR': R_SHARE_DIR, + 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, + 'RSTUDIO_DEFAULT_R_VERSION': version, + } + + return { + 'command': [ + 'rsession', + '--standalone=1', + '--program-mode=server', + '--log-stderr=1', + '--session-timeout-minutes=0', + '--user-identity=' + getpass.getuser(), + '--www-port={port}' + ], + 'environment': environment + } \ No newline at end of file diff --git a/nbrsessionproxy/handlers.py b/nbrsessionproxy/handlers.py deleted file mode 100644 index d3cb8394..00000000 --- a/nbrsessionproxy/handlers.py +++ /dev/null @@ -1,124 +0,0 @@ -# vim: set et sw=4 ts=4: -import os -import getpass -import pwd -import tempfile - -from urllib.parse import urlunparse, urlparse - -from tornado import web -import subprocess - -from notebook.utils import url_path_join as ujoin -from notebook.base.handlers import IPythonHandler - -from nbserverproxy.handlers import SuperviseAndProxyHandler - - -class AddSlashHandler(IPythonHandler): - """Handler for adding trailing slash to URLs that need them""" - @web.authenticated - def get(self, *args): - src = urlparse(self.request.uri) - dest = src._replace(path=src.path + '/') - self.redirect(urlunparse(dest)) - -def detectR(): - '''Detect R's version, R_HOME, and various other directories that rsession - requires. - - Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp''' - - cmd = ['R', '--slave', '--vanilla', '-e', - 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] - - p = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if p.returncode != 0: - raise Exception('Error detecting R') - R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ - p.stdout.decode().split(':') - - return { - 'R_DOC_DIR': R_DOC_DIR, - 'R_HOME': R_HOME, - 'R_INCLUDE_DIR': R_INCLUDE_DIR, - 'R_SHARE_DIR': R_SHARE_DIR, - 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, - 'RSTUDIO_DEFAULT_R_VERSION': version, - } - - - -class RSessionProxyHandler(SuperviseAndProxyHandler): - '''Manage an RStudio rsession instance.''' - - name = 'rsession' - - def get_env(self): - env = {} - try: - r_vars = detectR() - env.update(r_vars) - except: - raise web.HTTPError(reason='could not detect R', status_code=500) - - # rserver needs USER to be set to something sensible, - # otherwise it'll throw up an authentication page - if not os.environ.get('USER', ''): - env['USER'] = getpass.getuser() - - return env - - def get_cmd(self): - # rsession command. Augmented with user-identity and www-port. - return [ - 'rsession', - '--standalone=1', - '--program-mode=server', - '--log-stderr=1', - '--session-timeout-minutes=0', - '--user-identity=' + getpass.getuser(), - '--www-port=' + str(self.port) - ] - -class ShinyProxyHandler(SuperviseAndProxyHandler): - '''Manage a Shiny instance.''' - - name = 'shiny' - conf_tmpl = """run_as {user}; -server {{ - listen {port}; - location / {{ - site_dir {site_dir}; - log_dir {site_dir}/logs; - directory_index on; - }} -}} -""" - - def write_conf(self, user, port, site_dir): - '''Create a configuration file and return its name.''' - conf = self.conf_tmpl.format(user=user, port=port, site_dir=site_dir) - f = tempfile.NamedTemporaryFile(mode='w', delete=False) - f.write(conf) - f.close() - return f.name - - def get_env(self): - return {} - - def get_cmd(self): - user = getpass.getuser() - site_dir = pwd.getpwnam(user).pw_dir - filename = self.write_conf(user, self.port, site_dir) - - # shiny command. - return [ 'shiny-server', filename ] - -def setup_handlers(web_app): - web_app.add_handlers('.*', [ - (ujoin(web_app.settings['base_url'], 'rstudio/(.*)'), RSessionProxyHandler, dict(state={})), - (ujoin(web_app.settings['base_url'], 'shiny/(.*)'), ShinyProxyHandler, dict(state={})), - (ujoin(web_app.settings['base_url'], 'rstudio'), AddSlashHandler), - (ujoin(web_app.settings['base_url'], 'shiny'), AddSlashHandler) - ]) diff --git a/setup.py b/setup.py index c0fbac85..18796df3 100644 --- a/setup.py +++ b/setup.py @@ -14,4 +14,10 @@ 'nbserverproxy >= 0.8.8' ], package_data={'nbrsessionproxy': ['static/*']}, + entry_points={ + 'jupyter_serverproxy_servers': [ + 'rstudio = nbrsessionproxy:setup_rstudio', + 'shiny = nbrsessionproxy:setup_shiny' + ] + } ) From 2bb81126448388059550c76e2e91a3f9dc567808 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sun, 30 Dec 2018 21:31:34 -0800 Subject: [PATCH 112/125] Determine rsession environment variables only on demand Notebook server will no longer exit at startup if there is no R --- nbrsessionproxy/__init__.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index 155fb1ef..f0cfc345 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -35,23 +35,24 @@ def _get_shiny_cmd(port): } def setup_rstudio(): - # Detect various environment variables rsession requires to run - # Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp - cmd = ['R', '--slave', '--vanilla', '-e', - 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] + def _get_rsession_env(port): + # Detect various environment variables rsession requires to run + # Via rstudio's src/cpp/core/r_util/REnvironmentPosix.cpp + cmd = ['R', '--slave', '--vanilla', '-e', + 'cat(paste(R.home("home"),R.home("share"),R.home("include"),R.home("doc"),getRversion(),sep=":"))'] - r_output = subprocess.check_output(cmd) - R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ - r_output.decode().split(':') + r_output = subprocess.check_output(cmd) + R_HOME, R_SHARE_DIR, R_INCLUDE_DIR, R_DOC_DIR, version = \ + r_output.decode().split(':') - environment = { - 'R_DOC_DIR': R_DOC_DIR, - 'R_HOME': R_HOME, - 'R_INCLUDE_DIR': R_INCLUDE_DIR, - 'R_SHARE_DIR': R_SHARE_DIR, - 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, - 'RSTUDIO_DEFAULT_R_VERSION': version, - } + return { + 'R_DOC_DIR': R_DOC_DIR, + 'R_HOME': R_HOME, + 'R_INCLUDE_DIR': R_INCLUDE_DIR, + 'R_SHARE_DIR': R_SHARE_DIR, + 'RSTUDIO_DEFAULT_R_VERSION_HOME': R_HOME, + 'RSTUDIO_DEFAULT_R_VERSION': version, + } return { 'command': [ @@ -63,5 +64,5 @@ def setup_rstudio(): '--user-identity=' + getpass.getuser(), '--www-port={port}' ], - 'environment': environment + 'environment': _get_rsession_env } \ No newline at end of file From ff2afa3fc4db3c2270c8209ff4919c6aab5a0b52 Mon Sep 17 00:00:00 2001 From: ryanlovett Date: Sun, 30 Dec 2018 23:38:14 -0800 Subject: [PATCH 113/125] Remove note about rstudio-server. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 821c36e2..0673d83c 100644 --- a/README.md +++ b/README.md @@ -49,5 +49,3 @@ The Dockerfile contains an example installation on top of [jupyter/r-notebook](h ### Multiuser Considerations This extension launches an rstudio server process from the jupyter notebook server. This is fine in JupyterHub deployments where user servers are containerized since other users cannot connect to the rstudio server port. In non-containerized JupyterHub deployments, for example on multiuser systems running LocalSpawner or BatchSpawner, this not secure. Any user may connect to rstudio server and run arbitrary code. - -Additionally, rstudio-server expects to write to `/tmp/rstudio-server/secure-cookie-key`, which means without separate mount namespaces for /tmp, only one user can run rstudio server at a time. From c14879838cc20ecbd3d764d09cbae0dce60f40e7 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Mon, 31 Dec 2018 11:58:01 -0800 Subject: [PATCH 114/125] Add titles for RStudio & Shiny entries This adds entries to the 'New' menu under notebook, while removing our own nbextension --- nbrsessionproxy/__init__.py | 6 +++-- nbrsessionproxy/static/tree.js | 45 ---------------------------------- setup.py | 1 - 3 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 nbrsessionproxy/static/tree.js diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index f0cfc345..43f80551 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -31,7 +31,8 @@ def _get_shiny_cmd(port): return ['shiny-server', f.name] return { - 'command': _get_shiny_cmd + 'command': _get_shiny_cmd, + 'title': 'Shiny' } def setup_rstudio(): @@ -64,5 +65,6 @@ def _get_rsession_env(port): '--user-identity=' + getpass.getuser(), '--www-port={port}' ], - 'environment': _get_rsession_env + 'environment': _get_rsession_env, + 'title': 'RStudio' } \ No newline at end of file diff --git a/nbrsessionproxy/static/tree.js b/nbrsessionproxy/static/tree.js deleted file mode 100644 index f1a207a9..00000000 --- a/nbrsessionproxy/static/tree.js +++ /dev/null @@ -1,45 +0,0 @@ -define(function(require) { - var $ = require('jquery'); - var Jupyter = require('base/js/namespace'); - var utils = require('base/js/utils'); - - var base_url = utils.get_body_data('baseUrl'); - - - function load() { - if (!Jupyter.notebook_list) return; - - /* locate the right-side dropdown menu of apps and notebooks */ - var menu = $('.tree-buttons').find('.dropdown-menu'); - - /* create a divider */ - var divider = $('
  • ') - .attr('role', 'presentation') - .addClass('divider'); - - /* add the divider */ - menu.append(divider); - - /* create our list item */ - var rsession_item = $('
  • ') - .attr('role', 'presentation') - .addClass('new-rstudio'); - - /* create our list item's link */ - var rsession_link = $('') - .attr('role', 'menuitem') - .attr('tabindex', '-1') - .attr('href', base_url + 'rstudio/') - .attr('target', '_blank') - .text('RStudio Session'); - - /* add the link to the item and - * the item to the menu */ - rsession_item.append(rsession_link); - menu.append(rsession_item); - } - - return { - load_ipython_extension: load - }; -}); diff --git a/setup.py b/setup.py index 18796df3..12aa92d9 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ 'notebook', 'nbserverproxy >= 0.8.8' ], - package_data={'nbrsessionproxy': ['static/*']}, entry_points={ 'jupyter_serverproxy_servers': [ 'rstudio = nbrsessionproxy:setup_rstudio', From 55324e767801ce2e2b42e83dace419c9e63b18e8 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Mon, 31 Dec 2018 16:08:56 -0800 Subject: [PATCH 115/125] Add icons for JupyterLab launchers --- nbrsessionproxy/__init__.py | 6 ++- nbrsessionproxy/icons/rstudio.svg | 1 + nbrsessionproxy/icons/shiny.svg | 65 +++++++++++++++++++++++++++++++ setup.py | 3 +- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 nbrsessionproxy/icons/rstudio.svg create mode 100644 nbrsessionproxy/icons/shiny.svg diff --git a/nbrsessionproxy/__init__.py b/nbrsessionproxy/__init__.py index 43f80551..2267fd70 100644 --- a/nbrsessionproxy/__init__.py +++ b/nbrsessionproxy/__init__.py @@ -32,7 +32,8 @@ def _get_shiny_cmd(port): return { 'command': _get_shiny_cmd, - 'title': 'Shiny' + 'title': 'Shiny', + 'icon': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'shiny.svg') } def setup_rstudio(): @@ -66,5 +67,6 @@ def _get_rsession_env(port): '--www-port={port}' ], 'environment': _get_rsession_env, - 'title': 'RStudio' + 'title': 'RStudio', + 'icon': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'rstudio.svg') } \ No newline at end of file diff --git a/nbrsessionproxy/icons/rstudio.svg b/nbrsessionproxy/icons/rstudio.svg new file mode 100644 index 00000000..3fb496e1 --- /dev/null +++ b/nbrsessionproxy/icons/rstudio.svg @@ -0,0 +1 @@ + diff --git a/nbrsessionproxy/icons/shiny.svg b/nbrsessionproxy/icons/shiny.svg new file mode 100644 index 00000000..c6cc72a6 --- /dev/null +++ b/nbrsessionproxy/icons/shiny.svg @@ -0,0 +1,65 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/setup.py b/setup.py index 12aa92d9..2ba1cb0f 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,6 @@ 'rstudio = nbrsessionproxy:setup_rstudio', 'shiny = nbrsessionproxy:setup_shiny' ] - } + }, + include_package_data=True ) From b9b0bc4efb5afb4f15c4d6f630d1cecd8215250f Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Mon, 31 Dec 2018 16:11:28 -0800 Subject: [PATCH 116/125] Remove custom jupyterlab extension --- jupyterlab-rsessionproxy/package.json | 53 ----------- jupyterlab-rsessionproxy/src/index.ts | 88 ------------------- .../style/images/rstudio.svg | 1 - jupyterlab-rsessionproxy/style/index.css | 29 ------ jupyterlab-rsessionproxy/tsconfig.json | 16 ---- 5 files changed, 187 deletions(-) delete mode 100644 jupyterlab-rsessionproxy/package.json delete mode 100644 jupyterlab-rsessionproxy/src/index.ts delete mode 100644 jupyterlab-rsessionproxy/style/images/rstudio.svg delete mode 100644 jupyterlab-rsessionproxy/style/index.css delete mode 100644 jupyterlab-rsessionproxy/tsconfig.json diff --git a/jupyterlab-rsessionproxy/package.json b/jupyterlab-rsessionproxy/package.json deleted file mode 100644 index 8721890e..00000000 --- a/jupyterlab-rsessionproxy/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@jupyterlab/rsessionproxy-extension", - "version": "0.1.3", - "description": "JupyterLab - RSession Proxy Extension", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "files": [ - "lib/*.d.ts", - "lib/*.js", - "style/images/*.*", - "style/*.css" - ], - "directories": { - "lib": "lib/" - }, - "jupyterlab": { - "extension": true - }, - "dependencies": { - "@jupyterlab/apputils": "^0.19.1", - "@jupyterlab/application": "^0.19.1", - "@jupyterlab/mainmenu": "^0.8.1", - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/launcher": "^0.19.1", - "@jupyterlab/services": "^3.2.1", - "@phosphor/messaging": "^1.2.2", - "@phosphor/widgets": "^1.6.0" - }, - "devDependencies": { - "rimraf": "^2.6.2", - "typescript": "~2.9.2" - }, - "keywords": [ - "jupyter", - "jupyterlab" - ], - "scripts": { - "build": "tsc", - "clean": "rimraf lib", - "prepublish": "npm run build", - "watch": "tsc -w" - }, - "repository": { - "type": "git", - "url": "https://github.com/jupyterhub/nbrsessionproxy.git" - }, - "author": "Project Jupyter", - "license": "BSD-3-Clause", - "bugs": { - "url": "https://github.com/jupyterhub/nbrsessionproxy/issues" - }, - "homepage": "https://github.com/jupyterhub/nbrsessionproxy" -} diff --git a/jupyterlab-rsessionproxy/src/index.ts b/jupyterlab-rsessionproxy/src/index.ts deleted file mode 100644 index 778ac9c0..00000000 --- a/jupyterlab-rsessionproxy/src/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Jupyter Development Team. -// Distributed under the terms of the Modified BSD License. - -import { - JupyterLab, JupyterLabPlugin -} from '@jupyterlab/application'; - -import { - ICommandPalette -} from '@jupyterlab/apputils'; - -import { - IMainMenu -} from '@jupyterlab/mainmenu'; - -import { - Message -} from '@phosphor/messaging'; - -import { - Menu -} from '@phosphor/widgets'; - -import { - PageConfig, URLExt -} from '@jupyterlab/coreutils'; - -import '../style/index.css'; - -/** - * The command IDs used by the rstudio plugin. - */ -namespace CommandIDs { - export - const launch = 'rsession:launch'; -}; - -/** - * The class name for the rstudio icon - */ -const RSTUDIO_ICON_CLASS = 'jp-RStudioIcon'; - - -/** - * Activate the rsession extension. - */ -function activate(app: JupyterLab, palette: ICommandPalette, mainMenu: IMainMenu): void { - let counter = 0; - const category = 'RStudio'; - const namespace = 'rsession-proxy'; - const command = CommandIDs.launch; - const { commands, shell } = app; - - commands.addCommand(command, { - label: 'Launch RStudio', - caption: 'Start a new Rstudio Session', - execute: () => { - window.open(PageConfig.getBaseUrl() + 'rstudio/', 'RStudio Session'); - } - }); - - // Add commands and menu itmes. - let menu = new Menu({ commands }); - menu.title.label = category; - [ - CommandIDs.launch, - ].forEach(command => { - palette.addItem({ command, category }); - menu.addItem({ command }); - }); - mainMenu.addMenu(menu, {rank: 98}); -} - -/** - * The rsession handler extension. - */ -const plugin: JupyterLabPlugin = { - id: 'jupyterlab_rsessionproxy', - autoStart: true, - requires: [ICommandPalette, IMainMenu], - activate: activate, -}; - - -/** - * Export the plugin as default. - */ -export default plugin; diff --git a/jupyterlab-rsessionproxy/style/images/rstudio.svg b/jupyterlab-rsessionproxy/style/images/rstudio.svg deleted file mode 100644 index 3fb496e1..00000000 --- a/jupyterlab-rsessionproxy/style/images/rstudio.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/jupyterlab-rsessionproxy/style/index.css b/jupyterlab-rsessionproxy/style/index.css deleted file mode 100644 index 1a46ba13..00000000 --- a/jupyterlab-rsessionproxy/style/index.css +++ /dev/null @@ -1,29 +0,0 @@ -/*----------------------------------------------------------------------------- -| Copyright (c) Jupyter Development Team. -| Distributed under the terms of the Modified BSD License. -|----------------------------------------------------------------------------*/ - - -.jp-RStudioIcon { - background-image: url(images/rstudio.svg); -} -.jp-Rsession { - min-width: 480px; - background: white; -} - - -.jp-Rsession::before { - content: ''; - display: block; - height: var(--jp-toolbar-micro-height); - background: var(--jp-toolbar-background); - border-bottom: 1px solid var(--jp-toolbar-border-color); - box-shadow: var(--jp-toolbar-box-shadow); - z-index: 1; -} - - -.jp-Rsession > iframe { - border: none; -} diff --git a/jupyterlab-rsessionproxy/tsconfig.json b/jupyterlab-rsessionproxy/tsconfig.json deleted file mode 100644 index 0a6ecc07..00000000 --- a/jupyterlab-rsessionproxy/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "noImplicitAny": true, - "noEmitOnError": true, - "noUnusedLocals": false, - "module": "commonjs", - "moduleResolution": "node", - "target": "ES5", - "outDir": "./lib", - "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"], - "types": [] - }, - "include": ["src/*"] -} - From 53db92d0634c27408749fddb4ab2c9c78bd3dd60 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 09:29:51 -0800 Subject: [PATCH 117/125] Rename nbrsessionproxy to jupyter-rsession-proxy --- .gitignore | 2 +- Dockerfile | 8 +---- README.md | 32 ++++--------------- .../__init__.py | 0 .../icons/rstudio.svg | 0 .../icons/shiny.svg | 0 setup.py | 15 ++++----- 7 files changed, 15 insertions(+), 42 deletions(-) rename {nbrsessionproxy => jupyter_rsession_proxy}/__init__.py (100%) rename {nbrsessionproxy => jupyter_rsession_proxy}/icons/rstudio.svg (100%) rename {nbrsessionproxy => jupyter_rsession_proxy}/icons/shiny.svg (100%) diff --git a/.gitignore b/.gitignore index b399da28..bee8a64b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -nbrsessionproxy/__pycache__ +__pycache__ diff --git a/Dockerfile b/Dockerfile index e995d493..2c09ebb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,13 +23,7 @@ RUN apt-get clean && \ USER $NB_USER -RUN pip install git+https://github.com/jupyterhub/nbserverproxy.git -RUN jupyter serverextension enable --sys-prefix --py nbserverproxy - -RUN pip install git+https://github.com/jupyterhub/nbrsessionproxy.git -RUN jupyter serverextension enable --sys-prefix --py nbrsessionproxy -RUN jupyter nbextension install --sys-prefix --py nbrsessionproxy -RUN jupyter nbextension enable --sys-prefix --py nbrsessionproxy +RUN pip install git+https://github.com/jupyterhub/jupyter-rsession-proxy # The desktop package uses /usr/lib/rstudio/bin ENV PATH="${PATH}:/usr/lib/rstudio-server/bin" diff --git a/README.md b/README.md index 0673d83c..726cd7e2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# nbrsessionproxy +# jupyter-rsession-proxy -**nbrsessionproxy** provides Jupyter server and notebook extensions to proxy RStudio. +**jupyter-rsession-proxy** provides Jupyter server and notebook extensions to proxy RStudio. ![Screenshot](screenshot.png) -If you have a JupyterHub deployment, nbrsessionproxy can take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. +If you have a JupyterHub deployment, jupyter-rsession-proxy can take advantage of JupyterHub's existing authenticator and spawner to launch RStudio in users' Jupyter environments. You can also run this from within Jupyter. Note that [RStudio Server Pro](https://www.rstudio.com/products/rstudio-server-pro/architecture) has more featureful authentication and spawning than the standard version, in the event that you do not want to use Jupyter's. ## Installation @@ -16,31 +16,11 @@ Use conda `conda install rstudio` or [download](https://www.rstudio.com/products Note that rstudio server is needed to work with this extension. -### Install nbrsessionproxy -Install the library: -``` -pip install nbrsessionproxy -``` -or -``` -conda install -c conda-forge nbrsessionproxy -``` +### Install jupyter-rsession-proxy -If installing via pip, you need to enable the extension. - -``` -jupyter serverextension enable --py --sys-prefix nbrsessionproxy -jupyter nbextension install --py --sys-prefix nbrsessionproxy -jupyter nbextension enable --py --sys-prefix nbrsessionproxy -``` - -For JupyterLab first clone this repository to a known location and -install from the directory. +Install the library: ``` -git clone https://github.com/jupyterhub/nbrsessionproxy /opt/nbrsessionproxy -pip install -e /opt/nbrsessionproxy -jupyter serverextension enable --py nbrsessionproxy -jupyter labextension link /opt/nbrsessionproxy/jupyterlab-rsessionproxy +pip install jupyter-rsession-proxy ``` The Dockerfile contains an example installation on top of [jupyter/r-notebook](https://github.com/jupyter/docker-stacks/tree/master/r-notebook). diff --git a/nbrsessionproxy/__init__.py b/jupyter_rsession_proxy/__init__.py similarity index 100% rename from nbrsessionproxy/__init__.py rename to jupyter_rsession_proxy/__init__.py diff --git a/nbrsessionproxy/icons/rstudio.svg b/jupyter_rsession_proxy/icons/rstudio.svg similarity index 100% rename from nbrsessionproxy/icons/rstudio.svg rename to jupyter_rsession_proxy/icons/rstudio.svg diff --git a/nbrsessionproxy/icons/shiny.svg b/jupyter_rsession_proxy/icons/shiny.svg similarity index 100% rename from nbrsessionproxy/icons/shiny.svg rename to jupyter_rsession_proxy/icons/shiny.svg diff --git a/setup.py b/setup.py index 2ba1cb0f..1fe19eed 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,21 @@ import setuptools setuptools.setup( - name="nbrsessionproxy", - version='0.8.0', - url="https://github.com/jupyterhub/nbrsessionproxy", - author="Ryan Lovett", + name="jupyter-rsession-proxy", + version='1.0dev', + url="https://github.com/jupyterhub/jupyter-rsession-proxy", + author="Ryan Lovett & Yuvi Panda", description="Jupyter extension to proxy RStudio's rsession", packages=setuptools.find_packages(), keywords=['Jupyter'], classifiers=['Framework :: Jupyter'], install_requires=[ - 'notebook', - 'nbserverproxy >= 0.8.8' + 'jupyter-server-proxy' ], entry_points={ 'jupyter_serverproxy_servers': [ - 'rstudio = nbrsessionproxy:setup_rstudio', - 'shiny = nbrsessionproxy:setup_shiny' + 'rstudio = jupyter_rsession_proxy:setup_rstudio', + 'shiny = jupyter_rsession_proxy:setup_shiny' ] }, include_package_data=True From 490f35b0aea71ee5d65993c73b36b17ec726c822 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 09:34:03 -0800 Subject: [PATCH 118/125] Add MANIFEST.in file to include icons in package --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..c320e7b7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include nbrsessionproxy/icons From 6213af091ba44fb6121cc86b8ca369a0369667cc Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 10:51:39 -0800 Subject: [PATCH 119/125] Use newer package name in MANIFEST.in --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c320e7b7..718c4134 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -recursive-include nbrsessionproxy/icons +recursive-include jupyter_rsession_proxy/icons From da68f744cf08f42e668aa629580c9d34c41232c0 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 14:18:25 -0800 Subject: [PATCH 120/125] Use package_data instead of MANIFEST.in MANIFEST.in wasn't actually working, and package_data is clearer --- MANIFEST.in | 1 - setup.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 718c4134..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-include jupyter_rsession_proxy/icons diff --git a/setup.py b/setup.py index 1fe19eed..9b8480f3 100644 --- a/setup.py +++ b/setup.py @@ -18,5 +18,8 @@ 'shiny = jupyter_rsession_proxy:setup_shiny' ] }, + package_data={ + 'jupyter_rsession_proxy': ['icons/*'], + }, include_package_data=True ) From ef06ba57ac916462e191be4f2ab5c11534318051 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 15:12:16 -0800 Subject: [PATCH 121/125] Remove include_package_data=True This looks for MANIFEST.in and seems to ignore package_data? Python Packaging is messy :( --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 9b8480f3..03d29222 100644 --- a/setup.py +++ b/setup.py @@ -21,5 +21,4 @@ package_data={ 'jupyter_rsession_proxy': ['icons/*'], }, - include_package_data=True ) From b1e9a27bb2bd29196c533f327ca3f319094581e1 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 16:55:41 -0800 Subject: [PATCH 122/125] Detect path of rsession in default rstudio install automatically For some reason the rstudio deb doesn't put executables in the normal locations --- jupyter_rsession_proxy/__init__.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/jupyter_rsession_proxy/__init__.py b/jupyter_rsession_proxy/__init__.py index 2267fd70..2d1950de 100644 --- a/jupyter_rsession_proxy/__init__.py +++ b/jupyter_rsession_proxy/__init__.py @@ -2,6 +2,7 @@ import tempfile import subprocess import getpass +import shutil from textwrap import dedent def setup_shiny(): @@ -56,16 +57,25 @@ def _get_rsession_env(port): 'RSTUDIO_DEFAULT_R_VERSION': version, } - return { - 'command': [ - 'rsession', + def _get_rsession_cmd(port): + if shutil.which('rsession'): + executable = 'rsession' + else: + # Default path for rsession if installed with the deb package + executable = '/usr/lib/rstudio-server/bin/rsession' + + return [ + executable, '--standalone=1', '--program-mode=server', '--log-stderr=1', '--session-timeout-minutes=0', '--user-identity=' + getpass.getuser(), - '--www-port={port}' - ], + '--www-port=' + str(port) + ] + + return { + 'command': _get_rsession_cmd, 'environment': _get_rsession_env, 'title': 'RStudio', 'icon': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'rstudio.svg') From cf844be19bd52543386d53922709a299aae0012c Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Wed, 2 Jan 2019 17:01:41 -0800 Subject: [PATCH 123/125] Look for rsession in more paths Fail explicitly if rsession isn't found --- jupyter_rsession_proxy/__init__.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/jupyter_rsession_proxy/__init__.py b/jupyter_rsession_proxy/__init__.py index 2d1950de..7faa8645 100644 --- a/jupyter_rsession_proxy/__init__.py +++ b/jupyter_rsession_proxy/__init__.py @@ -58,11 +58,22 @@ def _get_rsession_env(port): } def _get_rsession_cmd(port): + # Other paths rsession maybe in + other_paths = [ + # When rstudio-server deb is installed + '/usr/lib/rstudio-server/bin/rsession', + # When just rstudio deb is installed + '/usr/lib/rstudio/bin/rsession', + ] if shutil.which('rsession'): executable = 'rsession' else: - # Default path for rsession if installed with the deb package - executable = '/usr/lib/rstudio-server/bin/rsession' + for op in other_paths: + if os.path.exists(op): + executable = op + break + else: + raise FileNotFoundError('Can not find rsession in PATH') return [ executable, From d969f092904a25b243573063bb3617c54be7f887 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Thu, 3 Jan 2019 00:05:13 -0800 Subject: [PATCH 124/125] Update config format --- jupyter_rsession_proxy/__init__.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jupyter_rsession_proxy/__init__.py b/jupyter_rsession_proxy/__init__.py index 7faa8645..b7e67f01 100644 --- a/jupyter_rsession_proxy/__init__.py +++ b/jupyter_rsession_proxy/__init__.py @@ -33,8 +33,10 @@ def _get_shiny_cmd(port): return { 'command': _get_shiny_cmd, - 'title': 'Shiny', - 'icon': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'shiny.svg') + 'launcher_entry': { + 'title': 'Shiny', + 'icon_path': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'shiny.svg') + } } def setup_rstudio(): @@ -88,6 +90,8 @@ def _get_rsession_cmd(port): return { 'command': _get_rsession_cmd, 'environment': _get_rsession_env, - 'title': 'RStudio', - 'icon': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'rstudio.svg') + 'launcher_entry': { + 'title': 'RStudio', + 'icon_path': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icons', 'rstudio.svg') + } } \ No newline at end of file From 1e260bf01603432b87173760a47c4753fc84a945 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Mon, 4 Feb 2019 09:59:32 -0800 Subject: [PATCH 125/125] Prepare jupyter-rserver-proxy for merging - Move everything under contrib/rstudio - Remove extrenous LICENSE, CONTRIBUTING, travis & gitignore files --- .gitignore | 1 - .travis.yml | 14 ------- CONTRIBUTING.md | 3 -- LICENSE | 29 --------------- LICENSE.txt | 35 ------------------ Dockerfile => contrib/rstudio/Dockerfile | 0 README.md => contrib/rstudio/README.md | 0 .../jupyter_rsession_proxy}/__init__.py | 0 .../jupyter_rsession_proxy}/icons/rstudio.svg | 0 .../jupyter_rsession_proxy}/icons/shiny.svg | 0 contrib/rstudio/screenshot.png | Bin 0 -> 25448 bytes setup.py => contrib/rstudio/setup.py | 0 12 files changed, 82 deletions(-) delete mode 100644 .gitignore delete mode 100644 .travis.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 LICENSE.txt rename Dockerfile => contrib/rstudio/Dockerfile (100%) rename README.md => contrib/rstudio/README.md (100%) rename {jupyter_rsession_proxy => contrib/rstudio/jupyter_rsession_proxy}/__init__.py (100%) rename {jupyter_rsession_proxy => contrib/rstudio/jupyter_rsession_proxy}/icons/rstudio.svg (100%) rename {jupyter_rsession_proxy => contrib/rstudio/jupyter_rsession_proxy}/icons/shiny.svg (100%) create mode 100644 contrib/rstudio/screenshot.png rename setup.py => contrib/rstudio/setup.py (100%) diff --git a/.gitignore b/.gitignore deleted file mode 100644 index bee8a64b..00000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9d8edbd9..00000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python -python: -- 3.5 -script: -- true -deploy: - provider: pypi - user: nbrsessionproxy - password: - secure: JilUEN99KU/dZbBtStWLvJ7EdU6gud8/9C58ZhA2p8B7XgGF0GRG9/Sved9wxab1J5AL1GiweazxSn7WJVleBelNaKRmImVyukHcPIw5hS01+eE1KAWMcimPPB3Mp5JxfmupO2vaWJu5C2Lp5VM5MLuRHtxLTqYNW2qUNOmh0fpNrUIF+ATRKaDXaGE5o68FB/UZ9J91FoTK2GUcFFjmTwC4qz10udfHDMTE0YzsgyDBiX8RdeGdDzVLSP2kjdm0k/zORNhTPnzDhUX9wLAaZh5pRxYrS2Wy0C2u55CvcV/YxZiSOq9FY6Eblm8SJSOl+kaQw8sj20J7DFIwVKBpimTa/k2Byh8JrjeCO3wyZ/UfPycmRC73V5nxRBrDcOvr83j8SYhOcD6/6OwN+dnkoYJYak2YGs8EM6d78cuZ5CEl6k0nPsdr6nd3thefBrOGYZN9AehnF9fHFRwAHzKvObfJ0UxB/uyh8VIvSRqYHVxgyBj6U1erXrhkjqWTVcAAvv9mjLnSQG7YePtmaYj3qPreZ5mAj9QaTO8XE1e6oiv+5MDrTXfK7kKocXNijs30+SQBk9M/euuaxzFcXrCe98hKIKMqzzNrC9S/NTQ/13WdgxQL6At/7Tcsxep+rqDHL9T7lsJ6t1ydvXMk4AnZQaA77FZQvq2RCDPmyktYmxI= - on: - tags: true - repo: jupyterhub/nbrsessionproxy - distributions: sdist bdist_wheel diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 815a9af1..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,3 +0,0 @@ -# Contributing - -Welcome! As a [Jupyter](https://jupyter.org) project, we follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html). diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0dc89813..00000000 --- a/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2017, Project Jupyter Contributors -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 919c332a..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,35 +0,0 @@ -BSD 3-Clause License - - - Copyright (c) 2017, Project Jupyter Contributors - All rights reserved. - - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - - * Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Dockerfile b/contrib/rstudio/Dockerfile similarity index 100% rename from Dockerfile rename to contrib/rstudio/Dockerfile diff --git a/README.md b/contrib/rstudio/README.md similarity index 100% rename from README.md rename to contrib/rstudio/README.md diff --git a/jupyter_rsession_proxy/__init__.py b/contrib/rstudio/jupyter_rsession_proxy/__init__.py similarity index 100% rename from jupyter_rsession_proxy/__init__.py rename to contrib/rstudio/jupyter_rsession_proxy/__init__.py diff --git a/jupyter_rsession_proxy/icons/rstudio.svg b/contrib/rstudio/jupyter_rsession_proxy/icons/rstudio.svg similarity index 100% rename from jupyter_rsession_proxy/icons/rstudio.svg rename to contrib/rstudio/jupyter_rsession_proxy/icons/rstudio.svg diff --git a/jupyter_rsession_proxy/icons/shiny.svg b/contrib/rstudio/jupyter_rsession_proxy/icons/shiny.svg similarity index 100% rename from jupyter_rsession_proxy/icons/shiny.svg rename to contrib/rstudio/jupyter_rsession_proxy/icons/shiny.svg diff --git a/contrib/rstudio/screenshot.png b/contrib/rstudio/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..fc7d336a17be1f096f0726e472140a8a320c9fbb GIT binary patch literal 25448 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 SC+m6Ou#%j*Y^AhC$bSGyB((?t literal 0 HcmV?d00001 diff --git a/setup.py b/contrib/rstudio/setup.py similarity index 100% rename from setup.py rename to contrib/rstudio/setup.py