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