From 221dac4932cc68660dd822b7685a9785fb405c64 Mon Sep 17 00:00:00 2001 From: Jonathan Curran Date: Wed, 9 Jan 2019 13:48:01 -0700 Subject: [PATCH] Show logs while deploying (#146) --- CHANGELOG.md | 1 + docs/guide/README.md | 10 +++ rsconnect_jupyter/__init__.py | 30 ++++++++- rsconnect_jupyter/api.py | 27 +++----- rsconnect_jupyter/static/connect.js | 95 +++++++++++++++++++++++++---- rsconnect_jupyter/static/main.css | 7 +++ 6 files changed, 138 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edbf5708..53e4e648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Renamed plugin from `rsconnect` to `rsconnect_jupyter` - Publish notebooks with source allowing them to be rendered on RStudio Connect. `pip` and `virtualenv` are used to determine dependent packages. +- Deploy logs are shown while notebook is deployed. # v1.0.1 - Ensure json is decoded as utf-8 diff --git a/docs/guide/README.md b/docs/guide/README.md index 5ec16bdf..9059fb50 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -12,6 +12,8 @@ Connect](https://www.rstudio.com/products/connect/). - Jupyter Notebook 5.x - [pip](https://pypi.org/project/pip/) - [wheel](https://pypi.org/project/wheel/) +- [RStudio Connect](https://www.rstudio.com/products/connect/download-commercial/) v1.7.0 + or higher, configured with Python support. If using `conda`, `pip` and `wheel` should already be installed. @@ -34,6 +36,14 @@ virtualenv /my/path source /my/path/bin/activate ``` +Install Jupyter inside the `virtualenv`: +```bash +pip install jupyter +``` + +> Note: be sure to run Jupyter from the virtual environment, not from a global +> installation. + Install the `rsconnect-jupyter` package with the following command: ```bash diff --git a/rsconnect_jupyter/__init__.py b/rsconnect_jupyter/__init__.py index bc8170f8..f7956d59 100644 --- a/rsconnect_jupyter/__init__.py +++ b/rsconnect_jupyter/__init__.py @@ -8,7 +8,7 @@ from notebook.utils import url_path_join from tornado import web -from .api import app_get, app_search, deploy, verify_server, RSConnectException +from .api import app_config, app_get, app_search, deploy, task_get, verify_server, RSConnectException from .bundle import make_html_bundle, make_source_bundle __version__ = '1.0.0' @@ -122,11 +122,11 @@ def post(self, action): raise web.HTTPError(400, 'Invalid app_mode: %s, must be "static" or "jupyter-static"' % app_mode) try: - published_app = deploy(uri, api_key, app_id, nb_name, nb_title, bundle) + retval = deploy(uri, api_key, app_id, nb_name, nb_title, bundle) except RSConnectException as exc: raise web.HTTPError(400, exc.message) - self.finish(published_app) + self.finish(retval) return if action == 'app_get': @@ -159,6 +159,30 @@ def post(self, action): self.set_secure_cookie('key_' + address_hash, api_key, expires_days=3650) return + if action == 'get_log': + uri = urlparse(data['server_address']) + api_key = data['api_key'] + task_id = data['task_id'] + last_status = data['last_status'] + try: + retval = task_get(uri, api_key, task_id, last_status) + except RSConnectException as exc: + raise web.HTTPError(400, exc.message) + self.finish(json.dumps(retval)) + return + + if action == 'app_config': + uri = urlparse(data['server_address']) + api_key = data['api_key'] + app_id = data['app_id'] + try: + retval = app_config(uri, api_key, app_id) + except RSConnectException as exc: + raise web.HTTPError(400, exc.message) + self.finish(json.dumps(retval)) + return + + def load_jupyter_server_extension(nb_app): nb_app.log.info("rsconnect_jupyter enabled!") diff --git a/rsconnect_jupyter/api.py b/rsconnect_jupyter/api.py index 67a3715b..360efc4a 100644 --- a/rsconnect_jupyter/api.py +++ b/rsconnect_jupyter/api.py @@ -244,27 +244,20 @@ def deploy(uri, api_key, app_id, app_name, app_title, tarball): app_bundle = api.app_upload(app['id'], tarball) task_id = api.app_deploy(app['id'], app_bundle['id'])['id'] + return { + 'task_id': task_id, + 'app_id': app['id'], + } - # 10 minute timeout - timeout = 600 - task = wait_for_task(api, task_id, timeout) - if task is None: - raise RSConnectException('Deployment timed out') +def task_get(uri, api_key, task_id, last_status): + with RSConnect(uri, api_key) as api: + return api.task_get(task_id, first_status=last_status) - if task['code'] != 0: - # app failed to deploy - err_msg = 'Failed to deploy successfully' - if 'error' in task: - err_msg += ': ' + task['error'] - raise RSConnectException(err_msg) - # app deployed successfully - config = api.app_config(app['id']) - return { - 'app_id': app['id'], - 'config': config, - } +def app_config(uri, api_key, app_id): + with RSConnect(uri, api_key) as api: + return api.app_config(app_id) APP_MODE_STATIC = 4 diff --git a/rsconnect_jupyter/static/connect.js b/rsconnect_jupyter/static/connect.js index ca25d787..90fbb757 100644 --- a/rsconnect_jupyter/static/connect.js +++ b/rsconnect_jupyter/static/connect.js @@ -251,7 +251,14 @@ define([ var path = Jupyter.notebook.notebook_name; try { - var cmd = ["!", Jupyter.notebook.kernel_selector.kernelspecs[Jupyter.notebook.kernel.name].spec.argv[0], " -m rsconnect_jupyter.environment ${PWD}/", path].join(""); + var cmd = [ + "!", + Jupyter.notebook.kernel_selector.kernelspecs[ + Jupyter.notebook.kernel.name + ].spec.argv[0], + " -m rsconnect_jupyter.environment ${PWD}/", + path + ].join(""); console.log("executing: " + cmd); } catch (e) { return $.Deferred().reject(e); @@ -296,6 +303,67 @@ define([ var entry = this.servers[serverId]; + var $log = $("#rsc-log").attr("hidden", null); + $log.text("Deploying...\n"); + + function getLogs(deployResult) { + function inner(lastStatus) { + lastStatus = lastStatus || null; + return Utils.ajax({ + url: Jupyter.notebook.base_url + "rsconnect_jupyter/get_log", + method: "POST", + headers: { "Content-Type": "application/json" }, + data: JSON.stringify({ + server_address: entry.server, + api_key: apiKey, + task_id: deployResult["task_id"], + last_status: lastStatus + }) + }).then(function(result) { + if (result["last_status"] != lastStatus) { + lastStatus = result["lastStatus"]; + var output = result["status"].join("\n"); + $log.text(output); + // scroll to bottom + $log.scrollTop($log.get(0).scrollHeight); + } + if (result["finished"]) { + if (result["code"] != 0) { + return $.Deferred().reject( + "Failed to deploy successfully: " + result["error"] + ); + } + debug.info("logs:", result["status"].join("\n")); + return $.Deferred().resolve(deployResult["app_id"]); + } + var next = $.Deferred(); + setTimeout(function() { + return inner(lastStatus).then(next.resolve); + }, 1000); + return next; + }); + } + return inner(); + } + + function appConfig(receivedAppId) { + return Utils.ajax({ + url: Jupyter.notebook.base_url + "rsconnect_jupyter/app_config", + method: "POST", + headers: { "Content-Type": "application/json" }, + data: JSON.stringify({ + server_address: entry.server, + api_key: apiKey, + app_id: receivedAppId + }) + }).then(function(config) { + return { + appId: receivedAppId, + config: config + }; + }); + } + function deploy(environment) { var data = { notebook_path: notebookPath, @@ -313,18 +381,20 @@ define([ method: "POST", headers: { "Content-Type": "application/json" }, data: JSON.stringify(data) - }); + }) + .then(getLogs) + .then(appConfig); // update server with title and appId and set recently selected // server - xhr.then(function(result) { + xhr.then(function(configResult) { self.previousServerId = serverId; return self.updateServer( serverId, - result.app_id, + configResult.appId, notebookTitle, appMode, - result.config.config_url + configResult.config.config_url ); }); @@ -763,18 +833,23 @@ define([ " ", '
', ' ', - ' ', + ' ', ' Publish document with source code
', ' Choose this option if you want to create a scheduled report or rebuild your document on the server', "
", ' ', - ' ', + ' ', ' Publish finished document only
', ' Choose this option to publish a snapshot of the notebook as it appears in Jupyter', "
", ' ', "
", " ", + ' ', '
', ' ', "" @@ -910,11 +985,7 @@ define([ } function handleFailure(xhr) { - addValidationMarkup( - false, - txtTitle, - xhr.responseJSON.message - ); + addValidationMarkup(false, txtTitle, xhr.responseJSON.message); togglePublishButton(true); } diff --git a/rsconnect_jupyter/static/main.css b/rsconnect_jupyter/static/main.css index 2400fc9e..77ee6708 100644 --- a/rsconnect_jupyter/static/main.css +++ b/rsconnect_jupyter/static/main.css @@ -30,3 +30,10 @@ color: black; padding-left: 10px; } + +#rsc-log { + height: 10rem; + margin-top: 1rem; + overflow-y: scroll; + overflow: break-word; +}