From 71902059d397e95ed349008ab66c28677b61d16f Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 8 Jan 2014 13:02:36 -0800 Subject: [PATCH 1/5] Write notebook server info file in security directory --- IPython/html/notebookapp.py | 79 ++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 0dc0dc258..4a59892a5 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -19,6 +19,8 @@ from __future__ import print_function # stdlib import errno +import io +import json import logging import os import random @@ -72,6 +74,7 @@ from .base.handlers import AuthenticatedFileHandler, FileFindHandler from IPython.config.application import catch_config_error, boolean_flag from IPython.core.application import BaseIPythonApplication +from IPython.core.profiledir import ProfileDir from IPython.consoleapp import IPythonConsoleApp from IPython.kernel import swallow_argv from IPython.kernel.zmq.session import default_secure @@ -499,6 +502,12 @@ class NotebookApp(BaseIPythonApplication): "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL") ) + info_file = Unicode() + + def _info_file_default(self): + info_file = "nbserver-%s.json"%os.getpid() + return os.path.join(self.profile_dir.security_dir, info_file) + def parse_command_line(self, argv=None): super(NotebookApp, self).parse_command_line(argv) @@ -594,6 +603,20 @@ class NotebookApp(BaseIPythonApplication): 'no available port could be found.') self.exit(1) + @property + def display_url(self): + ip = self.ip if self.ip else '[all ip addresses on your system]' + return self._url(ip) + + @property + def connection_url(self): + ip = self.ip if self.ip else localhost() + return self._url(ip) + + def _url(self, ip): + proto = 'https' if self.certfile else 'http' + return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url) + def init_signal(self): if not sys.platform.startswith('win'): signal.signal(signal.SIGINT, self._handle_sigint) @@ -666,7 +689,6 @@ class NotebookApp(BaseIPythonApplication): elif status == 'unclean': self.log.warn("components submodule unclean, you may see 404s on static/components") self.log.warn("run `setup.py submodule` or `git submodule update` to update") - @catch_config_error def initialize(self, argv=None): @@ -691,33 +713,56 @@ class NotebookApp(BaseIPythonApplication): "Return the current working directory and the server url information" info = self.notebook_manager.info_string() + "\n" info += "%d active kernels \n" % len(self.kernel_manager._kernels) - return info + "The IPython Notebook is running at: %s" % self._url + return info + "The IPython Notebook is running at: %s" % self.display_url + + def server_info(self): + """Return a JSONable dict of information about this server.""" + return {'url': self.connection_url, + 'hostname': self.ip if self.ip else 'localhost', + 'port': self.port, + 'secure': bool(self.certfile), + 'baseurlpath': self.base_project_url, + 'notebookdir': os.path.abspath(self.notebook_manager.notebook_dir), + } + + def write_server_info_file(self): + """Write the result of server_info() to the JSON file info_file.""" + with io.open(self.info_file, 'w', encoding='utf-8') as f: + json.dump(self.server_info(), f) + + def remove_server_info_file(self): + """Remove the nbserver-.json file created for this server. + + Ignores the error raised when the file has already been removed. + """ + try: + os.unlink(self.info_file) + except OSError as e: + if e.errno != errno.ENOENT: + raise def start(self): """ Start the IPython Notebook server app, after initialization This method takes no arguments so all configuration and initialization must be done prior to calling this method.""" - ip = self.ip if self.ip else '[all ip addresses on your system]' - proto = 'https' if self.certfile else 'http' info = self.log.info - self._url = "%s://%s:%i%s" % (proto, ip, self.port, - self.base_project_url) for line in self.notebook_info().split("\n"): info(line) info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).") + self.write_server_info_file() + if self.open_browser or self.file_to_run: - ip = self.ip or localhost() try: browser = webbrowser.get(self.browser or None) except webbrowser.Error as e: self.log.warn('No web browser found: %s.' % e) browser = None - nbdir = os.path.abspath(self.notebook_manager.notebook_dir) f = self.file_to_run if f: + nbdir = os.path.abspath(self.notebook_manager.notebook_dir) if f.startswith(nbdir): f = f[len(nbdir):] else: @@ -732,8 +777,8 @@ class NotebookApp(BaseIPythonApplication): else: url = url_path_join('tree', f) if browser: - b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip, - self.port, self.base_project_url, url), new=2) + b = lambda : browser.open("%s%s" % (self.connection_url, url), + new=2) threading.Thread(target=b).start() try: ioloop.IOLoop.instance().start() @@ -741,7 +786,21 @@ class NotebookApp(BaseIPythonApplication): info("Interrupted...") finally: self.cleanup_kernels() + self.remove_server_info_file() + + +def discover_running_servers(profile='default'): + """Iterate over the server info files of running notebook servers. + Given a profile name, find nbserver-* files in the security directory of + that profile, and yield dicts of their information, each one pertaining to + a currently running notebook server instance. + """ + pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile) + for file in os.listdir(pd.security_dir): + if file.startswith('nbserver-'): + with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f: + yield json.load(f) #----------------------------------------------------------------------------- # Main entry point From 3fc26c1886d60898b192d5590ecf48004c351833 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 8 Jan 2014 13:02:50 -0800 Subject: [PATCH 2/5] Test for writing and removing server info files --- IPython/html/tests/test_notebookapp.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/IPython/html/tests/test_notebookapp.py b/IPython/html/tests/test_notebookapp.py index 56509cdc9..c5f8c07dc 100644 --- a/IPython/html/tests/test_notebookapp.py +++ b/IPython/html/tests/test_notebookapp.py @@ -14,6 +14,7 @@ import nose.tools as nt import IPython.testing.tools as tt +from IPython.html import notebookapp #----------------------------------------------------------------------------- # Test functions @@ -23,3 +24,18 @@ def test_help_output(): """ipython notebook --help-all works""" tt.help_all_output_test('notebook') +def test_server_info_file(): + nbapp = notebookapp.NotebookApp(profile='nbserver_file_test') + def get_servers(): + return list(notebookapp.discover_running_servers(profile='nbserver_file_test')) + nbapp.initialize(argv=[]) + nbapp.write_server_info_file() + servers = get_servers() + nt.assert_equal(len(servers), 1) + nt.assert_equal(servers[0]['port'], nbapp.port) + nt.assert_equal(servers[0]['url'], nbapp.connection_url) + nbapp.remove_server_info_file() + nt.assert_equal(get_servers(), []) + + # The ENOENT error should be silenced. + nbapp.remove_server_info_file() \ No newline at end of file From 15f94953bb706c4f3f617637c594239dd6bbabee Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Wed, 8 Jan 2014 17:56:12 -0800 Subject: [PATCH 3/5] Fix writing server info files on Python 2 --- IPython/html/notebookapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 4a59892a5..d3dae642a 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -727,8 +727,8 @@ class NotebookApp(BaseIPythonApplication): def write_server_info_file(self): """Write the result of server_info() to the JSON file info_file.""" - with io.open(self.info_file, 'w', encoding='utf-8') as f: - json.dump(self.server_info(), f) + with open(self.info_file, 'w') as f: + json.dump(self.server_info(), f, indent=2) def remove_server_info_file(self): """Remove the nbserver-.json file created for this server. From e932429762761655a9f2033373ee7e5b06b0b46b Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 13 Jan 2014 16:22:43 -0800 Subject: [PATCH 4/5] Command line entry point to list running notebook servers --- IPython/html/notebookapp.py | 30 +++++++++++++++++++++++++- IPython/html/tests/test_notebookapp.py | 2 +- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index d3dae642a..2d4c99ba2 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -214,6 +214,27 @@ class NotebookWebApplication(web.Application): return new_handlers +class NbserverListApp(BaseIPythonApplication): + + description="List currently running notebook servers in this profile." + + flags = dict( + json=({'NbserverListApp': {'json': True}}, + "Produce machine-readable JSON output."), + ) + + json = Bool(False, config=True, + help="If True, each line of output will be a JSON object with the " + "details from the server info file.") + + def start(self): + if not self.json: + print("Currently running servers:") + for serverinfo in discover_running_servers(self.profile): + if self.json: + print(json.dumps(serverinfo)) + else: + print(serverinfo['url'], "::", serverinfo['notebookdir']) #----------------------------------------------------------------------------- # Aliases and Flags @@ -286,6 +307,10 @@ class NotebookApp(BaseIPythonApplication): FileNotebookManager] flags = Dict(flags) aliases = Dict(aliases) + + subcommands = dict( + list=(NbserverListApp, NbserverListApp.description.splitlines()[0]), + ) kernel_argv = List(Unicode) @@ -746,6 +771,9 @@ class NotebookApp(BaseIPythonApplication): This method takes no arguments so all configuration and initialization must be done prior to calling this method.""" + if self.subapp is not None: + return self.subapp.start() + info = self.log.info for line in self.notebook_info().split("\n"): info(line) @@ -789,7 +817,7 @@ class NotebookApp(BaseIPythonApplication): self.remove_server_info_file() -def discover_running_servers(profile='default'): +def list_running_servers(profile='default'): """Iterate over the server info files of running notebook servers. Given a profile name, find nbserver-* files in the security directory of diff --git a/IPython/html/tests/test_notebookapp.py b/IPython/html/tests/test_notebookapp.py index c5f8c07dc..4422da91a 100644 --- a/IPython/html/tests/test_notebookapp.py +++ b/IPython/html/tests/test_notebookapp.py @@ -27,7 +27,7 @@ def test_help_output(): def test_server_info_file(): nbapp = notebookapp.NotebookApp(profile='nbserver_file_test') def get_servers(): - return list(notebookapp.discover_running_servers(profile='nbserver_file_test')) + return list(notebookapp.list_running_servers(profile='nbserver_file_test')) nbapp.initialize(argv=[]) nbapp.write_server_info_file() servers = get_servers() From fcb21fed176692ef2de2cee580f84655deb4a46e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver Date: Mon, 13 Jan 2014 16:25:39 -0800 Subject: [PATCH 5/5] Make names in JSON more consistent as per @ellisonbg's suggestion. --- IPython/html/notebookapp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index 2d4c99ba2..e43b43063 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -230,11 +230,11 @@ class NbserverListApp(BaseIPythonApplication): def start(self): if not self.json: print("Currently running servers:") - for serverinfo in discover_running_servers(self.profile): + for serverinfo in list_running_servers(self.profile): if self.json: print(json.dumps(serverinfo)) else: - print(serverinfo['url'], "::", serverinfo['notebookdir']) + print(serverinfo['url'], "::", serverinfo['notebook_dir']) #----------------------------------------------------------------------------- # Aliases and Flags @@ -746,8 +746,8 @@ class NotebookApp(BaseIPythonApplication): 'hostname': self.ip if self.ip else 'localhost', 'port': self.port, 'secure': bool(self.certfile), - 'baseurlpath': self.base_project_url, - 'notebookdir': os.path.abspath(self.notebook_manager.notebook_dir), + 'base_project_url': self.base_project_url, + 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir), } def write_server_info_file(self):