##// END OF EJS Templates
Merge pull request #6099 from takluyver/check-nbservers-pid...
Min RK -
r17279:d01354e9 merge
parent child Browse files
Show More
@@ -1,924 +1,935 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server."""
2 """A tornado based IPython notebook server."""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import errno
10 import errno
11 import io
11 import io
12 import json
12 import json
13 import logging
13 import logging
14 import os
14 import os
15 import random
15 import random
16 import re
16 import re
17 import select
17 import select
18 import signal
18 import signal
19 import socket
19 import socket
20 import sys
20 import sys
21 import threading
21 import threading
22 import time
22 import time
23 import webbrowser
23 import webbrowser
24
24
25
25
26 # check for pyzmq 2.1.11
26 # check for pyzmq 2.1.11
27 from IPython.utils.zmqrelated import check_for_zmq
27 from IPython.utils.zmqrelated import check_for_zmq
28 check_for_zmq('2.1.11', 'IPython.html')
28 check_for_zmq('2.1.11', 'IPython.html')
29
29
30 from jinja2 import Environment, FileSystemLoader
30 from jinja2 import Environment, FileSystemLoader
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 ioloop.install()
35 ioloop.install()
36
36
37 # check for tornado 3.1.0
37 # check for tornado 3.1.0
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 try:
39 try:
40 import tornado
40 import tornado
41 except ImportError:
41 except ImportError:
42 raise ImportError(msg)
42 raise ImportError(msg)
43 try:
43 try:
44 version_info = tornado.version_info
44 version_info = tornado.version_info
45 except AttributeError:
45 except AttributeError:
46 raise ImportError(msg + ", but you have < 1.1.0")
46 raise ImportError(msg + ", but you have < 1.1.0")
47 if version_info < (3,1,0):
47 if version_info < (3,1,0):
48 raise ImportError(msg + ", but you have %s" % tornado.version)
48 raise ImportError(msg + ", but you have %s" % tornado.version)
49
49
50 from tornado import httpserver
50 from tornado import httpserver
51 from tornado import web
51 from tornado import web
52 from tornado.log import LogFormatter
52 from tornado.log import LogFormatter
53
53
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 from .base.handlers import Template404
55 from .base.handlers import Template404
56 from .log import log_request
56 from .log import log_request
57 from .services.kernels.kernelmanager import MappingKernelManager
57 from .services.kernels.kernelmanager import MappingKernelManager
58 from .services.notebooks.nbmanager import NotebookManager
58 from .services.notebooks.nbmanager import NotebookManager
59 from .services.notebooks.filenbmanager import FileNotebookManager
59 from .services.notebooks.filenbmanager import FileNotebookManager
60 from .services.clusters.clustermanager import ClusterManager
60 from .services.clusters.clustermanager import ClusterManager
61 from .services.sessions.sessionmanager import SessionManager
61 from .services.sessions.sessionmanager import SessionManager
62
62
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64
64
65 from IPython.config import Config
65 from IPython.config import Config
66 from IPython.config.application import catch_config_error, boolean_flag
66 from IPython.config.application import catch_config_error, boolean_flag
67 from IPython.core.application import (
67 from IPython.core.application import (
68 BaseIPythonApplication, base_flags, base_aliases,
68 BaseIPythonApplication, base_flags, base_aliases,
69 )
69 )
70 from IPython.core.profiledir import ProfileDir
70 from IPython.core.profiledir import ProfileDir
71 from IPython.kernel import KernelManager
71 from IPython.kernel import KernelManager
72 from IPython.kernel.kernelspec import KernelSpecManager
72 from IPython.kernel.kernelspec import KernelSpecManager
73 from IPython.kernel.zmq.session import default_secure, Session
73 from IPython.kernel.zmq.session import default_secure, Session
74 from IPython.nbformat.sign import NotebookNotary
74 from IPython.nbformat.sign import NotebookNotary
75 from IPython.utils.importstring import import_item
75 from IPython.utils.importstring import import_item
76 from IPython.utils import submodule
76 from IPython.utils import submodule
77 from IPython.utils.process import check_pid
77 from IPython.utils.traitlets import (
78 from IPython.utils.traitlets import (
78 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
79 DottedObjectName, TraitError,
80 DottedObjectName, TraitError,
80 )
81 )
81 from IPython.utils import py3compat
82 from IPython.utils import py3compat
82 from IPython.utils.path import filefind, get_ipython_dir
83 from IPython.utils.path import filefind, get_ipython_dir
83
84
84 from .utils import url_path_join
85 from .utils import url_path_join
85
86
86 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
87 # Module globals
88 # Module globals
88 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
89
90
90 _examples = """
91 _examples = """
91 ipython notebook # start the notebook
92 ipython notebook # start the notebook
92 ipython notebook --profile=sympy # use the sympy profile
93 ipython notebook --profile=sympy # use the sympy profile
93 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
94 """
95 """
95
96
96 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
97 # Helper functions
98 # Helper functions
98 #-----------------------------------------------------------------------------
99 #-----------------------------------------------------------------------------
99
100
100 def random_ports(port, n):
101 def random_ports(port, n):
101 """Generate a list of n random ports near the given port.
102 """Generate a list of n random ports near the given port.
102
103
103 The first 5 ports will be sequential, and the remaining n-5 will be
104 The first 5 ports will be sequential, and the remaining n-5 will be
104 randomly selected in the range [port-2*n, port+2*n].
105 randomly selected in the range [port-2*n, port+2*n].
105 """
106 """
106 for i in range(min(5, n)):
107 for i in range(min(5, n)):
107 yield port + i
108 yield port + i
108 for i in range(n-5):
109 for i in range(n-5):
109 yield max(1, port + random.randint(-2*n, 2*n))
110 yield max(1, port + random.randint(-2*n, 2*n))
110
111
111 def load_handlers(name):
112 def load_handlers(name):
112 """Load the (URL pattern, handler) tuples for each component."""
113 """Load the (URL pattern, handler) tuples for each component."""
113 name = 'IPython.html.' + name
114 name = 'IPython.html.' + name
114 mod = __import__(name, fromlist=['default_handlers'])
115 mod = __import__(name, fromlist=['default_handlers'])
115 return mod.default_handlers
116 return mod.default_handlers
116
117
117 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
118 # The Tornado web application
119 # The Tornado web application
119 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
120
121
121 class NotebookWebApplication(web.Application):
122 class NotebookWebApplication(web.Application):
122
123
123 def __init__(self, ipython_app, kernel_manager, notebook_manager,
124 def __init__(self, ipython_app, kernel_manager, notebook_manager,
124 cluster_manager, session_manager, kernel_spec_manager, log,
125 cluster_manager, session_manager, kernel_spec_manager, log,
125 base_url, settings_overrides, jinja_env_options):
126 base_url, settings_overrides, jinja_env_options):
126
127
127 settings = self.init_settings(
128 settings = self.init_settings(
128 ipython_app, kernel_manager, notebook_manager, cluster_manager,
129 ipython_app, kernel_manager, notebook_manager, cluster_manager,
129 session_manager, kernel_spec_manager, log, base_url,
130 session_manager, kernel_spec_manager, log, base_url,
130 settings_overrides, jinja_env_options)
131 settings_overrides, jinja_env_options)
131 handlers = self.init_handlers(settings)
132 handlers = self.init_handlers(settings)
132
133
133 super(NotebookWebApplication, self).__init__(handlers, **settings)
134 super(NotebookWebApplication, self).__init__(handlers, **settings)
134
135
135 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
136 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
136 cluster_manager, session_manager, kernel_spec_manager,
137 cluster_manager, session_manager, kernel_spec_manager,
137 log, base_url, settings_overrides,
138 log, base_url, settings_overrides,
138 jinja_env_options=None):
139 jinja_env_options=None):
139 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
140 # base_url will always be unicode, which will in turn
141 # base_url will always be unicode, which will in turn
141 # make the patterns unicode, and ultimately result in unicode
142 # make the patterns unicode, and ultimately result in unicode
142 # keys in kwargs to handler._execute(**kwargs) in tornado.
143 # keys in kwargs to handler._execute(**kwargs) in tornado.
143 # This enforces that base_url be ascii in that situation.
144 # This enforces that base_url be ascii in that situation.
144 #
145 #
145 # Note that the URLs these patterns check against are escaped,
146 # Note that the URLs these patterns check against are escaped,
146 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
147 base_url = py3compat.unicode_to_str(base_url, 'ascii')
148 base_url = py3compat.unicode_to_str(base_url, 'ascii')
148 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
149 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
149 jenv_opt = jinja_env_options if jinja_env_options else {}
150 jenv_opt = jinja_env_options if jinja_env_options else {}
150 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
151 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
151 settings = dict(
152 settings = dict(
152 # basics
153 # basics
153 log_function=log_request,
154 log_function=log_request,
154 base_url=base_url,
155 base_url=base_url,
155 template_path=template_path,
156 template_path=template_path,
156 static_path=ipython_app.static_file_path,
157 static_path=ipython_app.static_file_path,
157 static_handler_class = FileFindHandler,
158 static_handler_class = FileFindHandler,
158 static_url_prefix = url_path_join(base_url,'/static/'),
159 static_url_prefix = url_path_join(base_url,'/static/'),
159
160
160 # authentication
161 # authentication
161 cookie_secret=ipython_app.cookie_secret,
162 cookie_secret=ipython_app.cookie_secret,
162 login_url=url_path_join(base_url,'/login'),
163 login_url=url_path_join(base_url,'/login'),
163 password=ipython_app.password,
164 password=ipython_app.password,
164
165
165 # managers
166 # managers
166 kernel_manager=kernel_manager,
167 kernel_manager=kernel_manager,
167 notebook_manager=notebook_manager,
168 notebook_manager=notebook_manager,
168 cluster_manager=cluster_manager,
169 cluster_manager=cluster_manager,
169 session_manager=session_manager,
170 session_manager=session_manager,
170 kernel_spec_manager=kernel_spec_manager,
171 kernel_spec_manager=kernel_spec_manager,
171
172
172 # IPython stuff
173 # IPython stuff
173 nbextensions_path = ipython_app.nbextensions_path,
174 nbextensions_path = ipython_app.nbextensions_path,
174 mathjax_url=ipython_app.mathjax_url,
175 mathjax_url=ipython_app.mathjax_url,
175 config=ipython_app.config,
176 config=ipython_app.config,
176 jinja2_env=env,
177 jinja2_env=env,
177 )
178 )
178
179
179 # allow custom overrides for the tornado web app.
180 # allow custom overrides for the tornado web app.
180 settings.update(settings_overrides)
181 settings.update(settings_overrides)
181 return settings
182 return settings
182
183
183 def init_handlers(self, settings):
184 def init_handlers(self, settings):
184 # Load the (URL pattern, handler) tuples for each component.
185 # Load the (URL pattern, handler) tuples for each component.
185 handlers = []
186 handlers = []
186 handlers.extend(load_handlers('base.handlers'))
187 handlers.extend(load_handlers('base.handlers'))
187 handlers.extend(load_handlers('tree.handlers'))
188 handlers.extend(load_handlers('tree.handlers'))
188 handlers.extend(load_handlers('auth.login'))
189 handlers.extend(load_handlers('auth.login'))
189 handlers.extend(load_handlers('auth.logout'))
190 handlers.extend(load_handlers('auth.logout'))
190 handlers.extend(load_handlers('notebook.handlers'))
191 handlers.extend(load_handlers('notebook.handlers'))
191 handlers.extend(load_handlers('nbconvert.handlers'))
192 handlers.extend(load_handlers('nbconvert.handlers'))
192 handlers.extend(load_handlers('kernelspecs.handlers'))
193 handlers.extend(load_handlers('kernelspecs.handlers'))
193 handlers.extend(load_handlers('services.kernels.handlers'))
194 handlers.extend(load_handlers('services.kernels.handlers'))
194 handlers.extend(load_handlers('services.notebooks.handlers'))
195 handlers.extend(load_handlers('services.notebooks.handlers'))
195 handlers.extend(load_handlers('services.clusters.handlers'))
196 handlers.extend(load_handlers('services.clusters.handlers'))
196 handlers.extend(load_handlers('services.sessions.handlers'))
197 handlers.extend(load_handlers('services.sessions.handlers'))
197 handlers.extend(load_handlers('services.nbconvert.handlers'))
198 handlers.extend(load_handlers('services.nbconvert.handlers'))
198 handlers.extend(load_handlers('services.kernelspecs.handlers'))
199 handlers.extend(load_handlers('services.kernelspecs.handlers'))
199 # FIXME: /files/ should be handled by the Contents service when it exists
200 # FIXME: /files/ should be handled by the Contents service when it exists
200 nbm = settings['notebook_manager']
201 nbm = settings['notebook_manager']
201 if hasattr(nbm, 'notebook_dir'):
202 if hasattr(nbm, 'notebook_dir'):
202 handlers.extend([
203 handlers.extend([
203 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
204 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
204 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
205 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
205 ])
206 ])
206 # prepend base_url onto the patterns that we match
207 # prepend base_url onto the patterns that we match
207 new_handlers = []
208 new_handlers = []
208 for handler in handlers:
209 for handler in handlers:
209 pattern = url_path_join(settings['base_url'], handler[0])
210 pattern = url_path_join(settings['base_url'], handler[0])
210 new_handler = tuple([pattern] + list(handler[1:]))
211 new_handler = tuple([pattern] + list(handler[1:]))
211 new_handlers.append(new_handler)
212 new_handlers.append(new_handler)
212 # add 404 on the end, which will catch everything that falls through
213 # add 404 on the end, which will catch everything that falls through
213 new_handlers.append((r'(.*)', Template404))
214 new_handlers.append((r'(.*)', Template404))
214 return new_handlers
215 return new_handlers
215
216
216
217
217 class NbserverListApp(BaseIPythonApplication):
218 class NbserverListApp(BaseIPythonApplication):
218
219
219 description="List currently running notebook servers in this profile."
220 description="List currently running notebook servers in this profile."
220
221
221 flags = dict(
222 flags = dict(
222 json=({'NbserverListApp': {'json': True}},
223 json=({'NbserverListApp': {'json': True}},
223 "Produce machine-readable JSON output."),
224 "Produce machine-readable JSON output."),
224 )
225 )
225
226
226 json = Bool(False, config=True,
227 json = Bool(False, config=True,
227 help="If True, each line of output will be a JSON object with the "
228 help="If True, each line of output will be a JSON object with the "
228 "details from the server info file.")
229 "details from the server info file.")
229
230
230 def start(self):
231 def start(self):
231 if not self.json:
232 if not self.json:
232 print("Currently running servers:")
233 print("Currently running servers:")
233 for serverinfo in list_running_servers(self.profile):
234 for serverinfo in list_running_servers(self.profile):
234 if self.json:
235 if self.json:
235 print(json.dumps(serverinfo))
236 print(json.dumps(serverinfo))
236 else:
237 else:
237 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
238 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
238
239
239 #-----------------------------------------------------------------------------
240 #-----------------------------------------------------------------------------
240 # Aliases and Flags
241 # Aliases and Flags
241 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
242
243
243 flags = dict(base_flags)
244 flags = dict(base_flags)
244 flags['no-browser']=(
245 flags['no-browser']=(
245 {'NotebookApp' : {'open_browser' : False}},
246 {'NotebookApp' : {'open_browser' : False}},
246 "Don't open the notebook in a browser after startup."
247 "Don't open the notebook in a browser after startup."
247 )
248 )
248 flags['pylab']=(
249 flags['pylab']=(
249 {'NotebookApp' : {'pylab' : 'warn'}},
250 {'NotebookApp' : {'pylab' : 'warn'}},
250 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
251 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
251 )
252 )
252 flags['no-mathjax']=(
253 flags['no-mathjax']=(
253 {'NotebookApp' : {'enable_mathjax' : False}},
254 {'NotebookApp' : {'enable_mathjax' : False}},
254 """Disable MathJax
255 """Disable MathJax
255
256
256 MathJax is the javascript library IPython uses to render math/LaTeX. It is
257 MathJax is the javascript library IPython uses to render math/LaTeX. It is
257 very large, so you may want to disable it if you have a slow internet
258 very large, so you may want to disable it if you have a slow internet
258 connection, or for offline use of the notebook.
259 connection, or for offline use of the notebook.
259
260
260 When disabled, equations etc. will appear as their untransformed TeX source.
261 When disabled, equations etc. will appear as their untransformed TeX source.
261 """
262 """
262 )
263 )
263
264
264 # Add notebook manager flags
265 # Add notebook manager flags
265 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
266 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
266 'Auto-save a .py script everytime the .ipynb notebook is saved',
267 'Auto-save a .py script everytime the .ipynb notebook is saved',
267 'Do not auto-save .py scripts for every notebook'))
268 'Do not auto-save .py scripts for every notebook'))
268
269
269 aliases = dict(base_aliases)
270 aliases = dict(base_aliases)
270
271
271 aliases.update({
272 aliases.update({
272 'ip': 'NotebookApp.ip',
273 'ip': 'NotebookApp.ip',
273 'port': 'NotebookApp.port',
274 'port': 'NotebookApp.port',
274 'port-retries': 'NotebookApp.port_retries',
275 'port-retries': 'NotebookApp.port_retries',
275 'transport': 'KernelManager.transport',
276 'transport': 'KernelManager.transport',
276 'keyfile': 'NotebookApp.keyfile',
277 'keyfile': 'NotebookApp.keyfile',
277 'certfile': 'NotebookApp.certfile',
278 'certfile': 'NotebookApp.certfile',
278 'notebook-dir': 'NotebookApp.notebook_dir',
279 'notebook-dir': 'NotebookApp.notebook_dir',
279 'browser': 'NotebookApp.browser',
280 'browser': 'NotebookApp.browser',
280 'pylab': 'NotebookApp.pylab',
281 'pylab': 'NotebookApp.pylab',
281 })
282 })
282
283
283 #-----------------------------------------------------------------------------
284 #-----------------------------------------------------------------------------
284 # NotebookApp
285 # NotebookApp
285 #-----------------------------------------------------------------------------
286 #-----------------------------------------------------------------------------
286
287
287 class NotebookApp(BaseIPythonApplication):
288 class NotebookApp(BaseIPythonApplication):
288
289
289 name = 'ipython-notebook'
290 name = 'ipython-notebook'
290
291
291 description = """
292 description = """
292 The IPython HTML Notebook.
293 The IPython HTML Notebook.
293
294
294 This launches a Tornado based HTML Notebook Server that serves up an
295 This launches a Tornado based HTML Notebook Server that serves up an
295 HTML5/Javascript Notebook client.
296 HTML5/Javascript Notebook client.
296 """
297 """
297 examples = _examples
298 examples = _examples
298 aliases = aliases
299 aliases = aliases
299 flags = flags
300 flags = flags
300
301
301 classes = [
302 classes = [
302 KernelManager, ProfileDir, Session, MappingKernelManager,
303 KernelManager, ProfileDir, Session, MappingKernelManager,
303 NotebookManager, FileNotebookManager, NotebookNotary,
304 NotebookManager, FileNotebookManager, NotebookNotary,
304 ]
305 ]
305 flags = Dict(flags)
306 flags = Dict(flags)
306 aliases = Dict(aliases)
307 aliases = Dict(aliases)
307
308
308 subcommands = dict(
309 subcommands = dict(
309 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
310 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
310 )
311 )
311
312
312 kernel_argv = List(Unicode)
313 kernel_argv = List(Unicode)
313
314
314 _log_formatter_cls = LogFormatter
315 _log_formatter_cls = LogFormatter
315
316
316 def _log_level_default(self):
317 def _log_level_default(self):
317 return logging.INFO
318 return logging.INFO
318
319
319 def _log_datefmt_default(self):
320 def _log_datefmt_default(self):
320 """Exclude date from default date format"""
321 """Exclude date from default date format"""
321 return "%H:%M:%S"
322 return "%H:%M:%S"
322
323
323 def _log_format_default(self):
324 def _log_format_default(self):
324 """override default log format to include time"""
325 """override default log format to include time"""
325 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
326 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
326
327
327 # create requested profiles by default, if they don't exist:
328 # create requested profiles by default, if they don't exist:
328 auto_create = Bool(True)
329 auto_create = Bool(True)
329
330
330 # file to be opened in the notebook server
331 # file to be opened in the notebook server
331 file_to_run = Unicode('', config=True)
332 file_to_run = Unicode('', config=True)
332 def _file_to_run_changed(self, name, old, new):
333 def _file_to_run_changed(self, name, old, new):
333 path, base = os.path.split(new)
334 path, base = os.path.split(new)
334 if path:
335 if path:
335 self.file_to_run = base
336 self.file_to_run = base
336 self.notebook_dir = path
337 self.notebook_dir = path
337
338
338 # Network related information
339 # Network related information
339
340
340 allow_origin = Unicode('', config=True,
341 allow_origin = Unicode('', config=True,
341 help="""Set the Access-Control-Allow-Origin header
342 help="""Set the Access-Control-Allow-Origin header
342
343
343 Use '*' to allow any origin to access your server.
344 Use '*' to allow any origin to access your server.
344
345
345 Takes precedence over allow_origin_pat.
346 Takes precedence over allow_origin_pat.
346 """
347 """
347 )
348 )
348
349
349 allow_origin_pat = Unicode('', config=True,
350 allow_origin_pat = Unicode('', config=True,
350 help="""Use a regular expression for the Access-Control-Allow-Origin header
351 help="""Use a regular expression for the Access-Control-Allow-Origin header
351
352
352 Requests from an origin matching the expression will get replies with:
353 Requests from an origin matching the expression will get replies with:
353
354
354 Access-Control-Allow-Origin: origin
355 Access-Control-Allow-Origin: origin
355
356
356 where `origin` is the origin of the request.
357 where `origin` is the origin of the request.
357
358
358 Ignored if allow_origin is set.
359 Ignored if allow_origin is set.
359 """
360 """
360 )
361 )
361
362
362 allow_credentials = Bool(False, config=True,
363 allow_credentials = Bool(False, config=True,
363 help="Set the Access-Control-Allow-Credentials: true header"
364 help="Set the Access-Control-Allow-Credentials: true header"
364 )
365 )
365
366
366 ip = Unicode('localhost', config=True,
367 ip = Unicode('localhost', config=True,
367 help="The IP address the notebook server will listen on."
368 help="The IP address the notebook server will listen on."
368 )
369 )
369
370
370 def _ip_changed(self, name, old, new):
371 def _ip_changed(self, name, old, new):
371 if new == u'*': self.ip = u''
372 if new == u'*': self.ip = u''
372
373
373 port = Integer(8888, config=True,
374 port = Integer(8888, config=True,
374 help="The port the notebook server will listen on."
375 help="The port the notebook server will listen on."
375 )
376 )
376 port_retries = Integer(50, config=True,
377 port_retries = Integer(50, config=True,
377 help="The number of additional ports to try if the specified port is not available."
378 help="The number of additional ports to try if the specified port is not available."
378 )
379 )
379
380
380 certfile = Unicode(u'', config=True,
381 certfile = Unicode(u'', config=True,
381 help="""The full path to an SSL/TLS certificate file."""
382 help="""The full path to an SSL/TLS certificate file."""
382 )
383 )
383
384
384 keyfile = Unicode(u'', config=True,
385 keyfile = Unicode(u'', config=True,
385 help="""The full path to a private key file for usage with SSL/TLS."""
386 help="""The full path to a private key file for usage with SSL/TLS."""
386 )
387 )
387
388
388 cookie_secret_file = Unicode(config=True,
389 cookie_secret_file = Unicode(config=True,
389 help="""The file where the cookie secret is stored."""
390 help="""The file where the cookie secret is stored."""
390 )
391 )
391 def _cookie_secret_file_default(self):
392 def _cookie_secret_file_default(self):
392 if self.profile_dir is None:
393 if self.profile_dir is None:
393 return ''
394 return ''
394 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
395 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
395
396
396 cookie_secret = Bytes(b'', config=True,
397 cookie_secret = Bytes(b'', config=True,
397 help="""The random bytes used to secure cookies.
398 help="""The random bytes used to secure cookies.
398 By default this is a new random number every time you start the Notebook.
399 By default this is a new random number every time you start the Notebook.
399 Set it to a value in a config file to enable logins to persist across server sessions.
400 Set it to a value in a config file to enable logins to persist across server sessions.
400
401
401 Note: Cookie secrets should be kept private, do not share config files with
402 Note: Cookie secrets should be kept private, do not share config files with
402 cookie_secret stored in plaintext (you can read the value from a file).
403 cookie_secret stored in plaintext (you can read the value from a file).
403 """
404 """
404 )
405 )
405 def _cookie_secret_default(self):
406 def _cookie_secret_default(self):
406 if os.path.exists(self.cookie_secret_file):
407 if os.path.exists(self.cookie_secret_file):
407 with io.open(self.cookie_secret_file, 'rb') as f:
408 with io.open(self.cookie_secret_file, 'rb') as f:
408 return f.read()
409 return f.read()
409 else:
410 else:
410 secret = base64.encodestring(os.urandom(1024))
411 secret = base64.encodestring(os.urandom(1024))
411 self._write_cookie_secret_file(secret)
412 self._write_cookie_secret_file(secret)
412 return secret
413 return secret
413
414
414 def _write_cookie_secret_file(self, secret):
415 def _write_cookie_secret_file(self, secret):
415 """write my secret to my secret_file"""
416 """write my secret to my secret_file"""
416 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
417 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
417 with io.open(self.cookie_secret_file, 'wb') as f:
418 with io.open(self.cookie_secret_file, 'wb') as f:
418 f.write(secret)
419 f.write(secret)
419 try:
420 try:
420 os.chmod(self.cookie_secret_file, 0o600)
421 os.chmod(self.cookie_secret_file, 0o600)
421 except OSError:
422 except OSError:
422 self.log.warn(
423 self.log.warn(
423 "Could not set permissions on %s",
424 "Could not set permissions on %s",
424 self.cookie_secret_file
425 self.cookie_secret_file
425 )
426 )
426
427
427 password = Unicode(u'', config=True,
428 password = Unicode(u'', config=True,
428 help="""Hashed password to use for web authentication.
429 help="""Hashed password to use for web authentication.
429
430
430 To generate, type in a python/IPython shell:
431 To generate, type in a python/IPython shell:
431
432
432 from IPython.lib import passwd; passwd()
433 from IPython.lib import passwd; passwd()
433
434
434 The string should be of the form type:salt:hashed-password.
435 The string should be of the form type:salt:hashed-password.
435 """
436 """
436 )
437 )
437
438
438 open_browser = Bool(True, config=True,
439 open_browser = Bool(True, config=True,
439 help="""Whether to open in a browser after starting.
440 help="""Whether to open in a browser after starting.
440 The specific browser used is platform dependent and
441 The specific browser used is platform dependent and
441 determined by the python standard library `webbrowser`
442 determined by the python standard library `webbrowser`
442 module, unless it is overridden using the --browser
443 module, unless it is overridden using the --browser
443 (NotebookApp.browser) configuration option.
444 (NotebookApp.browser) configuration option.
444 """)
445 """)
445
446
446 browser = Unicode(u'', config=True,
447 browser = Unicode(u'', config=True,
447 help="""Specify what command to use to invoke a web
448 help="""Specify what command to use to invoke a web
448 browser when opening the notebook. If not specified, the
449 browser when opening the notebook. If not specified, the
449 default browser will be determined by the `webbrowser`
450 default browser will be determined by the `webbrowser`
450 standard library module, which allows setting of the
451 standard library module, which allows setting of the
451 BROWSER environment variable to override it.
452 BROWSER environment variable to override it.
452 """)
453 """)
453
454
454 webapp_settings = Dict(config=True,
455 webapp_settings = Dict(config=True,
455 help="Supply overrides for the tornado.web.Application that the "
456 help="Supply overrides for the tornado.web.Application that the "
456 "IPython notebook uses.")
457 "IPython notebook uses.")
457
458
458 jinja_environment_options = Dict(config=True,
459 jinja_environment_options = Dict(config=True,
459 help="Supply extra arguments that will be passed to Jinja environment.")
460 help="Supply extra arguments that will be passed to Jinja environment.")
460
461
461
462
462 enable_mathjax = Bool(True, config=True,
463 enable_mathjax = Bool(True, config=True,
463 help="""Whether to enable MathJax for typesetting math/TeX
464 help="""Whether to enable MathJax for typesetting math/TeX
464
465
465 MathJax is the javascript library IPython uses to render math/LaTeX. It is
466 MathJax is the javascript library IPython uses to render math/LaTeX. It is
466 very large, so you may want to disable it if you have a slow internet
467 very large, so you may want to disable it if you have a slow internet
467 connection, or for offline use of the notebook.
468 connection, or for offline use of the notebook.
468
469
469 When disabled, equations etc. will appear as their untransformed TeX source.
470 When disabled, equations etc. will appear as their untransformed TeX source.
470 """
471 """
471 )
472 )
472 def _enable_mathjax_changed(self, name, old, new):
473 def _enable_mathjax_changed(self, name, old, new):
473 """set mathjax url to empty if mathjax is disabled"""
474 """set mathjax url to empty if mathjax is disabled"""
474 if not new:
475 if not new:
475 self.mathjax_url = u''
476 self.mathjax_url = u''
476
477
477 base_url = Unicode('/', config=True,
478 base_url = Unicode('/', config=True,
478 help='''The base URL for the notebook server.
479 help='''The base URL for the notebook server.
479
480
480 Leading and trailing slashes can be omitted,
481 Leading and trailing slashes can be omitted,
481 and will automatically be added.
482 and will automatically be added.
482 ''')
483 ''')
483 def _base_url_changed(self, name, old, new):
484 def _base_url_changed(self, name, old, new):
484 if not new.startswith('/'):
485 if not new.startswith('/'):
485 self.base_url = '/'+new
486 self.base_url = '/'+new
486 elif not new.endswith('/'):
487 elif not new.endswith('/'):
487 self.base_url = new+'/'
488 self.base_url = new+'/'
488
489
489 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
490 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
490 def _base_project_url_changed(self, name, old, new):
491 def _base_project_url_changed(self, name, old, new):
491 self.log.warn("base_project_url is deprecated, use base_url")
492 self.log.warn("base_project_url is deprecated, use base_url")
492 self.base_url = new
493 self.base_url = new
493
494
494 extra_static_paths = List(Unicode, config=True,
495 extra_static_paths = List(Unicode, config=True,
495 help="""Extra paths to search for serving static files.
496 help="""Extra paths to search for serving static files.
496
497
497 This allows adding javascript/css to be available from the notebook server machine,
498 This allows adding javascript/css to be available from the notebook server machine,
498 or overriding individual files in the IPython"""
499 or overriding individual files in the IPython"""
499 )
500 )
500 def _extra_static_paths_default(self):
501 def _extra_static_paths_default(self):
501 return [os.path.join(self.profile_dir.location, 'static')]
502 return [os.path.join(self.profile_dir.location, 'static')]
502
503
503 @property
504 @property
504 def static_file_path(self):
505 def static_file_path(self):
505 """return extra paths + the default location"""
506 """return extra paths + the default location"""
506 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
507 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
507
508
508 nbextensions_path = List(Unicode, config=True,
509 nbextensions_path = List(Unicode, config=True,
509 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
510 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
510 )
511 )
511 def _nbextensions_path_default(self):
512 def _nbextensions_path_default(self):
512 return [os.path.join(get_ipython_dir(), 'nbextensions')]
513 return [os.path.join(get_ipython_dir(), 'nbextensions')]
513
514
514 mathjax_url = Unicode("", config=True,
515 mathjax_url = Unicode("", config=True,
515 help="""The url for MathJax.js."""
516 help="""The url for MathJax.js."""
516 )
517 )
517 def _mathjax_url_default(self):
518 def _mathjax_url_default(self):
518 if not self.enable_mathjax:
519 if not self.enable_mathjax:
519 return u''
520 return u''
520 static_url_prefix = self.webapp_settings.get("static_url_prefix",
521 static_url_prefix = self.webapp_settings.get("static_url_prefix",
521 url_path_join(self.base_url, "static")
522 url_path_join(self.base_url, "static")
522 )
523 )
523
524
524 # try local mathjax, either in nbextensions/mathjax or static/mathjax
525 # try local mathjax, either in nbextensions/mathjax or static/mathjax
525 for (url_prefix, search_path) in [
526 for (url_prefix, search_path) in [
526 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
527 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
527 (static_url_prefix, self.static_file_path),
528 (static_url_prefix, self.static_file_path),
528 ]:
529 ]:
529 self.log.debug("searching for local mathjax in %s", search_path)
530 self.log.debug("searching for local mathjax in %s", search_path)
530 try:
531 try:
531 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
532 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
532 except IOError:
533 except IOError:
533 continue
534 continue
534 else:
535 else:
535 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
536 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
536 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
537 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
537 return url
538 return url
538
539
539 # no local mathjax, serve from CDN
540 # no local mathjax, serve from CDN
540 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
541 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
541 self.log.info("Using MathJax from CDN: %s", url)
542 self.log.info("Using MathJax from CDN: %s", url)
542 return url
543 return url
543
544
544 def _mathjax_url_changed(self, name, old, new):
545 def _mathjax_url_changed(self, name, old, new):
545 if new and not self.enable_mathjax:
546 if new and not self.enable_mathjax:
546 # enable_mathjax=False overrides mathjax_url
547 # enable_mathjax=False overrides mathjax_url
547 self.mathjax_url = u''
548 self.mathjax_url = u''
548 else:
549 else:
549 self.log.info("Using MathJax: %s", new)
550 self.log.info("Using MathJax: %s", new)
550
551
551 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
552 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
552 config=True,
553 config=True,
553 help='The notebook manager class to use.'
554 help='The notebook manager class to use.'
554 )
555 )
555 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
556 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
556 config=True,
557 config=True,
557 help='The kernel manager class to use.'
558 help='The kernel manager class to use.'
558 )
559 )
559 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
560 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
560 config=True,
561 config=True,
561 help='The session manager class to use.'
562 help='The session manager class to use.'
562 )
563 )
563 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
564 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
564 config=True,
565 config=True,
565 help='The cluster manager class to use.'
566 help='The cluster manager class to use.'
566 )
567 )
567
568
568 kernel_spec_manager = Instance(KernelSpecManager)
569 kernel_spec_manager = Instance(KernelSpecManager)
569
570
570 def _kernel_spec_manager_default(self):
571 def _kernel_spec_manager_default(self):
571 return KernelSpecManager(ipython_dir=self.ipython_dir)
572 return KernelSpecManager(ipython_dir=self.ipython_dir)
572
573
573 trust_xheaders = Bool(False, config=True,
574 trust_xheaders = Bool(False, config=True,
574 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
575 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
575 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
576 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
576 )
577 )
577
578
578 info_file = Unicode()
579 info_file = Unicode()
579
580
580 def _info_file_default(self):
581 def _info_file_default(self):
581 info_file = "nbserver-%s.json"%os.getpid()
582 info_file = "nbserver-%s.json"%os.getpid()
582 return os.path.join(self.profile_dir.security_dir, info_file)
583 return os.path.join(self.profile_dir.security_dir, info_file)
583
584
584 notebook_dir = Unicode(py3compat.getcwd(), config=True,
585 notebook_dir = Unicode(py3compat.getcwd(), config=True,
585 help="The directory to use for notebooks and kernels."
586 help="The directory to use for notebooks and kernels."
586 )
587 )
587
588
588 pylab = Unicode('disabled', config=True,
589 pylab = Unicode('disabled', config=True,
589 help="""
590 help="""
590 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
591 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
591 """
592 """
592 )
593 )
593 def _pylab_changed(self, name, old, new):
594 def _pylab_changed(self, name, old, new):
594 """when --pylab is specified, display a warning and exit"""
595 """when --pylab is specified, display a warning and exit"""
595 if new != 'warn':
596 if new != 'warn':
596 backend = ' %s' % new
597 backend = ' %s' % new
597 else:
598 else:
598 backend = ''
599 backend = ''
599 self.log.error("Support for specifying --pylab on the command line has been removed.")
600 self.log.error("Support for specifying --pylab on the command line has been removed.")
600 self.log.error(
601 self.log.error(
601 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
602 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
602 )
603 )
603 self.exit(1)
604 self.exit(1)
604
605
605 def _notebook_dir_changed(self, name, old, new):
606 def _notebook_dir_changed(self, name, old, new):
606 """Do a bit of validation of the notebook dir."""
607 """Do a bit of validation of the notebook dir."""
607 if not os.path.isabs(new):
608 if not os.path.isabs(new):
608 # If we receive a non-absolute path, make it absolute.
609 # If we receive a non-absolute path, make it absolute.
609 self.notebook_dir = os.path.abspath(new)
610 self.notebook_dir = os.path.abspath(new)
610 return
611 return
611 if not os.path.isdir(new):
612 if not os.path.isdir(new):
612 raise TraitError("No such notebook dir: %r" % new)
613 raise TraitError("No such notebook dir: %r" % new)
613
614
614 # setting App.notebook_dir implies setting notebook and kernel dirs as well
615 # setting App.notebook_dir implies setting notebook and kernel dirs as well
615 self.config.FileNotebookManager.notebook_dir = new
616 self.config.FileNotebookManager.notebook_dir = new
616 self.config.MappingKernelManager.root_dir = new
617 self.config.MappingKernelManager.root_dir = new
617
618
618
619
619 def parse_command_line(self, argv=None):
620 def parse_command_line(self, argv=None):
620 super(NotebookApp, self).parse_command_line(argv)
621 super(NotebookApp, self).parse_command_line(argv)
621
622
622 if self.extra_args:
623 if self.extra_args:
623 arg0 = self.extra_args[0]
624 arg0 = self.extra_args[0]
624 f = os.path.abspath(arg0)
625 f = os.path.abspath(arg0)
625 self.argv.remove(arg0)
626 self.argv.remove(arg0)
626 if not os.path.exists(f):
627 if not os.path.exists(f):
627 self.log.critical("No such file or directory: %s", f)
628 self.log.critical("No such file or directory: %s", f)
628 self.exit(1)
629 self.exit(1)
629
630
630 # Use config here, to ensure that it takes higher priority than
631 # Use config here, to ensure that it takes higher priority than
631 # anything that comes from the profile.
632 # anything that comes from the profile.
632 c = Config()
633 c = Config()
633 if os.path.isdir(f):
634 if os.path.isdir(f):
634 c.NotebookApp.notebook_dir = f
635 c.NotebookApp.notebook_dir = f
635 elif os.path.isfile(f):
636 elif os.path.isfile(f):
636 c.NotebookApp.file_to_run = f
637 c.NotebookApp.file_to_run = f
637 self.update_config(c)
638 self.update_config(c)
638
639
639 def init_kernel_argv(self):
640 def init_kernel_argv(self):
640 """construct the kernel arguments"""
641 """construct the kernel arguments"""
641 # Kernel should get *absolute* path to profile directory
642 # Kernel should get *absolute* path to profile directory
642 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
643 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
643
644
644 def init_configurables(self):
645 def init_configurables(self):
645 # force Session default to be secure
646 # force Session default to be secure
646 default_secure(self.config)
647 default_secure(self.config)
647 kls = import_item(self.kernel_manager_class)
648 kls = import_item(self.kernel_manager_class)
648 self.kernel_manager = kls(
649 self.kernel_manager = kls(
649 parent=self, log=self.log, kernel_argv=self.kernel_argv,
650 parent=self, log=self.log, kernel_argv=self.kernel_argv,
650 connection_dir = self.profile_dir.security_dir,
651 connection_dir = self.profile_dir.security_dir,
651 )
652 )
652 kls = import_item(self.notebook_manager_class)
653 kls = import_item(self.notebook_manager_class)
653 self.notebook_manager = kls(parent=self, log=self.log)
654 self.notebook_manager = kls(parent=self, log=self.log)
654 kls = import_item(self.session_manager_class)
655 kls = import_item(self.session_manager_class)
655 self.session_manager = kls(parent=self, log=self.log,
656 self.session_manager = kls(parent=self, log=self.log,
656 kernel_manager=self.kernel_manager,
657 kernel_manager=self.kernel_manager,
657 notebook_manager=self.notebook_manager)
658 notebook_manager=self.notebook_manager)
658 kls = import_item(self.cluster_manager_class)
659 kls = import_item(self.cluster_manager_class)
659 self.cluster_manager = kls(parent=self, log=self.log)
660 self.cluster_manager = kls(parent=self, log=self.log)
660 self.cluster_manager.update_profiles()
661 self.cluster_manager.update_profiles()
661
662
662 def init_logging(self):
663 def init_logging(self):
663 # This prevents double log messages because tornado use a root logger that
664 # This prevents double log messages because tornado use a root logger that
664 # self.log is a child of. The logging module dipatches log messages to a log
665 # self.log is a child of. The logging module dipatches log messages to a log
665 # and all of its ancenstors until propagate is set to False.
666 # and all of its ancenstors until propagate is set to False.
666 self.log.propagate = False
667 self.log.propagate = False
667
668
668 # hook up tornado 3's loggers to our app handlers
669 # hook up tornado 3's loggers to our app handlers
669 logger = logging.getLogger('tornado')
670 logger = logging.getLogger('tornado')
670 logger.propagate = True
671 logger.propagate = True
671 logger.parent = self.log
672 logger.parent = self.log
672 logger.setLevel(self.log.level)
673 logger.setLevel(self.log.level)
673
674
674 def init_webapp(self):
675 def init_webapp(self):
675 """initialize tornado webapp and httpserver"""
676 """initialize tornado webapp and httpserver"""
676 self.webapp_settings['allow_origin'] = self.allow_origin
677 self.webapp_settings['allow_origin'] = self.allow_origin
677 if self.allow_origin_pat:
678 if self.allow_origin_pat:
678 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
679 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
679 self.webapp_settings['allow_credentials'] = self.allow_credentials
680 self.webapp_settings['allow_credentials'] = self.allow_credentials
680
681
681 self.web_app = NotebookWebApplication(
682 self.web_app = NotebookWebApplication(
682 self, self.kernel_manager, self.notebook_manager,
683 self, self.kernel_manager, self.notebook_manager,
683 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
684 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
684 self.log, self.base_url, self.webapp_settings,
685 self.log, self.base_url, self.webapp_settings,
685 self.jinja_environment_options
686 self.jinja_environment_options
686 )
687 )
687 if self.certfile:
688 if self.certfile:
688 ssl_options = dict(certfile=self.certfile)
689 ssl_options = dict(certfile=self.certfile)
689 if self.keyfile:
690 if self.keyfile:
690 ssl_options['keyfile'] = self.keyfile
691 ssl_options['keyfile'] = self.keyfile
691 else:
692 else:
692 ssl_options = None
693 ssl_options = None
693 self.web_app.password = self.password
694 self.web_app.password = self.password
694 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
695 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
695 xheaders=self.trust_xheaders)
696 xheaders=self.trust_xheaders)
696 if not self.ip:
697 if not self.ip:
697 warning = "WARNING: The notebook server is listening on all IP addresses"
698 warning = "WARNING: The notebook server is listening on all IP addresses"
698 if ssl_options is None:
699 if ssl_options is None:
699 self.log.critical(warning + " and not using encryption. This "
700 self.log.critical(warning + " and not using encryption. This "
700 "is not recommended.")
701 "is not recommended.")
701 if not self.password:
702 if not self.password:
702 self.log.critical(warning + " and not using authentication. "
703 self.log.critical(warning + " and not using authentication. "
703 "This is highly insecure and not recommended.")
704 "This is highly insecure and not recommended.")
704 success = None
705 success = None
705 for port in random_ports(self.port, self.port_retries+1):
706 for port in random_ports(self.port, self.port_retries+1):
706 try:
707 try:
707 self.http_server.listen(port, self.ip)
708 self.http_server.listen(port, self.ip)
708 except socket.error as e:
709 except socket.error as e:
709 if e.errno == errno.EADDRINUSE:
710 if e.errno == errno.EADDRINUSE:
710 self.log.info('The port %i is already in use, trying another random port.' % port)
711 self.log.info('The port %i is already in use, trying another random port.' % port)
711 continue
712 continue
712 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
713 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
713 self.log.warn("Permission to listen on port %i denied" % port)
714 self.log.warn("Permission to listen on port %i denied" % port)
714 continue
715 continue
715 else:
716 else:
716 raise
717 raise
717 else:
718 else:
718 self.port = port
719 self.port = port
719 success = True
720 success = True
720 break
721 break
721 if not success:
722 if not success:
722 self.log.critical('ERROR: the notebook server could not be started because '
723 self.log.critical('ERROR: the notebook server could not be started because '
723 'no available port could be found.')
724 'no available port could be found.')
724 self.exit(1)
725 self.exit(1)
725
726
726 @property
727 @property
727 def display_url(self):
728 def display_url(self):
728 ip = self.ip if self.ip else '[all ip addresses on your system]'
729 ip = self.ip if self.ip else '[all ip addresses on your system]'
729 return self._url(ip)
730 return self._url(ip)
730
731
731 @property
732 @property
732 def connection_url(self):
733 def connection_url(self):
733 ip = self.ip if self.ip else 'localhost'
734 ip = self.ip if self.ip else 'localhost'
734 return self._url(ip)
735 return self._url(ip)
735
736
736 def _url(self, ip):
737 def _url(self, ip):
737 proto = 'https' if self.certfile else 'http'
738 proto = 'https' if self.certfile else 'http'
738 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
739 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
739
740
740 def init_signal(self):
741 def init_signal(self):
741 if not sys.platform.startswith('win'):
742 if not sys.platform.startswith('win'):
742 signal.signal(signal.SIGINT, self._handle_sigint)
743 signal.signal(signal.SIGINT, self._handle_sigint)
743 signal.signal(signal.SIGTERM, self._signal_stop)
744 signal.signal(signal.SIGTERM, self._signal_stop)
744 if hasattr(signal, 'SIGUSR1'):
745 if hasattr(signal, 'SIGUSR1'):
745 # Windows doesn't support SIGUSR1
746 # Windows doesn't support SIGUSR1
746 signal.signal(signal.SIGUSR1, self._signal_info)
747 signal.signal(signal.SIGUSR1, self._signal_info)
747 if hasattr(signal, 'SIGINFO'):
748 if hasattr(signal, 'SIGINFO'):
748 # only on BSD-based systems
749 # only on BSD-based systems
749 signal.signal(signal.SIGINFO, self._signal_info)
750 signal.signal(signal.SIGINFO, self._signal_info)
750
751
751 def _handle_sigint(self, sig, frame):
752 def _handle_sigint(self, sig, frame):
752 """SIGINT handler spawns confirmation dialog"""
753 """SIGINT handler spawns confirmation dialog"""
753 # register more forceful signal handler for ^C^C case
754 # register more forceful signal handler for ^C^C case
754 signal.signal(signal.SIGINT, self._signal_stop)
755 signal.signal(signal.SIGINT, self._signal_stop)
755 # request confirmation dialog in bg thread, to avoid
756 # request confirmation dialog in bg thread, to avoid
756 # blocking the App
757 # blocking the App
757 thread = threading.Thread(target=self._confirm_exit)
758 thread = threading.Thread(target=self._confirm_exit)
758 thread.daemon = True
759 thread.daemon = True
759 thread.start()
760 thread.start()
760
761
761 def _restore_sigint_handler(self):
762 def _restore_sigint_handler(self):
762 """callback for restoring original SIGINT handler"""
763 """callback for restoring original SIGINT handler"""
763 signal.signal(signal.SIGINT, self._handle_sigint)
764 signal.signal(signal.SIGINT, self._handle_sigint)
764
765
765 def _confirm_exit(self):
766 def _confirm_exit(self):
766 """confirm shutdown on ^C
767 """confirm shutdown on ^C
767
768
768 A second ^C, or answering 'y' within 5s will cause shutdown,
769 A second ^C, or answering 'y' within 5s will cause shutdown,
769 otherwise original SIGINT handler will be restored.
770 otherwise original SIGINT handler will be restored.
770
771
771 This doesn't work on Windows.
772 This doesn't work on Windows.
772 """
773 """
773 info = self.log.info
774 info = self.log.info
774 info('interrupted')
775 info('interrupted')
775 print(self.notebook_info())
776 print(self.notebook_info())
776 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
777 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
777 sys.stdout.flush()
778 sys.stdout.flush()
778 r,w,x = select.select([sys.stdin], [], [], 5)
779 r,w,x = select.select([sys.stdin], [], [], 5)
779 if r:
780 if r:
780 line = sys.stdin.readline()
781 line = sys.stdin.readline()
781 if line.lower().startswith('y') and 'n' not in line.lower():
782 if line.lower().startswith('y') and 'n' not in line.lower():
782 self.log.critical("Shutdown confirmed")
783 self.log.critical("Shutdown confirmed")
783 ioloop.IOLoop.instance().stop()
784 ioloop.IOLoop.instance().stop()
784 return
785 return
785 else:
786 else:
786 print("No answer for 5s:", end=' ')
787 print("No answer for 5s:", end=' ')
787 print("resuming operation...")
788 print("resuming operation...")
788 # no answer, or answer is no:
789 # no answer, or answer is no:
789 # set it back to original SIGINT handler
790 # set it back to original SIGINT handler
790 # use IOLoop.add_callback because signal.signal must be called
791 # use IOLoop.add_callback because signal.signal must be called
791 # from main thread
792 # from main thread
792 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
793 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
793
794
794 def _signal_stop(self, sig, frame):
795 def _signal_stop(self, sig, frame):
795 self.log.critical("received signal %s, stopping", sig)
796 self.log.critical("received signal %s, stopping", sig)
796 ioloop.IOLoop.instance().stop()
797 ioloop.IOLoop.instance().stop()
797
798
798 def _signal_info(self, sig, frame):
799 def _signal_info(self, sig, frame):
799 print(self.notebook_info())
800 print(self.notebook_info())
800
801
801 def init_components(self):
802 def init_components(self):
802 """Check the components submodule, and warn if it's unclean"""
803 """Check the components submodule, and warn if it's unclean"""
803 status = submodule.check_submodule_status()
804 status = submodule.check_submodule_status()
804 if status == 'missing':
805 if status == 'missing':
805 self.log.warn("components submodule missing, running `git submodule update`")
806 self.log.warn("components submodule missing, running `git submodule update`")
806 submodule.update_submodules(submodule.ipython_parent())
807 submodule.update_submodules(submodule.ipython_parent())
807 elif status == 'unclean':
808 elif status == 'unclean':
808 self.log.warn("components submodule unclean, you may see 404s on static/components")
809 self.log.warn("components submodule unclean, you may see 404s on static/components")
809 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
810 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
810
811
811 @catch_config_error
812 @catch_config_error
812 def initialize(self, argv=None):
813 def initialize(self, argv=None):
813 super(NotebookApp, self).initialize(argv)
814 super(NotebookApp, self).initialize(argv)
814 self.init_logging()
815 self.init_logging()
815 self.init_kernel_argv()
816 self.init_kernel_argv()
816 self.init_configurables()
817 self.init_configurables()
817 self.init_components()
818 self.init_components()
818 self.init_webapp()
819 self.init_webapp()
819 self.init_signal()
820 self.init_signal()
820
821
821 def cleanup_kernels(self):
822 def cleanup_kernels(self):
822 """Shutdown all kernels.
823 """Shutdown all kernels.
823
824
824 The kernels will shutdown themselves when this process no longer exists,
825 The kernels will shutdown themselves when this process no longer exists,
825 but explicit shutdown allows the KernelManagers to cleanup the connection files.
826 but explicit shutdown allows the KernelManagers to cleanup the connection files.
826 """
827 """
827 self.log.info('Shutting down kernels')
828 self.log.info('Shutting down kernels')
828 self.kernel_manager.shutdown_all()
829 self.kernel_manager.shutdown_all()
829
830
830 def notebook_info(self):
831 def notebook_info(self):
831 "Return the current working directory and the server url information"
832 "Return the current working directory and the server url information"
832 info = self.notebook_manager.info_string() + "\n"
833 info = self.notebook_manager.info_string() + "\n"
833 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
834 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
834 return info + "The IPython Notebook is running at: %s" % self.display_url
835 return info + "The IPython Notebook is running at: %s" % self.display_url
835
836
836 def server_info(self):
837 def server_info(self):
837 """Return a JSONable dict of information about this server."""
838 """Return a JSONable dict of information about this server."""
838 return {'url': self.connection_url,
839 return {'url': self.connection_url,
839 'hostname': self.ip if self.ip else 'localhost',
840 'hostname': self.ip if self.ip else 'localhost',
840 'port': self.port,
841 'port': self.port,
841 'secure': bool(self.certfile),
842 'secure': bool(self.certfile),
842 'base_url': self.base_url,
843 'base_url': self.base_url,
843 'notebook_dir': os.path.abspath(self.notebook_dir),
844 'notebook_dir': os.path.abspath(self.notebook_dir),
845 'pid': os.getpid()
844 }
846 }
845
847
846 def write_server_info_file(self):
848 def write_server_info_file(self):
847 """Write the result of server_info() to the JSON file info_file."""
849 """Write the result of server_info() to the JSON file info_file."""
848 with open(self.info_file, 'w') as f:
850 with open(self.info_file, 'w') as f:
849 json.dump(self.server_info(), f, indent=2)
851 json.dump(self.server_info(), f, indent=2)
850
852
851 def remove_server_info_file(self):
853 def remove_server_info_file(self):
852 """Remove the nbserver-<pid>.json file created for this server.
854 """Remove the nbserver-<pid>.json file created for this server.
853
855
854 Ignores the error raised when the file has already been removed.
856 Ignores the error raised when the file has already been removed.
855 """
857 """
856 try:
858 try:
857 os.unlink(self.info_file)
859 os.unlink(self.info_file)
858 except OSError as e:
860 except OSError as e:
859 if e.errno != errno.ENOENT:
861 if e.errno != errno.ENOENT:
860 raise
862 raise
861
863
862 def start(self):
864 def start(self):
863 """ Start the IPython Notebook server app, after initialization
865 """ Start the IPython Notebook server app, after initialization
864
866
865 This method takes no arguments so all configuration and initialization
867 This method takes no arguments so all configuration and initialization
866 must be done prior to calling this method."""
868 must be done prior to calling this method."""
867 if self.subapp is not None:
869 if self.subapp is not None:
868 return self.subapp.start()
870 return self.subapp.start()
869
871
870 info = self.log.info
872 info = self.log.info
871 for line in self.notebook_info().split("\n"):
873 for line in self.notebook_info().split("\n"):
872 info(line)
874 info(line)
873 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
875 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
874
876
875 self.write_server_info_file()
877 self.write_server_info_file()
876
878
877 if self.open_browser or self.file_to_run:
879 if self.open_browser or self.file_to_run:
878 try:
880 try:
879 browser = webbrowser.get(self.browser or None)
881 browser = webbrowser.get(self.browser or None)
880 except webbrowser.Error as e:
882 except webbrowser.Error as e:
881 self.log.warn('No web browser found: %s.' % e)
883 self.log.warn('No web browser found: %s.' % e)
882 browser = None
884 browser = None
883
885
884 if self.file_to_run:
886 if self.file_to_run:
885 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
887 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
886 if not os.path.exists(fullpath):
888 if not os.path.exists(fullpath):
887 self.log.critical("%s does not exist" % fullpath)
889 self.log.critical("%s does not exist" % fullpath)
888 self.exit(1)
890 self.exit(1)
889
891
890 uri = url_path_join('notebooks', self.file_to_run)
892 uri = url_path_join('notebooks', self.file_to_run)
891 else:
893 else:
892 uri = 'tree'
894 uri = 'tree'
893 if browser:
895 if browser:
894 b = lambda : browser.open(url_path_join(self.connection_url, uri),
896 b = lambda : browser.open(url_path_join(self.connection_url, uri),
895 new=2)
897 new=2)
896 threading.Thread(target=b).start()
898 threading.Thread(target=b).start()
897 try:
899 try:
898 ioloop.IOLoop.instance().start()
900 ioloop.IOLoop.instance().start()
899 except KeyboardInterrupt:
901 except KeyboardInterrupt:
900 info("Interrupted...")
902 info("Interrupted...")
901 finally:
903 finally:
902 self.cleanup_kernels()
904 self.cleanup_kernels()
903 self.remove_server_info_file()
905 self.remove_server_info_file()
904
906
905
907
906 def list_running_servers(profile='default'):
908 def list_running_servers(profile='default'):
907 """Iterate over the server info files of running notebook servers.
909 """Iterate over the server info files of running notebook servers.
908
910
909 Given a profile name, find nbserver-* files in the security directory of
911 Given a profile name, find nbserver-* files in the security directory of
910 that profile, and yield dicts of their information, each one pertaining to
912 that profile, and yield dicts of their information, each one pertaining to
911 a currently running notebook server instance.
913 a currently running notebook server instance.
912 """
914 """
913 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
915 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
914 for file in os.listdir(pd.security_dir):
916 for file in os.listdir(pd.security_dir):
915 if file.startswith('nbserver-'):
917 if file.startswith('nbserver-'):
916 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
918 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
917 yield json.load(f)
919 info = json.load(f)
918
920
921 # Simple check whether that process is really still running
922 if check_pid(info['pid']):
923 yield info
924 else:
925 # If the process has died, try to delete its info file
926 try:
927 os.unlink(file)
928 except OSError:
929 pass # TODO: This should warn or log or something
919 #-----------------------------------------------------------------------------
930 #-----------------------------------------------------------------------------
920 # Main entry point
931 # Main entry point
921 #-----------------------------------------------------------------------------
932 #-----------------------------------------------------------------------------
922
933
923 launch_new_instance = NotebookApp.launch_instance
934 launch_new_instance = NotebookApp.launch_instance
924
935
@@ -1,276 +1,260 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 The Base Application class for IPython.parallel apps
3 The Base Application class for IPython.parallel apps
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 * Min RK
8 * Min RK
9
9
10 """
10 """
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Copyright (C) 2008-2011 The IPython Development Team
13 # Copyright (C) 2008-2011 The IPython Development Team
14 #
14 #
15 # Distributed under the terms of the BSD License. The full license is in
15 # Distributed under the terms of the BSD License. The full license is in
16 # the file COPYING, distributed as part of this software.
16 # the file COPYING, distributed as part of this software.
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
20 # Imports
20 # Imports
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22
22
23 import os
23 import os
24 import logging
24 import logging
25 import re
25 import re
26 import sys
26 import sys
27
27
28 from subprocess import Popen, PIPE
28 from subprocess import Popen, PIPE
29
29
30 from IPython.config.application import catch_config_error, LevelFormatter
30 from IPython.config.application import catch_config_error, LevelFormatter
31 from IPython.core import release
31 from IPython.core import release
32 from IPython.core.crashhandler import CrashHandler
32 from IPython.core.crashhandler import CrashHandler
33 from IPython.core.application import (
33 from IPython.core.application import (
34 BaseIPythonApplication,
34 BaseIPythonApplication,
35 base_aliases as base_ip_aliases,
35 base_aliases as base_ip_aliases,
36 base_flags as base_ip_flags
36 base_flags as base_ip_flags
37 )
37 )
38 from IPython.utils.path import expand_path
38 from IPython.utils.path import expand_path
39 from IPython.utils.process import check_pid
39 from IPython.utils import py3compat
40 from IPython.utils import py3compat
40 from IPython.utils.py3compat import unicode_type
41 from IPython.utils.py3compat import unicode_type
41
42
42 from IPython.utils.traitlets import Unicode, Bool, Instance, Dict
43 from IPython.utils.traitlets import Unicode, Bool, Instance, Dict
43
44
44 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
45 # Module errors
46 # Module errors
46 #-----------------------------------------------------------------------------
47 #-----------------------------------------------------------------------------
47
48
48 class PIDFileError(Exception):
49 class PIDFileError(Exception):
49 pass
50 pass
50
51
51
52
52 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
53 # Crash handler for this application
54 # Crash handler for this application
54 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
55
56
56 class ParallelCrashHandler(CrashHandler):
57 class ParallelCrashHandler(CrashHandler):
57 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
58 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
58
59
59 def __init__(self, app):
60 def __init__(self, app):
60 contact_name = release.authors['Min'][0]
61 contact_name = release.authors['Min'][0]
61 contact_email = release.author_email
62 contact_email = release.author_email
62 bug_tracker = 'https://github.com/ipython/ipython/issues'
63 bug_tracker = 'https://github.com/ipython/ipython/issues'
63 super(ParallelCrashHandler,self).__init__(
64 super(ParallelCrashHandler,self).__init__(
64 app, contact_name, contact_email, bug_tracker
65 app, contact_name, contact_email, bug_tracker
65 )
66 )
66
67
67
68
68 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
69 # Main application
70 # Main application
70 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
71 base_aliases = {}
72 base_aliases = {}
72 base_aliases.update(base_ip_aliases)
73 base_aliases.update(base_ip_aliases)
73 base_aliases.update({
74 base_aliases.update({
74 'work-dir' : 'BaseParallelApplication.work_dir',
75 'work-dir' : 'BaseParallelApplication.work_dir',
75 'log-to-file' : 'BaseParallelApplication.log_to_file',
76 'log-to-file' : 'BaseParallelApplication.log_to_file',
76 'clean-logs' : 'BaseParallelApplication.clean_logs',
77 'clean-logs' : 'BaseParallelApplication.clean_logs',
77 'log-url' : 'BaseParallelApplication.log_url',
78 'log-url' : 'BaseParallelApplication.log_url',
78 'cluster-id' : 'BaseParallelApplication.cluster_id',
79 'cluster-id' : 'BaseParallelApplication.cluster_id',
79 })
80 })
80
81
81 base_flags = {
82 base_flags = {
82 'log-to-file' : (
83 'log-to-file' : (
83 {'BaseParallelApplication' : {'log_to_file' : True}},
84 {'BaseParallelApplication' : {'log_to_file' : True}},
84 "send log output to a file"
85 "send log output to a file"
85 )
86 )
86 }
87 }
87 base_flags.update(base_ip_flags)
88 base_flags.update(base_ip_flags)
88
89
89 class BaseParallelApplication(BaseIPythonApplication):
90 class BaseParallelApplication(BaseIPythonApplication):
90 """The base Application for IPython.parallel apps
91 """The base Application for IPython.parallel apps
91
92
92 Principle extensions to BaseIPyythonApplication:
93 Principle extensions to BaseIPyythonApplication:
93
94
94 * work_dir
95 * work_dir
95 * remote logging via pyzmq
96 * remote logging via pyzmq
96 * IOLoop instance
97 * IOLoop instance
97 """
98 """
98
99
99 crash_handler_class = ParallelCrashHandler
100 crash_handler_class = ParallelCrashHandler
100
101
101 def _log_level_default(self):
102 def _log_level_default(self):
102 # temporarily override default_log_level to INFO
103 # temporarily override default_log_level to INFO
103 return logging.INFO
104 return logging.INFO
104
105
105 def _log_format_default(self):
106 def _log_format_default(self):
106 """override default log format to include time"""
107 """override default log format to include time"""
107 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
108 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
108
109
109 work_dir = Unicode(py3compat.getcwd(), config=True,
110 work_dir = Unicode(py3compat.getcwd(), config=True,
110 help='Set the working dir for the process.'
111 help='Set the working dir for the process.'
111 )
112 )
112 def _work_dir_changed(self, name, old, new):
113 def _work_dir_changed(self, name, old, new):
113 self.work_dir = unicode_type(expand_path(new))
114 self.work_dir = unicode_type(expand_path(new))
114
115
115 log_to_file = Bool(config=True,
116 log_to_file = Bool(config=True,
116 help="whether to log to a file")
117 help="whether to log to a file")
117
118
118 clean_logs = Bool(False, config=True,
119 clean_logs = Bool(False, config=True,
119 help="whether to cleanup old logfiles before starting")
120 help="whether to cleanup old logfiles before starting")
120
121
121 log_url = Unicode('', config=True,
122 log_url = Unicode('', config=True,
122 help="The ZMQ URL of the iplogger to aggregate logging.")
123 help="The ZMQ URL of the iplogger to aggregate logging.")
123
124
124 cluster_id = Unicode('', config=True,
125 cluster_id = Unicode('', config=True,
125 help="""String id to add to runtime files, to prevent name collisions when
126 help="""String id to add to runtime files, to prevent name collisions when
126 using multiple clusters with a single profile simultaneously.
127 using multiple clusters with a single profile simultaneously.
127
128
128 When set, files will be named like: 'ipcontroller-<cluster_id>-engine.json'
129 When set, files will be named like: 'ipcontroller-<cluster_id>-engine.json'
129
130
130 Since this is text inserted into filenames, typical recommendations apply:
131 Since this is text inserted into filenames, typical recommendations apply:
131 Simple character strings are ideal, and spaces are not recommended (but should
132 Simple character strings are ideal, and spaces are not recommended (but should
132 generally work).
133 generally work).
133 """
134 """
134 )
135 )
135 def _cluster_id_changed(self, name, old, new):
136 def _cluster_id_changed(self, name, old, new):
136 self.name = self.__class__.name
137 self.name = self.__class__.name
137 if new:
138 if new:
138 self.name += '-%s'%new
139 self.name += '-%s'%new
139
140
140 def _config_files_default(self):
141 def _config_files_default(self):
141 return ['ipcontroller_config.py', 'ipengine_config.py', 'ipcluster_config.py']
142 return ['ipcontroller_config.py', 'ipengine_config.py', 'ipcluster_config.py']
142
143
143 loop = Instance('zmq.eventloop.ioloop.IOLoop')
144 loop = Instance('zmq.eventloop.ioloop.IOLoop')
144 def _loop_default(self):
145 def _loop_default(self):
145 from zmq.eventloop.ioloop import IOLoop
146 from zmq.eventloop.ioloop import IOLoop
146 return IOLoop.instance()
147 return IOLoop.instance()
147
148
148 aliases = Dict(base_aliases)
149 aliases = Dict(base_aliases)
149 flags = Dict(base_flags)
150 flags = Dict(base_flags)
150
151
151 @catch_config_error
152 @catch_config_error
152 def initialize(self, argv=None):
153 def initialize(self, argv=None):
153 """initialize the app"""
154 """initialize the app"""
154 super(BaseParallelApplication, self).initialize(argv)
155 super(BaseParallelApplication, self).initialize(argv)
155 self.to_work_dir()
156 self.to_work_dir()
156 self.reinit_logging()
157 self.reinit_logging()
157
158
158 def to_work_dir(self):
159 def to_work_dir(self):
159 wd = self.work_dir
160 wd = self.work_dir
160 if unicode_type(wd) != py3compat.getcwd():
161 if unicode_type(wd) != py3compat.getcwd():
161 os.chdir(wd)
162 os.chdir(wd)
162 self.log.info("Changing to working dir: %s" % wd)
163 self.log.info("Changing to working dir: %s" % wd)
163 # This is the working dir by now.
164 # This is the working dir by now.
164 sys.path.insert(0, '')
165 sys.path.insert(0, '')
165
166
166 def reinit_logging(self):
167 def reinit_logging(self):
167 # Remove old log files
168 # Remove old log files
168 log_dir = self.profile_dir.log_dir
169 log_dir = self.profile_dir.log_dir
169 if self.clean_logs:
170 if self.clean_logs:
170 for f in os.listdir(log_dir):
171 for f in os.listdir(log_dir):
171 if re.match(r'%s-\d+\.(log|err|out)' % self.name, f):
172 if re.match(r'%s-\d+\.(log|err|out)' % self.name, f):
172 try:
173 try:
173 os.remove(os.path.join(log_dir, f))
174 os.remove(os.path.join(log_dir, f))
174 except (OSError, IOError):
175 except (OSError, IOError):
175 # probably just conflict from sibling process
176 # probably just conflict from sibling process
176 # already removing it
177 # already removing it
177 pass
178 pass
178 if self.log_to_file:
179 if self.log_to_file:
179 # Start logging to the new log file
180 # Start logging to the new log file
180 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
181 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
181 logfile = os.path.join(log_dir, log_filename)
182 logfile = os.path.join(log_dir, log_filename)
182 open_log_file = open(logfile, 'w')
183 open_log_file = open(logfile, 'w')
183 else:
184 else:
184 open_log_file = None
185 open_log_file = None
185 if open_log_file is not None:
186 if open_log_file is not None:
186 while self.log.handlers:
187 while self.log.handlers:
187 self.log.removeHandler(self.log.handlers[0])
188 self.log.removeHandler(self.log.handlers[0])
188 self._log_handler = logging.StreamHandler(open_log_file)
189 self._log_handler = logging.StreamHandler(open_log_file)
189 self.log.addHandler(self._log_handler)
190 self.log.addHandler(self._log_handler)
190 else:
191 else:
191 self._log_handler = self.log.handlers[0]
192 self._log_handler = self.log.handlers[0]
192 # Add timestamps to log format:
193 # Add timestamps to log format:
193 self._log_formatter = LevelFormatter(self.log_format,
194 self._log_formatter = LevelFormatter(self.log_format,
194 datefmt=self.log_datefmt)
195 datefmt=self.log_datefmt)
195 self._log_handler.setFormatter(self._log_formatter)
196 self._log_handler.setFormatter(self._log_formatter)
196 # do not propagate log messages to root logger
197 # do not propagate log messages to root logger
197 # ipcluster app will sometimes print duplicate messages during shutdown
198 # ipcluster app will sometimes print duplicate messages during shutdown
198 # if this is 1 (default):
199 # if this is 1 (default):
199 self.log.propagate = False
200 self.log.propagate = False
200
201
201 def write_pid_file(self, overwrite=False):
202 def write_pid_file(self, overwrite=False):
202 """Create a .pid file in the pid_dir with my pid.
203 """Create a .pid file in the pid_dir with my pid.
203
204
204 This must be called after pre_construct, which sets `self.pid_dir`.
205 This must be called after pre_construct, which sets `self.pid_dir`.
205 This raises :exc:`PIDFileError` if the pid file exists already.
206 This raises :exc:`PIDFileError` if the pid file exists already.
206 """
207 """
207 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
208 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
208 if os.path.isfile(pid_file):
209 if os.path.isfile(pid_file):
209 pid = self.get_pid_from_file()
210 pid = self.get_pid_from_file()
210 if not overwrite:
211 if not overwrite:
211 raise PIDFileError(
212 raise PIDFileError(
212 'The pid file [%s] already exists. \nThis could mean that this '
213 'The pid file [%s] already exists. \nThis could mean that this '
213 'server is already running with [pid=%s].' % (pid_file, pid)
214 'server is already running with [pid=%s].' % (pid_file, pid)
214 )
215 )
215 with open(pid_file, 'w') as f:
216 with open(pid_file, 'w') as f:
216 self.log.info("Creating pid file: %s" % pid_file)
217 self.log.info("Creating pid file: %s" % pid_file)
217 f.write(repr(os.getpid())+'\n')
218 f.write(repr(os.getpid())+'\n')
218
219
219 def remove_pid_file(self):
220 def remove_pid_file(self):
220 """Remove the pid file.
221 """Remove the pid file.
221
222
222 This should be called at shutdown by registering a callback with
223 This should be called at shutdown by registering a callback with
223 :func:`reactor.addSystemEventTrigger`. This needs to return
224 :func:`reactor.addSystemEventTrigger`. This needs to return
224 ``None``.
225 ``None``.
225 """
226 """
226 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
227 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
227 if os.path.isfile(pid_file):
228 if os.path.isfile(pid_file):
228 try:
229 try:
229 self.log.info("Removing pid file: %s" % pid_file)
230 self.log.info("Removing pid file: %s" % pid_file)
230 os.remove(pid_file)
231 os.remove(pid_file)
231 except:
232 except:
232 self.log.warn("Error removing the pid file: %s" % pid_file)
233 self.log.warn("Error removing the pid file: %s" % pid_file)
233
234
234 def get_pid_from_file(self):
235 def get_pid_from_file(self):
235 """Get the pid from the pid file.
236 """Get the pid from the pid file.
236
237
237 If the pid file doesn't exist a :exc:`PIDFileError` is raised.
238 If the pid file doesn't exist a :exc:`PIDFileError` is raised.
238 """
239 """
239 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
240 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
240 if os.path.isfile(pid_file):
241 if os.path.isfile(pid_file):
241 with open(pid_file, 'r') as f:
242 with open(pid_file, 'r') as f:
242 s = f.read().strip()
243 s = f.read().strip()
243 try:
244 try:
244 pid = int(s)
245 pid = int(s)
245 except:
246 except:
246 raise PIDFileError("invalid pid file: %s (contents: %r)"%(pid_file, s))
247 raise PIDFileError("invalid pid file: %s (contents: %r)"%(pid_file, s))
247 return pid
248 return pid
248 else:
249 else:
249 raise PIDFileError('pid file not found: %s' % pid_file)
250 raise PIDFileError('pid file not found: %s' % pid_file)
250
251
251 def check_pid(self, pid):
252 def check_pid(self, pid):
252 if os.name == 'nt':
253 try:
253 try:
254 return check_pid(pid)
254 import ctypes
255 except Exception:
255 # returns 0 if no such process (of ours) exists
256 self.log.warn(
256 # positive int otherwise
257 "Could not determine whether pid %i is running. "
257 p = ctypes.windll.kernel32.OpenProcess(1,0,pid)
258 " Making the likely assumption that it is."%pid
258 except Exception:
259 )
259 self.log.warn(
260 return True
260 "Could not determine whether pid %i is running via `OpenProcess`. "
261 " Making the likely assumption that it is."%pid
262 )
263 return True
264 return bool(p)
265 else:
266 try:
267 p = Popen(['ps','x'], stdout=PIPE, stderr=PIPE)
268 output,_ = p.communicate()
269 except OSError:
270 self.log.warn(
271 "Could not determine whether pid %i is running via `ps x`. "
272 " Making the likely assumption that it is."%pid
273 )
274 return True
275 pids = list(map(int, re.findall(br'^\W*\d+', output, re.MULTILINE)))
276 return pid in pids
@@ -1,213 +1,225 b''
1 """Posix-specific implementation of process utilities.
1 """Posix-specific implementation of process utilities.
2
2
3 This file is only meant to be imported by process.py, not by end-users.
3 This file is only meant to be imported by process.py, not by end-users.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from __future__ import print_function
16 from __future__ import print_function
17
17
18 # Stdlib
18 # Stdlib
19 import errno
20 import os
19 import subprocess as sp
21 import subprocess as sp
20 import sys
22 import sys
21
23
22 from IPython.external import pexpect
24 from IPython.external import pexpect
23
25
24 # Our own
26 # Our own
25 from ._process_common import getoutput, arg_split
27 from ._process_common import getoutput, arg_split
26 from IPython.utils import py3compat
28 from IPython.utils import py3compat
27 from IPython.utils.encoding import DEFAULT_ENCODING
29 from IPython.utils.encoding import DEFAULT_ENCODING
28
30
29 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
30 # Function definitions
32 # Function definitions
31 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
32
34
33 def _find_cmd(cmd):
35 def _find_cmd(cmd):
34 """Find the full path to a command using which."""
36 """Find the full path to a command using which."""
35
37
36 path = sp.Popen(['/usr/bin/env', 'which', cmd],
38 path = sp.Popen(['/usr/bin/env', 'which', cmd],
37 stdout=sp.PIPE, stderr=sp.PIPE).communicate()[0]
39 stdout=sp.PIPE, stderr=sp.PIPE).communicate()[0]
38 return py3compat.bytes_to_str(path)
40 return py3compat.bytes_to_str(path)
39
41
40
42
41 class ProcessHandler(object):
43 class ProcessHandler(object):
42 """Execute subprocesses under the control of pexpect.
44 """Execute subprocesses under the control of pexpect.
43 """
45 """
44 # Timeout in seconds to wait on each reading of the subprocess' output.
46 # Timeout in seconds to wait on each reading of the subprocess' output.
45 # This should not be set too low to avoid cpu overusage from our side,
47 # This should not be set too low to avoid cpu overusage from our side,
46 # since we read in a loop whose period is controlled by this timeout.
48 # since we read in a loop whose period is controlled by this timeout.
47 read_timeout = 0.05
49 read_timeout = 0.05
48
50
49 # Timeout to give a process if we receive SIGINT, between sending the
51 # Timeout to give a process if we receive SIGINT, between sending the
50 # SIGINT to the process and forcefully terminating it.
52 # SIGINT to the process and forcefully terminating it.
51 terminate_timeout = 0.2
53 terminate_timeout = 0.2
52
54
53 # File object where stdout and stderr of the subprocess will be written
55 # File object where stdout and stderr of the subprocess will be written
54 logfile = None
56 logfile = None
55
57
56 # Shell to call for subprocesses to execute
58 # Shell to call for subprocesses to execute
57 _sh = None
59 _sh = None
58
60
59 @property
61 @property
60 def sh(self):
62 def sh(self):
61 if self._sh is None:
63 if self._sh is None:
62 self._sh = pexpect.which('sh')
64 self._sh = pexpect.which('sh')
63 if self._sh is None:
65 if self._sh is None:
64 raise OSError('"sh" shell not found')
66 raise OSError('"sh" shell not found')
65
67
66 return self._sh
68 return self._sh
67
69
68 def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None):
70 def __init__(self, logfile=None, read_timeout=None, terminate_timeout=None):
69 """Arguments are used for pexpect calls."""
71 """Arguments are used for pexpect calls."""
70 self.read_timeout = (ProcessHandler.read_timeout if read_timeout is
72 self.read_timeout = (ProcessHandler.read_timeout if read_timeout is
71 None else read_timeout)
73 None else read_timeout)
72 self.terminate_timeout = (ProcessHandler.terminate_timeout if
74 self.terminate_timeout = (ProcessHandler.terminate_timeout if
73 terminate_timeout is None else
75 terminate_timeout is None else
74 terminate_timeout)
76 terminate_timeout)
75 self.logfile = sys.stdout if logfile is None else logfile
77 self.logfile = sys.stdout if logfile is None else logfile
76
78
77 def getoutput(self, cmd):
79 def getoutput(self, cmd):
78 """Run a command and return its stdout/stderr as a string.
80 """Run a command and return its stdout/stderr as a string.
79
81
80 Parameters
82 Parameters
81 ----------
83 ----------
82 cmd : str
84 cmd : str
83 A command to be executed in the system shell.
85 A command to be executed in the system shell.
84
86
85 Returns
87 Returns
86 -------
88 -------
87 output : str
89 output : str
88 A string containing the combination of stdout and stderr from the
90 A string containing the combination of stdout and stderr from the
89 subprocess, in whatever order the subprocess originally wrote to its
91 subprocess, in whatever order the subprocess originally wrote to its
90 file descriptors (so the order of the information in this string is the
92 file descriptors (so the order of the information in this string is the
91 correct order as would be seen if running the command in a terminal).
93 correct order as would be seen if running the command in a terminal).
92 """
94 """
93 try:
95 try:
94 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
96 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
95 except KeyboardInterrupt:
97 except KeyboardInterrupt:
96 print('^C', file=sys.stderr, end='')
98 print('^C', file=sys.stderr, end='')
97
99
98 def getoutput_pexpect(self, cmd):
100 def getoutput_pexpect(self, cmd):
99 """Run a command and return its stdout/stderr as a string.
101 """Run a command and return its stdout/stderr as a string.
100
102
101 Parameters
103 Parameters
102 ----------
104 ----------
103 cmd : str
105 cmd : str
104 A command to be executed in the system shell.
106 A command to be executed in the system shell.
105
107
106 Returns
108 Returns
107 -------
109 -------
108 output : str
110 output : str
109 A string containing the combination of stdout and stderr from the
111 A string containing the combination of stdout and stderr from the
110 subprocess, in whatever order the subprocess originally wrote to its
112 subprocess, in whatever order the subprocess originally wrote to its
111 file descriptors (so the order of the information in this string is the
113 file descriptors (so the order of the information in this string is the
112 correct order as would be seen if running the command in a terminal).
114 correct order as would be seen if running the command in a terminal).
113 """
115 """
114 try:
116 try:
115 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
117 return pexpect.run(self.sh, args=['-c', cmd]).replace('\r\n', '\n')
116 except KeyboardInterrupt:
118 except KeyboardInterrupt:
117 print('^C', file=sys.stderr, end='')
119 print('^C', file=sys.stderr, end='')
118
120
119 def system(self, cmd):
121 def system(self, cmd):
120 """Execute a command in a subshell.
122 """Execute a command in a subshell.
121
123
122 Parameters
124 Parameters
123 ----------
125 ----------
124 cmd : str
126 cmd : str
125 A command to be executed in the system shell.
127 A command to be executed in the system shell.
126
128
127 Returns
129 Returns
128 -------
130 -------
129 int : child's exitstatus
131 int : child's exitstatus
130 """
132 """
131 # Get likely encoding for the output.
133 # Get likely encoding for the output.
132 enc = DEFAULT_ENCODING
134 enc = DEFAULT_ENCODING
133
135
134 # Patterns to match on the output, for pexpect. We read input and
136 # Patterns to match on the output, for pexpect. We read input and
135 # allow either a short timeout or EOF
137 # allow either a short timeout or EOF
136 patterns = [pexpect.TIMEOUT, pexpect.EOF]
138 patterns = [pexpect.TIMEOUT, pexpect.EOF]
137 # the index of the EOF pattern in the list.
139 # the index of the EOF pattern in the list.
138 # even though we know it's 1, this call means we don't have to worry if
140 # even though we know it's 1, this call means we don't have to worry if
139 # we change the above list, and forget to change this value:
141 # we change the above list, and forget to change this value:
140 EOF_index = patterns.index(pexpect.EOF)
142 EOF_index = patterns.index(pexpect.EOF)
141 # The size of the output stored so far in the process output buffer.
143 # The size of the output stored so far in the process output buffer.
142 # Since pexpect only appends to this buffer, each time we print we
144 # Since pexpect only appends to this buffer, each time we print we
143 # record how far we've printed, so that next time we only print *new*
145 # record how far we've printed, so that next time we only print *new*
144 # content from the buffer.
146 # content from the buffer.
145 out_size = 0
147 out_size = 0
146 try:
148 try:
147 # Since we're not really searching the buffer for text patterns, we
149 # Since we're not really searching the buffer for text patterns, we
148 # can set pexpect's search window to be tiny and it won't matter.
150 # can set pexpect's search window to be tiny and it won't matter.
149 # We only search for the 'patterns' timeout or EOF, which aren't in
151 # We only search for the 'patterns' timeout or EOF, which aren't in
150 # the text itself.
152 # the text itself.
151 #child = pexpect.spawn(pcmd, searchwindowsize=1)
153 #child = pexpect.spawn(pcmd, searchwindowsize=1)
152 if hasattr(pexpect, 'spawnb'):
154 if hasattr(pexpect, 'spawnb'):
153 child = pexpect.spawnb(self.sh, args=['-c', cmd]) # Pexpect-U
155 child = pexpect.spawnb(self.sh, args=['-c', cmd]) # Pexpect-U
154 else:
156 else:
155 child = pexpect.spawn(self.sh, args=['-c', cmd]) # Vanilla Pexpect
157 child = pexpect.spawn(self.sh, args=['-c', cmd]) # Vanilla Pexpect
156 flush = sys.stdout.flush
158 flush = sys.stdout.flush
157 while True:
159 while True:
158 # res is the index of the pattern that caused the match, so we
160 # res is the index of the pattern that caused the match, so we
159 # know whether we've finished (if we matched EOF) or not
161 # know whether we've finished (if we matched EOF) or not
160 res_idx = child.expect_list(patterns, self.read_timeout)
162 res_idx = child.expect_list(patterns, self.read_timeout)
161 print(child.before[out_size:].decode(enc, 'replace'), end='')
163 print(child.before[out_size:].decode(enc, 'replace'), end='')
162 flush()
164 flush()
163 if res_idx==EOF_index:
165 if res_idx==EOF_index:
164 break
166 break
165 # Update the pointer to what we've already printed
167 # Update the pointer to what we've already printed
166 out_size = len(child.before)
168 out_size = len(child.before)
167 except KeyboardInterrupt:
169 except KeyboardInterrupt:
168 # We need to send ^C to the process. The ascii code for '^C' is 3
170 # We need to send ^C to the process. The ascii code for '^C' is 3
169 # (the character is known as ETX for 'End of Text', see
171 # (the character is known as ETX for 'End of Text', see
170 # curses.ascii.ETX).
172 # curses.ascii.ETX).
171 child.sendline(chr(3))
173 child.sendline(chr(3))
172 # Read and print any more output the program might produce on its
174 # Read and print any more output the program might produce on its
173 # way out.
175 # way out.
174 try:
176 try:
175 out_size = len(child.before)
177 out_size = len(child.before)
176 child.expect_list(patterns, self.terminate_timeout)
178 child.expect_list(patterns, self.terminate_timeout)
177 print(child.before[out_size:].decode(enc, 'replace'), end='')
179 print(child.before[out_size:].decode(enc, 'replace'), end='')
178 sys.stdout.flush()
180 sys.stdout.flush()
179 except KeyboardInterrupt:
181 except KeyboardInterrupt:
180 # Impatient users tend to type it multiple times
182 # Impatient users tend to type it multiple times
181 pass
183 pass
182 finally:
184 finally:
183 # Ensure the subprocess really is terminated
185 # Ensure the subprocess really is terminated
184 child.terminate(force=True)
186 child.terminate(force=True)
185 # add isalive check, to ensure exitstatus is set:
187 # add isalive check, to ensure exitstatus is set:
186 child.isalive()
188 child.isalive()
187
189
188 # We follow the subprocess pattern, returning either the exit status
190 # We follow the subprocess pattern, returning either the exit status
189 # as a positive number, or the terminating signal as a negative
191 # as a positive number, or the terminating signal as a negative
190 # number.
192 # number.
191 # on Linux, sh returns 128+n for signals terminating child processes on Linux
193 # on Linux, sh returns 128+n for signals terminating child processes on Linux
192 # on BSD (OS X), the signal code is set instead
194 # on BSD (OS X), the signal code is set instead
193 if child.exitstatus is None:
195 if child.exitstatus is None:
194 # on WIFSIGNALED, pexpect sets signalstatus, leaving exitstatus=None
196 # on WIFSIGNALED, pexpect sets signalstatus, leaving exitstatus=None
195 if child.signalstatus is None:
197 if child.signalstatus is None:
196 # this condition may never occur,
198 # this condition may never occur,
197 # but let's be certain we always return an integer.
199 # but let's be certain we always return an integer.
198 return 0
200 return 0
199 return -child.signalstatus
201 return -child.signalstatus
200 if child.exitstatus > 128:
202 if child.exitstatus > 128:
201 return -(child.exitstatus - 128)
203 return -(child.exitstatus - 128)
202 return child.exitstatus
204 return child.exitstatus
203
205
204
206
205 # Make system() with a functional interface for outside use. Note that we use
207 # Make system() with a functional interface for outside use. Note that we use
206 # getoutput() from the _common utils, which is built on top of popen(). Using
208 # getoutput() from the _common utils, which is built on top of popen(). Using
207 # pexpect to get subprocess output produces difficult to parse output, since
209 # pexpect to get subprocess output produces difficult to parse output, since
208 # programs think they are talking to a tty and produce highly formatted output
210 # programs think they are talking to a tty and produce highly formatted output
209 # (ls is a good example) that makes them hard.
211 # (ls is a good example) that makes them hard.
210 system = ProcessHandler().system
212 system = ProcessHandler().system
211
213
212
214 def check_pid(pid):
213
215 try:
216 os.kill(pid, 0)
217 except OSError as err:
218 if err.errno == errno.ESRCH:
219 return False
220 elif err.errno == errno.EPERM:
221 # Don't have permission to signal the process - probably means it exists
222 return True
223 raise
224 else:
225 return True
@@ -1,187 +1,192 b''
1 """Windows-specific implementation of process utilities.
1 """Windows-specific implementation of process utilities.
2
2
3 This file is only meant to be imported by process.py, not by end-users.
3 This file is only meant to be imported by process.py, not by end-users.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2010-2011 The IPython Development Team
7 # Copyright (C) 2010-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from __future__ import print_function
16 from __future__ import print_function
17
17
18 # stdlib
18 # stdlib
19 import os
19 import os
20 import sys
20 import sys
21 import ctypes
21 import ctypes
22
22
23 from ctypes import c_int, POINTER
23 from ctypes import c_int, POINTER
24 from ctypes.wintypes import LPCWSTR, HLOCAL
24 from ctypes.wintypes import LPCWSTR, HLOCAL
25 from subprocess import STDOUT
25 from subprocess import STDOUT
26
26
27 # our own imports
27 # our own imports
28 from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split
28 from ._process_common import read_no_interrupt, process_handler, arg_split as py_arg_split
29 from . import py3compat
29 from . import py3compat
30 from .encoding import DEFAULT_ENCODING
30 from .encoding import DEFAULT_ENCODING
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Function definitions
33 # Function definitions
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 class AvoidUNCPath(object):
36 class AvoidUNCPath(object):
37 """A context manager to protect command execution from UNC paths.
37 """A context manager to protect command execution from UNC paths.
38
38
39 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
39 In the Win32 API, commands can't be invoked with the cwd being a UNC path.
40 This context manager temporarily changes directory to the 'C:' drive on
40 This context manager temporarily changes directory to the 'C:' drive on
41 entering, and restores the original working directory on exit.
41 entering, and restores the original working directory on exit.
42
42
43 The context manager returns the starting working directory *if* it made a
43 The context manager returns the starting working directory *if* it made a
44 change and None otherwise, so that users can apply the necessary adjustment
44 change and None otherwise, so that users can apply the necessary adjustment
45 to their system calls in the event of a change.
45 to their system calls in the event of a change.
46
46
47 Examples
47 Examples
48 --------
48 --------
49 ::
49 ::
50 cmd = 'dir'
50 cmd = 'dir'
51 with AvoidUNCPath() as path:
51 with AvoidUNCPath() as path:
52 if path is not None:
52 if path is not None:
53 cmd = '"pushd %s &&"%s' % (path, cmd)
53 cmd = '"pushd %s &&"%s' % (path, cmd)
54 os.system(cmd)
54 os.system(cmd)
55 """
55 """
56 def __enter__(self):
56 def __enter__(self):
57 self.path = py3compat.getcwd()
57 self.path = py3compat.getcwd()
58 self.is_unc_path = self.path.startswith(r"\\")
58 self.is_unc_path = self.path.startswith(r"\\")
59 if self.is_unc_path:
59 if self.is_unc_path:
60 # change to c drive (as cmd.exe cannot handle UNC addresses)
60 # change to c drive (as cmd.exe cannot handle UNC addresses)
61 os.chdir("C:")
61 os.chdir("C:")
62 return self.path
62 return self.path
63 else:
63 else:
64 # We return None to signal that there was no change in the working
64 # We return None to signal that there was no change in the working
65 # directory
65 # directory
66 return None
66 return None
67
67
68 def __exit__(self, exc_type, exc_value, traceback):
68 def __exit__(self, exc_type, exc_value, traceback):
69 if self.is_unc_path:
69 if self.is_unc_path:
70 os.chdir(self.path)
70 os.chdir(self.path)
71
71
72
72
73 def _find_cmd(cmd):
73 def _find_cmd(cmd):
74 """Find the full path to a .bat or .exe using the win32api module."""
74 """Find the full path to a .bat or .exe using the win32api module."""
75 try:
75 try:
76 from win32api import SearchPath
76 from win32api import SearchPath
77 except ImportError:
77 except ImportError:
78 raise ImportError('you need to have pywin32 installed for this to work')
78 raise ImportError('you need to have pywin32 installed for this to work')
79 else:
79 else:
80 PATH = os.environ['PATH']
80 PATH = os.environ['PATH']
81 extensions = ['.exe', '.com', '.bat', '.py']
81 extensions = ['.exe', '.com', '.bat', '.py']
82 path = None
82 path = None
83 for ext in extensions:
83 for ext in extensions:
84 try:
84 try:
85 path = SearchPath(PATH, cmd, ext)[0]
85 path = SearchPath(PATH, cmd, ext)[0]
86 except:
86 except:
87 pass
87 pass
88 if path is None:
88 if path is None:
89 raise OSError("command %r not found" % cmd)
89 raise OSError("command %r not found" % cmd)
90 else:
90 else:
91 return path
91 return path
92
92
93
93
94 def _system_body(p):
94 def _system_body(p):
95 """Callback for _system."""
95 """Callback for _system."""
96 enc = DEFAULT_ENCODING
96 enc = DEFAULT_ENCODING
97 for line in read_no_interrupt(p.stdout).splitlines():
97 for line in read_no_interrupt(p.stdout).splitlines():
98 line = line.decode(enc, 'replace')
98 line = line.decode(enc, 'replace')
99 print(line, file=sys.stdout)
99 print(line, file=sys.stdout)
100 for line in read_no_interrupt(p.stderr).splitlines():
100 for line in read_no_interrupt(p.stderr).splitlines():
101 line = line.decode(enc, 'replace')
101 line = line.decode(enc, 'replace')
102 print(line, file=sys.stderr)
102 print(line, file=sys.stderr)
103
103
104 # Wait to finish for returncode
104 # Wait to finish for returncode
105 return p.wait()
105 return p.wait()
106
106
107
107
108 def system(cmd):
108 def system(cmd):
109 """Win32 version of os.system() that works with network shares.
109 """Win32 version of os.system() that works with network shares.
110
110
111 Note that this implementation returns None, as meant for use in IPython.
111 Note that this implementation returns None, as meant for use in IPython.
112
112
113 Parameters
113 Parameters
114 ----------
114 ----------
115 cmd : str or list
115 cmd : str or list
116 A command to be executed in the system shell.
116 A command to be executed in the system shell.
117
117
118 Returns
118 Returns
119 -------
119 -------
120 None : we explicitly do NOT return the subprocess status code, as this
120 None : we explicitly do NOT return the subprocess status code, as this
121 utility is meant to be used extensively in IPython, where any return value
121 utility is meant to be used extensively in IPython, where any return value
122 would trigger :func:`sys.displayhook` calls.
122 would trigger :func:`sys.displayhook` calls.
123 """
123 """
124 # The controller provides interactivity with both
124 # The controller provides interactivity with both
125 # stdin and stdout
125 # stdin and stdout
126 #import _process_win32_controller
126 #import _process_win32_controller
127 #_process_win32_controller.system(cmd)
127 #_process_win32_controller.system(cmd)
128
128
129 with AvoidUNCPath() as path:
129 with AvoidUNCPath() as path:
130 if path is not None:
130 if path is not None:
131 cmd = '"pushd %s &&"%s' % (path, cmd)
131 cmd = '"pushd %s &&"%s' % (path, cmd)
132 return process_handler(cmd, _system_body)
132 return process_handler(cmd, _system_body)
133
133
134 def getoutput(cmd):
134 def getoutput(cmd):
135 """Return standard output of executing cmd in a shell.
135 """Return standard output of executing cmd in a shell.
136
136
137 Accepts the same arguments as os.system().
137 Accepts the same arguments as os.system().
138
138
139 Parameters
139 Parameters
140 ----------
140 ----------
141 cmd : str or list
141 cmd : str or list
142 A command to be executed in the system shell.
142 A command to be executed in the system shell.
143
143
144 Returns
144 Returns
145 -------
145 -------
146 stdout : str
146 stdout : str
147 """
147 """
148
148
149 with AvoidUNCPath() as path:
149 with AvoidUNCPath() as path:
150 if path is not None:
150 if path is not None:
151 cmd = '"pushd %s &&"%s' % (path, cmd)
151 cmd = '"pushd %s &&"%s' % (path, cmd)
152 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
152 out = process_handler(cmd, lambda p: p.communicate()[0], STDOUT)
153
153
154 if out is None:
154 if out is None:
155 out = b''
155 out = b''
156 return py3compat.bytes_to_str(out)
156 return py3compat.bytes_to_str(out)
157
157
158 try:
158 try:
159 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
159 CommandLineToArgvW = ctypes.windll.shell32.CommandLineToArgvW
160 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
160 CommandLineToArgvW.arg_types = [LPCWSTR, POINTER(c_int)]
161 CommandLineToArgvW.restype = POINTER(LPCWSTR)
161 CommandLineToArgvW.restype = POINTER(LPCWSTR)
162 LocalFree = ctypes.windll.kernel32.LocalFree
162 LocalFree = ctypes.windll.kernel32.LocalFree
163 LocalFree.res_type = HLOCAL
163 LocalFree.res_type = HLOCAL
164 LocalFree.arg_types = [HLOCAL]
164 LocalFree.arg_types = [HLOCAL]
165
165
166 def arg_split(commandline, posix=False, strict=True):
166 def arg_split(commandline, posix=False, strict=True):
167 """Split a command line's arguments in a shell-like manner.
167 """Split a command line's arguments in a shell-like manner.
168
168
169 This is a special version for windows that use a ctypes call to CommandLineToArgvW
169 This is a special version for windows that use a ctypes call to CommandLineToArgvW
170 to do the argv splitting. The posix paramter is ignored.
170 to do the argv splitting. The posix paramter is ignored.
171
171
172 If strict=False, process_common.arg_split(...strict=False) is used instead.
172 If strict=False, process_common.arg_split(...strict=False) is used instead.
173 """
173 """
174 #CommandLineToArgvW returns path to executable if called with empty string.
174 #CommandLineToArgvW returns path to executable if called with empty string.
175 if commandline.strip() == "":
175 if commandline.strip() == "":
176 return []
176 return []
177 if not strict:
177 if not strict:
178 # not really a cl-arg, fallback on _process_common
178 # not really a cl-arg, fallback on _process_common
179 return py_arg_split(commandline, posix=posix, strict=strict)
179 return py_arg_split(commandline, posix=posix, strict=strict)
180 argvn = c_int()
180 argvn = c_int()
181 result_pointer = CommandLineToArgvW(py3compat.cast_unicode(commandline.lstrip()), ctypes.byref(argvn))
181 result_pointer = CommandLineToArgvW(py3compat.cast_unicode(commandline.lstrip()), ctypes.byref(argvn))
182 result_array_type = LPCWSTR * argvn.value
182 result_array_type = LPCWSTR * argvn.value
183 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
183 result = [arg for arg in result_array_type.from_address(ctypes.addressof(result_pointer.contents))]
184 retval = LocalFree(result_pointer)
184 retval = LocalFree(result_pointer)
185 return result
185 return result
186 except AttributeError:
186 except AttributeError:
187 arg_split = py_arg_split
187 arg_split = py_arg_split
188
189 def check_pid(pid):
190 # OpenProcess returns 0 if no such process (of ours) exists
191 # positive int otherwise
192 return bool(ctypes.windll.kernel32.OpenProcess(1,0,pid))
@@ -1,123 +1,123 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Utilities for working with external processes.
3 Utilities for working with external processes.
4 """
4 """
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (C) 2008-2011 The IPython Development Team
7 # Copyright (C) 2008-2011 The IPython Development Team
8 #
8 #
9 # Distributed under the terms of the BSD License. The full license is in
9 # Distributed under the terms of the BSD License. The full license is in
10 # the file COPYING, distributed as part of this software.
10 # the file COPYING, distributed as part of this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from __future__ import print_function
16 from __future__ import print_function
17
17
18 # Stdlib
18 # Stdlib
19 import os
19 import os
20 import sys
20 import sys
21
21
22 # Our own
22 # Our own
23 if sys.platform == 'win32':
23 if sys.platform == 'win32':
24 from ._process_win32 import _find_cmd, system, getoutput, arg_split
24 from ._process_win32 import _find_cmd, system, getoutput, arg_split, check_pid
25 elif sys.platform == 'cli':
25 elif sys.platform == 'cli':
26 from ._process_cli import _find_cmd, system, getoutput, arg_split
26 from ._process_cli import _find_cmd, system, getoutput, arg_split
27 else:
27 else:
28 from ._process_posix import _find_cmd, system, getoutput, arg_split
28 from ._process_posix import _find_cmd, system, getoutput, arg_split, check_pid
29
29
30 from ._process_common import getoutputerror, get_output_error_code, process_handler
30 from ._process_common import getoutputerror, get_output_error_code, process_handler
31 from . import py3compat
31 from . import py3compat
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Code
34 # Code
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37
37
38 class FindCmdError(Exception):
38 class FindCmdError(Exception):
39 pass
39 pass
40
40
41
41
42 def find_cmd(cmd):
42 def find_cmd(cmd):
43 """Find absolute path to executable cmd in a cross platform manner.
43 """Find absolute path to executable cmd in a cross platform manner.
44
44
45 This function tries to determine the full path to a command line program
45 This function tries to determine the full path to a command line program
46 using `which` on Unix/Linux/OS X and `win32api` on Windows. Most of the
46 using `which` on Unix/Linux/OS X and `win32api` on Windows. Most of the
47 time it will use the version that is first on the users `PATH`.
47 time it will use the version that is first on the users `PATH`.
48
48
49 Warning, don't use this to find IPython command line programs as there
49 Warning, don't use this to find IPython command line programs as there
50 is a risk you will find the wrong one. Instead find those using the
50 is a risk you will find the wrong one. Instead find those using the
51 following code and looking for the application itself::
51 following code and looking for the application itself::
52
52
53 from IPython.utils.path import get_ipython_module_path
53 from IPython.utils.path import get_ipython_module_path
54 from IPython.utils.process import pycmd2argv
54 from IPython.utils.process import pycmd2argv
55 argv = pycmd2argv(get_ipython_module_path('IPython.terminal.ipapp'))
55 argv = pycmd2argv(get_ipython_module_path('IPython.terminal.ipapp'))
56
56
57 Parameters
57 Parameters
58 ----------
58 ----------
59 cmd : str
59 cmd : str
60 The command line program to look for.
60 The command line program to look for.
61 """
61 """
62 try:
62 try:
63 path = _find_cmd(cmd).rstrip()
63 path = _find_cmd(cmd).rstrip()
64 except OSError:
64 except OSError:
65 raise FindCmdError('command could not be found: %s' % cmd)
65 raise FindCmdError('command could not be found: %s' % cmd)
66 # which returns empty if not found
66 # which returns empty if not found
67 if path == '':
67 if path == '':
68 raise FindCmdError('command could not be found: %s' % cmd)
68 raise FindCmdError('command could not be found: %s' % cmd)
69 return os.path.abspath(path)
69 return os.path.abspath(path)
70
70
71
71
72 def is_cmd_found(cmd):
72 def is_cmd_found(cmd):
73 """Check whether executable `cmd` exists or not and return a bool."""
73 """Check whether executable `cmd` exists or not and return a bool."""
74 try:
74 try:
75 find_cmd(cmd)
75 find_cmd(cmd)
76 return True
76 return True
77 except FindCmdError:
77 except FindCmdError:
78 return False
78 return False
79
79
80
80
81 def pycmd2argv(cmd):
81 def pycmd2argv(cmd):
82 r"""Take the path of a python command and return a list (argv-style).
82 r"""Take the path of a python command and return a list (argv-style).
83
83
84 This only works on Python based command line programs and will find the
84 This only works on Python based command line programs and will find the
85 location of the ``python`` executable using ``sys.executable`` to make
85 location of the ``python`` executable using ``sys.executable`` to make
86 sure the right version is used.
86 sure the right version is used.
87
87
88 For a given path ``cmd``, this returns [cmd] if cmd's extension is .exe,
88 For a given path ``cmd``, this returns [cmd] if cmd's extension is .exe,
89 .com or .bat, and [, cmd] otherwise.
89 .com or .bat, and [, cmd] otherwise.
90
90
91 Parameters
91 Parameters
92 ----------
92 ----------
93 cmd : string
93 cmd : string
94 The path of the command.
94 The path of the command.
95
95
96 Returns
96 Returns
97 -------
97 -------
98 argv-style list.
98 argv-style list.
99 """
99 """
100 ext = os.path.splitext(cmd)[1]
100 ext = os.path.splitext(cmd)[1]
101 if ext in ['.exe', '.com', '.bat']:
101 if ext in ['.exe', '.com', '.bat']:
102 return [cmd]
102 return [cmd]
103 else:
103 else:
104 return [sys.executable, cmd]
104 return [sys.executable, cmd]
105
105
106
106
107 def abbrev_cwd():
107 def abbrev_cwd():
108 """ Return abbreviated version of cwd, e.g. d:mydir """
108 """ Return abbreviated version of cwd, e.g. d:mydir """
109 cwd = py3compat.getcwd().replace('\\','/')
109 cwd = py3compat.getcwd().replace('\\','/')
110 drivepart = ''
110 drivepart = ''
111 tail = cwd
111 tail = cwd
112 if sys.platform == 'win32':
112 if sys.platform == 'win32':
113 if len(cwd) < 4:
113 if len(cwd) < 4:
114 return cwd
114 return cwd
115 drivepart,tail = os.path.splitdrive(cwd)
115 drivepart,tail = os.path.splitdrive(cwd)
116
116
117
117
118 parts = tail.split('/')
118 parts = tail.split('/')
119 if len(parts) > 2:
119 if len(parts) > 2:
120 tail = '/'.join(parts[-2:])
120 tail = '/'.join(parts[-2:])
121
121
122 return (drivepart + (
122 return (drivepart + (
123 cwd == '/' and '/' or tail))
123 cwd == '/' and '/' or tail))
General Comments 0
You need to be logged in to leave comments. Login now