##// END OF EJS Templates
Backport PR #4098: pass profile-dir instead of profile name to Kernel...
Thomas Kluyver -
Show More
@@ -1,578 +1,582 b''
1 1 # encoding: utf-8
2 2 """
3 3 A base class for a configurable application.
4 4
5 5 Authors:
6 6
7 7 * Brian Granger
8 8 * Min RK
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2011 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 import logging
23 23 import os
24 24 import re
25 25 import sys
26 26 from copy import deepcopy
27 27 from collections import defaultdict
28 28
29 29 from IPython.external.decorator import decorator
30 30
31 31 from IPython.config.configurable import SingletonConfigurable
32 32 from IPython.config.loader import (
33 33 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound,
34 34 )
35 35
36 36 from IPython.utils.traitlets import (
37 37 Unicode, List, Enum, Dict, Instance, TraitError
38 38 )
39 39 from IPython.utils.importstring import import_item
40 40 from IPython.utils.text import indent, wrap_paragraphs, dedent
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # function for re-wrapping a helpstring
44 44 #-----------------------------------------------------------------------------
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Descriptions for the various sections
48 48 #-----------------------------------------------------------------------------
49 49
50 50 # merge flags&aliases into options
51 51 option_description = """
52 52 Arguments that take values are actually convenience aliases to full
53 53 Configurables, whose aliases are listed on the help line. For more information
54 54 on full configurables, see '--help-all'.
55 55 """.strip() # trim newlines of front and back
56 56
57 57 keyvalue_description = """
58 58 Parameters are set from command-line arguments of the form:
59 59 `--Class.trait=value`.
60 60 This line is evaluated in Python, so simple expressions are allowed, e.g.::
61 61 `--C.a='range(3)'` For setting C.a=[0,1,2].
62 62 """.strip() # trim newlines of front and back
63 63
64 64 # sys.argv can be missing, for example when python is embedded. See the docs
65 65 # for details: http://docs.python.org/2/c-api/intro.html#embedding-python
66 66 if not hasattr(sys, "argv"):
67 67 sys.argv = [""]
68 68
69 69 subcommand_description = """
70 70 Subcommands are launched as `{app} cmd [args]`. For information on using
71 71 subcommand 'cmd', do: `{app} cmd -h`.
72 72 """.strip().format(app=os.path.basename(sys.argv[0]))
73 73 # get running program name
74 74
75 75 #-----------------------------------------------------------------------------
76 76 # Application class
77 77 #-----------------------------------------------------------------------------
78 78
79 79 @decorator
80 80 def catch_config_error(method, app, *args, **kwargs):
81 81 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
82 82
83 83 On a TraitError (generally caused by bad config), this will print the trait's
84 84 message, and exit the app.
85 85
86 86 For use on init methods, to prevent invoking excepthook on invalid input.
87 87 """
88 88 try:
89 89 return method(app, *args, **kwargs)
90 90 except (TraitError, ArgumentError) as e:
91 91 app.print_help()
92 92 app.log.fatal("Bad config encountered during initialization:")
93 93 app.log.fatal(str(e))
94 94 app.log.debug("Config at the time: %s", app.config)
95 95 app.exit(1)
96 96
97 97
98 98 class ApplicationError(Exception):
99 99 pass
100 100
101 101 class LevelFormatter(logging.Formatter):
102 102 """Formatter with additional `highlevel` record
103 103
104 104 This field is empty if log level is less than highlevel_limit,
105 105 otherwise it is formatted with self.highlevel_format.
106 106
107 107 Useful for adding 'WARNING' to warning messages,
108 108 without adding 'INFO' to info, etc.
109 109 """
110 110 highlevel_limit = logging.WARN
111 111 highlevel_format = " %(levelname)s |"
112 112
113 113 def format(self, record):
114 114 if record.levelno >= self.highlevel_limit:
115 115 record.highlevel = self.highlevel_format % record.__dict__
116 116 else:
117 117 record.highlevel = ""
118 118 if sys.version_info[:2] > (2,6):
119 119 return super(LevelFormatter, self).format(record)
120 120 else:
121 121 return logging.Formatter.format(self, record)
122 122
123 123
124 124 class Application(SingletonConfigurable):
125 125 """A singleton application with full configuration support."""
126 126
127 127 # The name of the application, will usually match the name of the command
128 128 # line application
129 129 name = Unicode(u'application')
130 130
131 131 # The description of the application that is printed at the beginning
132 132 # of the help.
133 133 description = Unicode(u'This is an application.')
134 134 # default section descriptions
135 135 option_description = Unicode(option_description)
136 136 keyvalue_description = Unicode(keyvalue_description)
137 137 subcommand_description = Unicode(subcommand_description)
138 138
139 139 # The usage and example string that goes at the end of the help string.
140 140 examples = Unicode()
141 141
142 142 # A sequence of Configurable subclasses whose config=True attributes will
143 143 # be exposed at the command line.
144 144 classes = List([])
145 145
146 146 # The version string of this application.
147 147 version = Unicode(u'0.0')
148
149 # the argv used to initialize the application
150 argv = List(Unicode)
148 151
149 152 # The log level for the application
150 153 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
151 154 default_value=logging.WARN,
152 155 config=True,
153 156 help="Set the log level by value or name.")
154 157 def _log_level_changed(self, name, old, new):
155 158 """Adjust the log level when log_level is set."""
156 159 if isinstance(new, basestring):
157 160 new = getattr(logging, new)
158 161 self.log_level = new
159 162 self.log.setLevel(new)
160 163
161 164 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
162 165 help="The date format used by logging formatters for %(asctime)s"
163 166 )
164 167 def _log_datefmt_changed(self, name, old, new):
165 168 self._log_format_changed()
166 169
167 170 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
168 171 help="The Logging format template",
169 172 )
170 173 def _log_format_changed(self, name, old, new):
171 174 """Change the log formatter when log_format is set."""
172 175 _log_handler = self.log.handlers[0]
173 176 _log_formatter = LevelFormatter(new, datefmt=self.log_datefmt)
174 177 _log_handler.setFormatter(_log_formatter)
175 178
176 179 log = Instance(logging.Logger)
177 180 def _log_default(self):
178 181 """Start logging for this application.
179 182
180 183 The default is to log to stderr using a StreamHandler, if no default
181 184 handler already exists. The log level starts at logging.WARN, but this
182 185 can be adjusted by setting the ``log_level`` attribute.
183 186 """
184 187 log = logging.getLogger(self.__class__.__name__)
185 188 log.setLevel(self.log_level)
186 189 log.propagate = False
187 190 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
188 191 while _log:
189 192 if _log.handlers:
190 193 return log
191 194 if not _log.propagate:
192 195 break
193 196 else:
194 197 _log = _log.parent
195 198 if sys.executable.endswith('pythonw.exe'):
196 199 # this should really go to a file, but file-logging is only
197 200 # hooked up in parallel applications
198 201 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
199 202 else:
200 203 _log_handler = logging.StreamHandler()
201 204 _log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt)
202 205 _log_handler.setFormatter(_log_formatter)
203 206 log.addHandler(_log_handler)
204 207 return log
205 208
206 209 # the alias map for configurables
207 210 aliases = Dict({'log-level' : 'Application.log_level'})
208 211
209 212 # flags for loading Configurables or store_const style flags
210 213 # flags are loaded from this dict by '--key' flags
211 214 # this must be a dict of two-tuples, the first element being the Config/dict
212 215 # and the second being the help string for the flag
213 216 flags = Dict()
214 217 def _flags_changed(self, name, old, new):
215 218 """ensure flags dict is valid"""
216 219 for key,value in new.iteritems():
217 220 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
218 221 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
219 222 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
220 223
221 224
222 225 # subcommands for launching other applications
223 226 # if this is not empty, this will be a parent Application
224 227 # this must be a dict of two-tuples,
225 228 # the first element being the application class/import string
226 229 # and the second being the help string for the subcommand
227 230 subcommands = Dict()
228 231 # parse_command_line will initialize a subapp, if requested
229 232 subapp = Instance('IPython.config.application.Application', allow_none=True)
230 233
231 234 # extra command-line arguments that don't set config values
232 235 extra_args = List(Unicode)
233 236
234 237
235 238 def __init__(self, **kwargs):
236 239 SingletonConfigurable.__init__(self, **kwargs)
237 240 # Ensure my class is in self.classes, so my attributes appear in command line
238 241 # options and config files.
239 242 if self.__class__ not in self.classes:
240 243 self.classes.insert(0, self.__class__)
241 244
242 245 def _config_changed(self, name, old, new):
243 246 SingletonConfigurable._config_changed(self, name, old, new)
244 247 self.log.debug('Config changed:')
245 248 self.log.debug(repr(new))
246 249
247 250 @catch_config_error
248 251 def initialize(self, argv=None):
249 252 """Do the basic steps to configure me.
250 253
251 254 Override in subclasses.
252 255 """
253 256 self.parse_command_line(argv)
254 257
255 258
256 259 def start(self):
257 260 """Start the app mainloop.
258 261
259 262 Override in subclasses.
260 263 """
261 264 if self.subapp is not None:
262 265 return self.subapp.start()
263 266
264 267 def print_alias_help(self):
265 268 """Print the alias part of the help."""
266 269 if not self.aliases:
267 270 return
268 271
269 272 lines = []
270 273 classdict = {}
271 274 for cls in self.classes:
272 275 # include all parents (up to, but excluding Configurable) in available names
273 276 for c in cls.mro()[:-3]:
274 277 classdict[c.__name__] = c
275 278
276 279 for alias, longname in self.aliases.iteritems():
277 280 classname, traitname = longname.split('.',1)
278 281 cls = classdict[classname]
279 282
280 283 trait = cls.class_traits(config=True)[traitname]
281 284 help = cls.class_get_trait_help(trait).splitlines()
282 285 # reformat first line
283 286 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
284 287 if len(alias) == 1:
285 288 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
286 289 lines.extend(help)
287 290 # lines.append('')
288 291 print os.linesep.join(lines)
289 292
290 293 def print_flag_help(self):
291 294 """Print the flag part of the help."""
292 295 if not self.flags:
293 296 return
294 297
295 298 lines = []
296 299 for m, (cfg,help) in self.flags.iteritems():
297 300 prefix = '--' if len(m) > 1 else '-'
298 301 lines.append(prefix+m)
299 302 lines.append(indent(dedent(help.strip())))
300 303 # lines.append('')
301 304 print os.linesep.join(lines)
302 305
303 306 def print_options(self):
304 307 if not self.flags and not self.aliases:
305 308 return
306 309 lines = ['Options']
307 310 lines.append('-'*len(lines[0]))
308 311 lines.append('')
309 312 for p in wrap_paragraphs(self.option_description):
310 313 lines.append(p)
311 314 lines.append('')
312 315 print os.linesep.join(lines)
313 316 self.print_flag_help()
314 317 self.print_alias_help()
315 318 print
316 319
317 320 def print_subcommands(self):
318 321 """Print the subcommand part of the help."""
319 322 if not self.subcommands:
320 323 return
321 324
322 325 lines = ["Subcommands"]
323 326 lines.append('-'*len(lines[0]))
324 327 lines.append('')
325 328 for p in wrap_paragraphs(self.subcommand_description):
326 329 lines.append(p)
327 330 lines.append('')
328 331 for subc, (cls, help) in self.subcommands.iteritems():
329 332 lines.append(subc)
330 333 if help:
331 334 lines.append(indent(dedent(help.strip())))
332 335 lines.append('')
333 336 print os.linesep.join(lines)
334 337
335 338 def print_help(self, classes=False):
336 339 """Print the help for each Configurable class in self.classes.
337 340
338 341 If classes=False (the default), only flags and aliases are printed.
339 342 """
340 343 self.print_description()
341 344 self.print_subcommands()
342 345 self.print_options()
343 346
344 347 if classes:
345 348 if self.classes:
346 349 print "Class parameters"
347 350 print "----------------"
348 351 print
349 352 for p in wrap_paragraphs(self.keyvalue_description):
350 353 print p
351 354 print
352 355
353 356 for cls in self.classes:
354 357 cls.class_print_help()
355 358 print
356 359 else:
357 360 print "To see all available configurables, use `--help-all`"
358 361 print
359 362
360 363 self.print_examples()
361 364
362 365
363 366 def print_description(self):
364 367 """Print the application description."""
365 368 for p in wrap_paragraphs(self.description):
366 369 print p
367 370 print
368 371
369 372 def print_examples(self):
370 373 """Print usage and examples.
371 374
372 375 This usage string goes at the end of the command line help string
373 376 and should contain examples of the application's usage.
374 377 """
375 378 if self.examples:
376 379 print "Examples"
377 380 print "--------"
378 381 print
379 382 print indent(dedent(self.examples.strip()))
380 383 print
381 384
382 385 def print_version(self):
383 386 """Print the version string."""
384 387 print self.version
385 388
386 389 def update_config(self, config):
387 390 """Fire the traits events when the config is updated."""
388 391 # Save a copy of the current config.
389 392 newconfig = deepcopy(self.config)
390 393 # Merge the new config into the current one.
391 394 newconfig.merge(config)
392 395 # Save the combined config as self.config, which triggers the traits
393 396 # events.
394 397 self.config = newconfig
395 398
396 399 @catch_config_error
397 400 def initialize_subcommand(self, subc, argv=None):
398 401 """Initialize a subcommand with argv."""
399 402 subapp,help = self.subcommands.get(subc)
400 403
401 404 if isinstance(subapp, basestring):
402 405 subapp = import_item(subapp)
403 406
404 407 # clear existing instances
405 408 self.__class__.clear_instance()
406 409 # instantiate
407 410 self.subapp = subapp.instance(config=self.config)
408 411 # and initialize subapp
409 412 self.subapp.initialize(argv)
410 413
411 414 def flatten_flags(self):
412 415 """flatten flags and aliases, so cl-args override as expected.
413 416
414 417 This prevents issues such as an alias pointing to InteractiveShell,
415 418 but a config file setting the same trait in TerminalInteraciveShell
416 419 getting inappropriate priority over the command-line arg.
417 420
418 421 Only aliases with exactly one descendent in the class list
419 422 will be promoted.
420 423
421 424 """
422 425 # build a tree of classes in our list that inherit from a particular
423 426 # it will be a dict by parent classname of classes in our list
424 427 # that are descendents
425 428 mro_tree = defaultdict(list)
426 429 for cls in self.classes:
427 430 clsname = cls.__name__
428 431 for parent in cls.mro()[1:-3]:
429 432 # exclude cls itself and Configurable,HasTraits,object
430 433 mro_tree[parent.__name__].append(clsname)
431 434 # flatten aliases, which have the form:
432 435 # { 'alias' : 'Class.trait' }
433 436 aliases = {}
434 437 for alias, cls_trait in self.aliases.iteritems():
435 438 cls,trait = cls_trait.split('.',1)
436 439 children = mro_tree[cls]
437 440 if len(children) == 1:
438 441 # exactly one descendent, promote alias
439 442 cls = children[0]
440 443 aliases[alias] = '.'.join([cls,trait])
441 444
442 445 # flatten flags, which are of the form:
443 446 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
444 447 flags = {}
445 448 for key, (flagdict, help) in self.flags.iteritems():
446 449 newflag = {}
447 450 for cls, subdict in flagdict.iteritems():
448 451 children = mro_tree[cls]
449 452 # exactly one descendent, promote flag section
450 453 if len(children) == 1:
451 454 cls = children[0]
452 455 newflag[cls] = subdict
453 456 flags[key] = (newflag, help)
454 457 return flags, aliases
455 458
456 459 @catch_config_error
457 460 def parse_command_line(self, argv=None):
458 461 """Parse the command line arguments."""
459 462 argv = sys.argv[1:] if argv is None else argv
463 self.argv = list(argv)
460 464
461 465 if argv and argv[0] == 'help':
462 466 # turn `ipython help notebook` into `ipython notebook -h`
463 467 argv = argv[1:] + ['-h']
464 468
465 469 if self.subcommands and len(argv) > 0:
466 470 # we have subcommands, and one may have been specified
467 471 subc, subargv = argv[0], argv[1:]
468 472 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
469 473 # it's a subcommand, and *not* a flag or class parameter
470 474 return self.initialize_subcommand(subc, subargv)
471 475
472 476 # Arguments after a '--' argument are for the script IPython may be
473 477 # about to run, not IPython iteslf. For arguments parsed here (help and
474 478 # version), we want to only search the arguments up to the first
475 479 # occurrence of '--', which we're calling interpreted_argv.
476 480 try:
477 481 interpreted_argv = argv[:argv.index('--')]
478 482 except ValueError:
479 483 interpreted_argv = argv
480 484
481 485 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
482 486 self.print_help('--help-all' in interpreted_argv)
483 487 self.exit(0)
484 488
485 489 if '--version' in interpreted_argv or '-V' in interpreted_argv:
486 490 self.print_version()
487 491 self.exit(0)
488 492
489 493 # flatten flags&aliases, so cl-args get appropriate priority:
490 494 flags,aliases = self.flatten_flags()
491 495
492 496 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
493 497 flags=flags)
494 498 config = loader.load_config()
495 499 self.update_config(config)
496 500 # store unparsed args in extra_args
497 501 self.extra_args = loader.extra_args
498 502
499 503 @catch_config_error
500 504 def load_config_file(self, filename, path=None):
501 505 """Load a .py based config file by filename and path."""
502 506 loader = PyFileConfigLoader(filename, path=path)
503 507 try:
504 508 config = loader.load_config()
505 509 except ConfigFileNotFound:
506 510 # problem finding the file, raise
507 511 raise
508 512 except Exception:
509 513 # try to get the full filename, but it will be empty in the
510 514 # unlikely event that the error raised before filefind finished
511 515 filename = loader.full_filename or filename
512 516 # problem while running the file
513 517 self.log.error("Exception while loading config file %s",
514 518 filename, exc_info=True)
515 519 else:
516 520 self.log.debug("Loaded config file: %s", loader.full_filename)
517 521 self.update_config(config)
518 522
519 523 def generate_config_file(self):
520 524 """generate default config file from Configurables"""
521 525 lines = ["# Configuration file for %s."%self.name]
522 526 lines.append('')
523 527 lines.append('c = get_config()')
524 528 lines.append('')
525 529 for cls in self.classes:
526 530 lines.append(cls.class_config_section())
527 531 return '\n'.join(lines)
528 532
529 533 def exit(self, exit_status=0):
530 534 self.log.debug("Exiting application: %s" % self.name)
531 535 sys.exit(exit_status)
532 536
533 537 @classmethod
534 538 def launch_instance(cls, argv=None, **kwargs):
535 539 """Launch a global instance of this Application
536 540
537 541 If a global instance already exists, this reinitializes and starts it
538 542 """
539 543 app = cls.instance(**kwargs)
540 544 app.initialize(argv)
541 545 app.start()
542 546
543 547 #-----------------------------------------------------------------------------
544 548 # utility functions, for convenience
545 549 #-----------------------------------------------------------------------------
546 550
547 551 def boolean_flag(name, configurable, set_help='', unset_help=''):
548 552 """Helper for building basic --trait, --no-trait flags.
549 553
550 554 Parameters
551 555 ----------
552 556
553 557 name : str
554 558 The name of the flag.
555 559 configurable : str
556 560 The 'Class.trait' string of the trait to be set/unset with the flag
557 561 set_help : unicode
558 562 help string for --name flag
559 563 unset_help : unicode
560 564 help string for --no-name flag
561 565
562 566 Returns
563 567 -------
564 568
565 569 cfg : dict
566 570 A dict with two keys: 'name', and 'no-name', for setting and unsetting
567 571 the trait, respectively.
568 572 """
569 573 # default helpstrings
570 574 set_help = set_help or "set %s=True"%configurable
571 575 unset_help = unset_help or "set %s=False"%configurable
572 576
573 577 cls,trait = configurable.split('.')
574 578
575 579 setter = {cls : {trait : True}}
576 580 unsetter = {cls : {trait : False}}
577 581 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
578 582
@@ -1,372 +1,373 b''
1 1 # encoding: utf-8
2 2 """
3 3 An application for IPython.
4 4
5 5 All top-level applications should use the classes in this module for
6 6 handling configuration and creating componenets.
7 7
8 8 The job of an :class:`Application` is to create the master configuration
9 9 object and then create the configurable objects, passing the config to them.
10 10
11 11 Authors:
12 12
13 13 * Brian Granger
14 14 * Fernando Perez
15 15 * Min RK
16 16
17 17 """
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Copyright (C) 2008-2011 The IPython Development Team
21 21 #
22 22 # Distributed under the terms of the BSD License. The full license is in
23 23 # the file COPYING, distributed as part of this software.
24 24 #-----------------------------------------------------------------------------
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Imports
28 28 #-----------------------------------------------------------------------------
29 29
30 30 import atexit
31 31 import glob
32 32 import logging
33 33 import os
34 34 import shutil
35 35 import sys
36 36
37 37 from IPython.config.application import Application, catch_config_error
38 38 from IPython.config.loader import ConfigFileNotFound
39 39 from IPython.core import release, crashhandler
40 40 from IPython.core.profiledir import ProfileDir, ProfileDirError
41 41 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
42 42 from IPython.utils.traitlets import List, Unicode, Type, Bool, Dict, Set, Instance
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # Classes and functions
46 46 #-----------------------------------------------------------------------------
47 47
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Base Application Class
51 51 #-----------------------------------------------------------------------------
52 52
53 53 # aliases and flags
54 54
55 55 base_aliases = {
56 'profile-dir' : 'ProfileDir.location',
56 57 'profile' : 'BaseIPythonApplication.profile',
57 58 'ipython-dir' : 'BaseIPythonApplication.ipython_dir',
58 59 'log-level' : 'Application.log_level',
59 60 'config' : 'BaseIPythonApplication.extra_config_file',
60 61 }
61 62
62 63 base_flags = dict(
63 64 debug = ({'Application' : {'log_level' : logging.DEBUG}},
64 65 "set log level to logging.DEBUG (maximize logging output)"),
65 66 quiet = ({'Application' : {'log_level' : logging.CRITICAL}},
66 67 "set log level to logging.CRITICAL (minimize logging output)"),
67 68 init = ({'BaseIPythonApplication' : {
68 69 'copy_config_files' : True,
69 70 'auto_create' : True}
70 71 }, """Initialize profile with default config files. This is equivalent
71 72 to running `ipython profile create <profile>` prior to startup.
72 73 """)
73 74 )
74 75
75 76
76 77 class BaseIPythonApplication(Application):
77 78
78 79 name = Unicode(u'ipython')
79 80 description = Unicode(u'IPython: an enhanced interactive Python shell.')
80 81 version = Unicode(release.version)
81 82
82 83 aliases = Dict(base_aliases)
83 84 flags = Dict(base_flags)
84 85 classes = List([ProfileDir])
85 86
86 87 # Track whether the config_file has changed,
87 88 # because some logic happens only if we aren't using the default.
88 89 config_file_specified = Set()
89 90
90 91 config_file_name = Unicode()
91 92 def _config_file_name_default(self):
92 93 return self.name.replace('-','_') + u'_config.py'
93 94 def _config_file_name_changed(self, name, old, new):
94 95 if new != old:
95 96 self.config_file_specified.add(new)
96 97
97 98 # The directory that contains IPython's builtin profiles.
98 99 builtin_profile_dir = Unicode(
99 100 os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
100 101 )
101 102
102 103 config_file_paths = List(Unicode)
103 104 def _config_file_paths_default(self):
104 105 return [os.getcwdu()]
105 106
106 107 extra_config_file = Unicode(config=True,
107 108 help="""Path to an extra config file to load.
108 109
109 110 If specified, load this config file in addition to any other IPython config.
110 111 """)
111 112 def _extra_config_file_changed(self, name, old, new):
112 113 try:
113 114 self.config_files.remove(old)
114 115 except ValueError:
115 116 pass
116 117 self.config_file_specified.add(new)
117 118 self.config_files.append(new)
118 119
119 120 profile = Unicode(u'default', config=True,
120 121 help="""The IPython profile to use."""
121 122 )
122 123
123 124 def _profile_changed(self, name, old, new):
124 125 self.builtin_profile_dir = os.path.join(
125 126 get_ipython_package_dir(), u'config', u'profile', new
126 127 )
127 128
128 129 ipython_dir = Unicode(get_ipython_dir(), config=True,
129 130 help="""
130 131 The name of the IPython directory. This directory is used for logging
131 132 configuration (through profiles), history storage, etc. The default
132 133 is usually $HOME/.ipython. This options can also be specified through
133 134 the environment variable IPYTHONDIR.
134 135 """
135 136 )
136 137 _in_init_profile_dir = False
137 138 profile_dir = Instance(ProfileDir)
138 139 def _profile_dir_default(self):
139 140 # avoid recursion
140 141 if self._in_init_profile_dir:
141 142 return
142 143 # profile_dir requested early, force initialization
143 144 self.init_profile_dir()
144 145 return self.profile_dir
145 146
146 147 overwrite = Bool(False, config=True,
147 148 help="""Whether to overwrite existing config files when copying""")
148 149 auto_create = Bool(False, config=True,
149 150 help="""Whether to create profile dir if it doesn't exist""")
150 151
151 152 config_files = List(Unicode)
152 153 def _config_files_default(self):
153 154 return [self.config_file_name]
154 155
155 156 copy_config_files = Bool(False, config=True,
156 157 help="""Whether to install the default config files into the profile dir.
157 158 If a new profile is being created, and IPython contains config files for that
158 159 profile, then they will be staged into the new directory. Otherwise,
159 160 default config files will be automatically generated.
160 161 """)
161 162
162 163 verbose_crash = Bool(False, config=True,
163 164 help="""Create a massive crash report when IPython encounters what may be an
164 165 internal error. The default is to append a short message to the
165 166 usual traceback""")
166 167
167 168 # The class to use as the crash handler.
168 169 crash_handler_class = Type(crashhandler.CrashHandler)
169 170
170 171 @catch_config_error
171 172 def __init__(self, **kwargs):
172 173 super(BaseIPythonApplication, self).__init__(**kwargs)
173 174 # ensure current working directory exists
174 175 try:
175 176 directory = os.getcwdu()
176 177 except:
177 178 # raise exception
178 179 self.log.error("Current working directory doesn't exist.")
179 180 raise
180 181
181 182 # ensure even default IPYTHONDIR exists
182 183 if not os.path.exists(self.ipython_dir):
183 184 self._ipython_dir_changed('ipython_dir', self.ipython_dir, self.ipython_dir)
184 185
185 186 #-------------------------------------------------------------------------
186 187 # Various stages of Application creation
187 188 #-------------------------------------------------------------------------
188 189
189 190 def init_crash_handler(self):
190 191 """Create a crash handler, typically setting sys.excepthook to it."""
191 192 self.crash_handler = self.crash_handler_class(self)
192 193 sys.excepthook = self.excepthook
193 194 def unset_crashhandler():
194 195 sys.excepthook = sys.__excepthook__
195 196 atexit.register(unset_crashhandler)
196 197
197 198 def excepthook(self, etype, evalue, tb):
198 199 """this is sys.excepthook after init_crashhandler
199 200
200 201 set self.verbose_crash=True to use our full crashhandler, instead of
201 202 a regular traceback with a short message (crash_handler_lite)
202 203 """
203 204
204 205 if self.verbose_crash:
205 206 return self.crash_handler(etype, evalue, tb)
206 207 else:
207 208 return crashhandler.crash_handler_lite(etype, evalue, tb)
208 209
209 210 def _ipython_dir_changed(self, name, old, new):
210 211 if old in sys.path:
211 212 sys.path.remove(old)
212 213 sys.path.append(os.path.abspath(new))
213 214 if not os.path.isdir(new):
214 215 os.makedirs(new, mode=0o777)
215 216 readme = os.path.join(new, 'README')
216 217 if not os.path.exists(readme):
217 218 path = os.path.join(get_ipython_package_dir(), u'config', u'profile')
218 219 shutil.copy(os.path.join(path, 'README'), readme)
219 220 self.log.debug("IPYTHONDIR set to: %s" % new)
220 221
221 222 def load_config_file(self, suppress_errors=True):
222 223 """Load the config file.
223 224
224 225 By default, errors in loading config are handled, and a warning
225 226 printed on screen. For testing, the suppress_errors option is set
226 227 to False, so errors will make tests fail.
227 228 """
228 229 self.log.debug("Searching path %s for config files", self.config_file_paths)
229 230 base_config = 'ipython_config.py'
230 231 self.log.debug("Attempting to load config file: %s" %
231 232 base_config)
232 233 try:
233 234 Application.load_config_file(
234 235 self,
235 236 base_config,
236 237 path=self.config_file_paths
237 238 )
238 239 except ConfigFileNotFound:
239 240 # ignore errors loading parent
240 241 self.log.debug("Config file %s not found", base_config)
241 242 pass
242 243
243 244 for config_file_name in self.config_files:
244 245 if not config_file_name or config_file_name == base_config:
245 246 continue
246 247 self.log.debug("Attempting to load config file: %s" %
247 248 self.config_file_name)
248 249 try:
249 250 Application.load_config_file(
250 251 self,
251 252 config_file_name,
252 253 path=self.config_file_paths
253 254 )
254 255 except ConfigFileNotFound:
255 256 # Only warn if the default config file was NOT being used.
256 257 if config_file_name in self.config_file_specified:
257 258 msg = self.log.warn
258 259 else:
259 260 msg = self.log.debug
260 261 msg("Config file not found, skipping: %s", config_file_name)
261 262 except:
262 263 # For testing purposes.
263 264 if not suppress_errors:
264 265 raise
265 266 self.log.warn("Error loading config file: %s" %
266 267 self.config_file_name, exc_info=True)
267 268
268 269 def init_profile_dir(self):
269 270 """initialize the profile dir"""
270 271 self._in_init_profile_dir = True
271 272 if self.profile_dir is not None:
272 273 # already ran
273 274 return
274 275 try:
275 276 # location explicitly specified:
276 277 location = self.config.ProfileDir.location
277 278 except AttributeError:
278 279 # location not specified, find by profile name
279 280 try:
280 281 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
281 282 except ProfileDirError:
282 283 # not found, maybe create it (always create default profile)
283 284 if self.auto_create or self.profile == 'default':
284 285 try:
285 286 p = ProfileDir.create_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
286 287 except ProfileDirError:
287 288 self.log.fatal("Could not create profile: %r"%self.profile)
288 289 self.exit(1)
289 290 else:
290 291 self.log.info("Created profile dir: %r"%p.location)
291 292 else:
292 293 self.log.fatal("Profile %r not found."%self.profile)
293 294 self.exit(1)
294 295 else:
295 296 self.log.info("Using existing profile dir: %r"%p.location)
296 297 else:
297 298 # location is fully specified
298 299 try:
299 300 p = ProfileDir.find_profile_dir(location, self.config)
300 301 except ProfileDirError:
301 302 # not found, maybe create it
302 303 if self.auto_create:
303 304 try:
304 305 p = ProfileDir.create_profile_dir(location, self.config)
305 306 except ProfileDirError:
306 307 self.log.fatal("Could not create profile directory: %r"%location)
307 308 self.exit(1)
308 309 else:
309 310 self.log.info("Creating new profile dir: %r"%location)
310 311 else:
311 312 self.log.fatal("Profile directory %r not found."%location)
312 313 self.exit(1)
313 314 else:
314 315 self.log.info("Using existing profile dir: %r"%location)
315 316
316 317 self.profile_dir = p
317 318 self.config_file_paths.append(p.location)
318 319 self._in_init_profile_dir = False
319 320
320 321 def init_config_files(self):
321 322 """[optionally] copy default config files into profile dir."""
322 323 # copy config files
323 324 path = self.builtin_profile_dir
324 325 if self.copy_config_files:
325 326 src = self.profile
326 327
327 328 cfg = self.config_file_name
328 329 if path and os.path.exists(os.path.join(path, cfg)):
329 330 self.log.warn("Staging %r from %s into %r [overwrite=%s]"%(
330 331 cfg, src, self.profile_dir.location, self.overwrite)
331 332 )
332 333 self.profile_dir.copy_config_file(cfg, path=path, overwrite=self.overwrite)
333 334 else:
334 335 self.stage_default_config_file()
335 336 else:
336 337 # Still stage *bundled* config files, but not generated ones
337 338 # This is necessary for `ipython profile=sympy` to load the profile
338 339 # on the first go
339 340 files = glob.glob(os.path.join(path, '*.py'))
340 341 for fullpath in files:
341 342 cfg = os.path.basename(fullpath)
342 343 if self.profile_dir.copy_config_file(cfg, path=path, overwrite=False):
343 344 # file was copied
344 345 self.log.warn("Staging bundled %s from %s into %r"%(
345 346 cfg, self.profile, self.profile_dir.location)
346 347 )
347 348
348 349
349 350 def stage_default_config_file(self):
350 351 """auto generate default config file, and stage it into the profile."""
351 352 s = self.generate_config_file()
352 353 fname = os.path.join(self.profile_dir.location, self.config_file_name)
353 354 if self.overwrite or not os.path.exists(fname):
354 355 self.log.warn("Generating default config file: %r"%(fname))
355 356 with open(fname, 'w') as f:
356 357 f.write(s)
357 358
358 359 @catch_config_error
359 360 def initialize(self, argv=None):
360 361 # don't hook up crash handler before parsing command-line
361 362 self.parse_command_line(argv)
362 363 self.init_crash_handler()
363 364 if self.subapp is not None:
364 365 # stop here if subapp is taking over
365 366 return
366 367 cl_config = self.config
367 368 self.init_profile_dir()
368 369 self.init_config_files()
369 370 self.load_config_file()
370 371 # enforce cl-opts override configfile opts:
371 372 self.update_config(cl_config)
372 373
@@ -1,723 +1,726 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import select
25 25 import signal
26 26 import socket
27 27 import sys
28 28 import threading
29 29 import time
30 30 import webbrowser
31 31
32 32
33 33 # Third party
34 34 # check for pyzmq 2.1.11
35 35 from IPython.utils.zmqrelated import check_for_zmq
36 36 check_for_zmq('2.1.11', 'IPython.html')
37 37
38 38 from jinja2 import Environment, FileSystemLoader
39 39
40 40 # Install the pyzmq ioloop. This has to be done before anything else from
41 41 # tornado is imported.
42 42 from zmq.eventloop import ioloop
43 43 ioloop.install()
44 44
45 45 # check for tornado 2.1.0
46 46 msg = "The IPython Notebook requires tornado >= 2.1.0"
47 47 try:
48 48 import tornado
49 49 except ImportError:
50 50 raise ImportError(msg)
51 51 try:
52 52 version_info = tornado.version_info
53 53 except AttributeError:
54 54 raise ImportError(msg + ", but you have < 1.1.0")
55 55 if version_info < (2,1,0):
56 56 raise ImportError(msg + ", but you have %s" % tornado.version)
57 57
58 58 from tornado import httpserver
59 59 from tornado import web
60 60
61 61 # Our own libraries
62 62 from IPython.html import DEFAULT_STATIC_FILES_PATH
63 63
64 64 from .services.kernels.kernelmanager import MappingKernelManager
65 65 from .services.notebooks.nbmanager import NotebookManager
66 66 from .services.notebooks.filenbmanager import FileNotebookManager
67 67 from .services.clusters.clustermanager import ClusterManager
68 68
69 69 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
70 70
71 71 from IPython.config.application import catch_config_error, boolean_flag
72 72 from IPython.core.application import BaseIPythonApplication
73 73 from IPython.consoleapp import IPythonConsoleApp
74 74 from IPython.kernel import swallow_argv
75 75 from IPython.kernel.zmq.session import default_secure
76 76 from IPython.kernel.zmq.kernelapp import (
77 77 kernel_flags,
78 78 kernel_aliases,
79 79 )
80 80 from IPython.utils.importstring import import_item
81 81 from IPython.utils.localinterfaces import LOCALHOST
82 82 from IPython.utils import submodule
83 83 from IPython.utils.traitlets import (
84 84 Dict, Unicode, Integer, List, Bool, Bytes,
85 85 DottedObjectName
86 86 )
87 87 from IPython.utils import py3compat
88 88 from IPython.utils.path import filefind
89 89
90 90 from .utils import url_path_join
91 91
92 92 #-----------------------------------------------------------------------------
93 93 # Module globals
94 94 #-----------------------------------------------------------------------------
95 95
96 96 _examples = """
97 97 ipython notebook # start the notebook
98 98 ipython notebook --profile=sympy # use the sympy profile
99 99 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
100 100 """
101 101
102 102 #-----------------------------------------------------------------------------
103 103 # Helper functions
104 104 #-----------------------------------------------------------------------------
105 105
106 106 def random_ports(port, n):
107 107 """Generate a list of n random ports near the given port.
108 108
109 109 The first 5 ports will be sequential, and the remaining n-5 will be
110 110 randomly selected in the range [port-2*n, port+2*n].
111 111 """
112 112 for i in range(min(5, n)):
113 113 yield port + i
114 114 for i in range(n-5):
115 115 yield port + random.randint(-2*n, 2*n)
116 116
117 117 def load_handlers(name):
118 118 """Load the (URL pattern, handler) tuples for each component."""
119 119 name = 'IPython.html.' + name
120 120 mod = __import__(name, fromlist=['default_handlers'])
121 121 return mod.default_handlers
122 122
123 123 #-----------------------------------------------------------------------------
124 124 # The Tornado web application
125 125 #-----------------------------------------------------------------------------
126 126
127 127 class NotebookWebApplication(web.Application):
128 128
129 129 def __init__(self, ipython_app, kernel_manager, notebook_manager,
130 130 cluster_manager, log,
131 131 base_project_url, settings_overrides):
132 132
133 133 settings = self.init_settings(
134 134 ipython_app, kernel_manager, notebook_manager, cluster_manager,
135 135 log, base_project_url, settings_overrides)
136 136 handlers = self.init_handlers(settings)
137 137
138 138 super(NotebookWebApplication, self).__init__(handlers, **settings)
139 139
140 140 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
141 141 cluster_manager, log,
142 142 base_project_url, settings_overrides):
143 143 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
144 144 # base_project_url will always be unicode, which will in turn
145 145 # make the patterns unicode, and ultimately result in unicode
146 146 # keys in kwargs to handler._execute(**kwargs) in tornado.
147 147 # This enforces that base_project_url be ascii in that situation.
148 148 #
149 149 # Note that the URLs these patterns check against are escaped,
150 150 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
151 151 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
152 152 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
153 153 settings = dict(
154 154 # basics
155 155 base_project_url=base_project_url,
156 156 base_kernel_url=ipython_app.base_kernel_url,
157 157 template_path=template_path,
158 158 static_path=ipython_app.static_file_path,
159 159 static_handler_class = FileFindHandler,
160 160 static_url_prefix = url_path_join(base_project_url,'/static/'),
161 161
162 162 # authentication
163 163 cookie_secret=ipython_app.cookie_secret,
164 164 login_url=url_path_join(base_project_url,'/login'),
165 165 password=ipython_app.password,
166 166
167 167 # managers
168 168 kernel_manager=kernel_manager,
169 169 notebook_manager=notebook_manager,
170 170 cluster_manager=cluster_manager,
171 171
172 172 # IPython stuff
173 173 mathjax_url=ipython_app.mathjax_url,
174 174 config=ipython_app.config,
175 175 use_less=ipython_app.use_less,
176 176 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
177 177 )
178 178
179 179 # allow custom overrides for the tornado web app.
180 180 settings.update(settings_overrides)
181 181 return settings
182 182
183 183 def init_handlers(self, settings):
184 184 # Load the (URL pattern, handler) tuples for each component.
185 185 handlers = []
186 186 handlers.extend(load_handlers('base.handlers'))
187 187 handlers.extend(load_handlers('tree.handlers'))
188 188 handlers.extend(load_handlers('auth.login'))
189 189 handlers.extend(load_handlers('auth.logout'))
190 190 handlers.extend(load_handlers('notebook.handlers'))
191 191 handlers.extend(load_handlers('services.kernels.handlers'))
192 192 handlers.extend(load_handlers('services.notebooks.handlers'))
193 193 handlers.extend(load_handlers('services.clusters.handlers'))
194 194 handlers.extend([
195 195 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
196 196 ])
197 197 # prepend base_project_url onto the patterns that we match
198 198 new_handlers = []
199 199 for handler in handlers:
200 200 pattern = url_path_join(settings['base_project_url'], handler[0])
201 201 new_handler = tuple([pattern] + list(handler[1:]))
202 202 new_handlers.append(new_handler)
203 203 return new_handlers
204 204
205 205
206 206
207 207 #-----------------------------------------------------------------------------
208 208 # Aliases and Flags
209 209 #-----------------------------------------------------------------------------
210 210
211 211 flags = dict(kernel_flags)
212 212 flags['no-browser']=(
213 213 {'NotebookApp' : {'open_browser' : False}},
214 214 "Don't open the notebook in a browser after startup."
215 215 )
216 216 flags['no-mathjax']=(
217 217 {'NotebookApp' : {'enable_mathjax' : False}},
218 218 """Disable MathJax
219 219
220 220 MathJax is the javascript library IPython uses to render math/LaTeX. It is
221 221 very large, so you may want to disable it if you have a slow internet
222 222 connection, or for offline use of the notebook.
223 223
224 224 When disabled, equations etc. will appear as their untransformed TeX source.
225 225 """
226 226 )
227 227
228 228 # Add notebook manager flags
229 229 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
230 230 'Auto-save a .py script everytime the .ipynb notebook is saved',
231 231 'Do not auto-save .py scripts for every notebook'))
232 232
233 233 # the flags that are specific to the frontend
234 234 # these must be scrubbed before being passed to the kernel,
235 235 # or it will raise an error on unrecognized flags
236 236 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
237 237
238 238 aliases = dict(kernel_aliases)
239 239
240 240 aliases.update({
241 241 'ip': 'NotebookApp.ip',
242 242 'port': 'NotebookApp.port',
243 243 'port-retries': 'NotebookApp.port_retries',
244 244 'transport': 'KernelManager.transport',
245 245 'keyfile': 'NotebookApp.keyfile',
246 246 'certfile': 'NotebookApp.certfile',
247 247 'notebook-dir': 'NotebookManager.notebook_dir',
248 248 'browser': 'NotebookApp.browser',
249 249 })
250 250
251 251 # remove ipkernel flags that are singletons, and don't make sense in
252 252 # multi-kernel evironment:
253 253 aliases.pop('f', None)
254 254
255 255 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
256 u'notebook-dir']
256 u'notebook-dir', u'profile', u'profile-dir']
257 257
258 258 #-----------------------------------------------------------------------------
259 259 # NotebookApp
260 260 #-----------------------------------------------------------------------------
261 261
262 262 class NotebookApp(BaseIPythonApplication):
263 263
264 264 name = 'ipython-notebook'
265 265
266 266 description = """
267 267 The IPython HTML Notebook.
268 268
269 269 This launches a Tornado based HTML Notebook Server that serves up an
270 270 HTML5/Javascript Notebook client.
271 271 """
272 272 examples = _examples
273 273
274 274 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
275 275 FileNotebookManager]
276 276 flags = Dict(flags)
277 277 aliases = Dict(aliases)
278 278
279 279 kernel_argv = List(Unicode)
280 280
281 281 def _log_level_default(self):
282 282 return logging.INFO
283 283
284 284 def _log_format_default(self):
285 285 """override default log format to include time"""
286 286 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
287 287
288 288 # create requested profiles by default, if they don't exist:
289 289 auto_create = Bool(True)
290 290
291 291 # file to be opened in the notebook server
292 292 file_to_run = Unicode('')
293 293
294 294 # Network related information.
295 295
296 296 ip = Unicode(LOCALHOST, config=True,
297 297 help="The IP address the notebook server will listen on."
298 298 )
299 299
300 300 def _ip_changed(self, name, old, new):
301 301 if new == u'*': self.ip = u''
302 302
303 303 port = Integer(8888, config=True,
304 304 help="The port the notebook server will listen on."
305 305 )
306 306 port_retries = Integer(50, config=True,
307 307 help="The number of additional ports to try if the specified port is not available."
308 308 )
309 309
310 310 certfile = Unicode(u'', config=True,
311 311 help="""The full path to an SSL/TLS certificate file."""
312 312 )
313 313
314 314 keyfile = Unicode(u'', config=True,
315 315 help="""The full path to a private key file for usage with SSL/TLS."""
316 316 )
317 317
318 318 cookie_secret = Bytes(b'', config=True,
319 319 help="""The random bytes used to secure cookies.
320 320 By default this is a new random number every time you start the Notebook.
321 321 Set it to a value in a config file to enable logins to persist across server sessions.
322 322
323 323 Note: Cookie secrets should be kept private, do not share config files with
324 324 cookie_secret stored in plaintext (you can read the value from a file).
325 325 """
326 326 )
327 327 def _cookie_secret_default(self):
328 328 return os.urandom(1024)
329 329
330 330 password = Unicode(u'', config=True,
331 331 help="""Hashed password to use for web authentication.
332 332
333 333 To generate, type in a python/IPython shell:
334 334
335 335 from IPython.lib import passwd; passwd()
336 336
337 337 The string should be of the form type:salt:hashed-password.
338 338 """
339 339 )
340 340
341 341 open_browser = Bool(True, config=True,
342 342 help="""Whether to open in a browser after starting.
343 343 The specific browser used is platform dependent and
344 344 determined by the python standard library `webbrowser`
345 345 module, unless it is overridden using the --browser
346 346 (NotebookApp.browser) configuration option.
347 347 """)
348 348
349 349 browser = Unicode(u'', config=True,
350 350 help="""Specify what command to use to invoke a web
351 351 browser when opening the notebook. If not specified, the
352 352 default browser will be determined by the `webbrowser`
353 353 standard library module, which allows setting of the
354 354 BROWSER environment variable to override it.
355 355 """)
356 356
357 357 use_less = Bool(False, config=True,
358 358 help="""Wether to use Browser Side less-css parsing
359 359 instead of compiled css version in templates that allows
360 360 it. This is mainly convenient when working on the less
361 361 file to avoid a build step, or if user want to overwrite
362 362 some of the less variables without having to recompile
363 363 everything.
364 364
365 365 You will need to install the less.js component in the static directory
366 366 either in the source tree or in your profile folder.
367 367 """)
368 368
369 369 webapp_settings = Dict(config=True,
370 370 help="Supply overrides for the tornado.web.Application that the "
371 371 "IPython notebook uses.")
372 372
373 373 enable_mathjax = Bool(True, config=True,
374 374 help="""Whether to enable MathJax for typesetting math/TeX
375 375
376 376 MathJax is the javascript library IPython uses to render math/LaTeX. It is
377 377 very large, so you may want to disable it if you have a slow internet
378 378 connection, or for offline use of the notebook.
379 379
380 380 When disabled, equations etc. will appear as their untransformed TeX source.
381 381 """
382 382 )
383 383 def _enable_mathjax_changed(self, name, old, new):
384 384 """set mathjax url to empty if mathjax is disabled"""
385 385 if not new:
386 386 self.mathjax_url = u''
387 387
388 388 base_project_url = Unicode('/', config=True,
389 389 help='''The base URL for the notebook server.
390 390
391 391 Leading and trailing slashes can be omitted,
392 392 and will automatically be added.
393 393 ''')
394 394 def _base_project_url_changed(self, name, old, new):
395 395 if not new.startswith('/'):
396 396 self.base_project_url = '/'+new
397 397 elif not new.endswith('/'):
398 398 self.base_project_url = new+'/'
399 399
400 400 base_kernel_url = Unicode('/', config=True,
401 401 help='''The base URL for the kernel server
402 402
403 403 Leading and trailing slashes can be omitted,
404 404 and will automatically be added.
405 405 ''')
406 406 def _base_kernel_url_changed(self, name, old, new):
407 407 if not new.startswith('/'):
408 408 self.base_kernel_url = '/'+new
409 409 elif not new.endswith('/'):
410 410 self.base_kernel_url = new+'/'
411 411
412 412 websocket_url = Unicode("", config=True,
413 413 help="""The base URL for the websocket server,
414 414 if it differs from the HTTP server (hint: it almost certainly doesn't).
415 415
416 416 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
417 417 """
418 418 )
419 419
420 420 extra_static_paths = List(Unicode, config=True,
421 421 help="""Extra paths to search for serving static files.
422 422
423 423 This allows adding javascript/css to be available from the notebook server machine,
424 424 or overriding individual files in the IPython"""
425 425 )
426 426 def _extra_static_paths_default(self):
427 427 return [os.path.join(self.profile_dir.location, 'static')]
428 428
429 429 @property
430 430 def static_file_path(self):
431 431 """return extra paths + the default location"""
432 432 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
433 433
434 434 mathjax_url = Unicode("", config=True,
435 435 help="""The url for MathJax.js."""
436 436 )
437 437 def _mathjax_url_default(self):
438 438 if not self.enable_mathjax:
439 439 return u''
440 440 static_url_prefix = self.webapp_settings.get("static_url_prefix",
441 441 url_path_join(self.base_project_url, "static")
442 442 )
443 443 try:
444 444 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
445 445 except IOError:
446 446 if self.certfile:
447 447 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
448 448 base = u"https://c328740.ssl.cf1.rackcdn.com"
449 449 else:
450 450 base = u"http://cdn.mathjax.org"
451 451
452 452 url = base + u"/mathjax/latest/MathJax.js"
453 453 self.log.info("Using MathJax from CDN: %s", url)
454 454 return url
455 455 else:
456 456 self.log.info("Using local MathJax from %s" % mathjax)
457 457 return url_path_join(static_url_prefix, u"mathjax/MathJax.js")
458 458
459 459 def _mathjax_url_changed(self, name, old, new):
460 460 if new and not self.enable_mathjax:
461 461 # enable_mathjax=False overrides mathjax_url
462 462 self.mathjax_url = u''
463 463 else:
464 464 self.log.info("Using MathJax: %s", new)
465 465
466 466 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
467 467 config=True,
468 468 help='The notebook manager class to use.')
469 469
470 470 trust_xheaders = Bool(False, config=True,
471 471 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
472 472 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
473 473 )
474
474
475 475 def parse_command_line(self, argv=None):
476 476 super(NotebookApp, self).parse_command_line(argv)
477 if argv is None:
478 argv = sys.argv[1:]
479
480 # Scrub frontend-specific flags
481 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
482 # Kernel should inherit default config file from frontend
483 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
484
477
485 478 if self.extra_args:
486 479 f = os.path.abspath(self.extra_args[0])
487 480 if os.path.isdir(f):
488 481 nbdir = f
489 482 else:
490 483 self.file_to_run = f
491 484 nbdir = os.path.dirname(f)
492 485 self.config.NotebookManager.notebook_dir = nbdir
493 486
487 def init_kernel_argv(self):
488 """construct the kernel arguments"""
489 # Scrub frontend-specific flags
490 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
491 # Kernel should inherit default config file from frontend
492 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
493 # Kernel should get *absolute* path to profile directory
494 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
495
494 496 def init_configurables(self):
495 497 # force Session default to be secure
496 498 default_secure(self.config)
497 499 self.kernel_manager = MappingKernelManager(
498 500 parent=self, log=self.log, kernel_argv=self.kernel_argv,
499 501 connection_dir = self.profile_dir.security_dir,
500 502 )
501 503 kls = import_item(self.notebook_manager_class)
502 504 self.notebook_manager = kls(parent=self, log=self.log)
503 505 self.notebook_manager.load_notebook_names()
504 506 self.cluster_manager = ClusterManager(parent=self, log=self.log)
505 507 self.cluster_manager.update_profiles()
506 508
507 509 def init_logging(self):
508 510 # This prevents double log messages because tornado use a root logger that
509 511 # self.log is a child of. The logging module dipatches log messages to a log
510 512 # and all of its ancenstors until propagate is set to False.
511 513 self.log.propagate = False
512 514
513 515 # hook up tornado 3's loggers to our app handlers
514 516 for name in ('access', 'application', 'general'):
515 517 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
516 518
517 519 def init_webapp(self):
518 520 """initialize tornado webapp and httpserver"""
519 521 self.web_app = NotebookWebApplication(
520 522 self, self.kernel_manager, self.notebook_manager,
521 523 self.cluster_manager, self.log,
522 524 self.base_project_url, self.webapp_settings
523 525 )
524 526 if self.certfile:
525 527 ssl_options = dict(certfile=self.certfile)
526 528 if self.keyfile:
527 529 ssl_options['keyfile'] = self.keyfile
528 530 else:
529 531 ssl_options = None
530 532 self.web_app.password = self.password
531 533 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
532 534 xheaders=self.trust_xheaders)
533 535 if not self.ip:
534 536 warning = "WARNING: The notebook server is listening on all IP addresses"
535 537 if ssl_options is None:
536 538 self.log.critical(warning + " and not using encryption. This "
537 539 "is not recommended.")
538 540 if not self.password:
539 541 self.log.critical(warning + " and not using authentication. "
540 542 "This is highly insecure and not recommended.")
541 543 success = None
542 544 for port in random_ports(self.port, self.port_retries+1):
543 545 try:
544 546 self.http_server.listen(port, self.ip)
545 547 except socket.error as e:
546 548 # XXX: remove the e.errno == -9 block when we require
547 549 # tornado >= 3.0
548 550 if e.errno == -9 and tornado.version_info[0] < 3:
549 551 # The flags passed to socket.getaddrinfo from
550 552 # tornado.netutils.bind_sockets can cause "gaierror:
551 553 # [Errno -9] Address family for hostname not supported"
552 554 # when the interface is not associated, for example.
553 555 # Changing the flags to exclude socket.AI_ADDRCONFIG does
554 556 # not cause this error, but the only way to do this is to
555 557 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
556 558 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
557 559 self.log.warn('Monkeypatching socket to fix tornado bug')
558 560 del(socket.AI_ADDRCONFIG)
559 561 try:
560 562 # retry the tornado call without AI_ADDRCONFIG flags
561 563 self.http_server.listen(port, self.ip)
562 564 except socket.error as e2:
563 565 e = e2
564 566 else:
565 567 self.port = port
566 568 success = True
567 569 break
568 570 # restore the monekypatch
569 571 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
570 572 if e.errno != errno.EADDRINUSE:
571 573 raise
572 574 self.log.info('The port %i is already in use, trying another random port.' % port)
573 575 else:
574 576 self.port = port
575 577 success = True
576 578 break
577 579 if not success:
578 580 self.log.critical('ERROR: the notebook server could not be started because '
579 581 'no available port could be found.')
580 582 self.exit(1)
581 583
582 584 def init_signal(self):
583 585 if not sys.platform.startswith('win'):
584 586 signal.signal(signal.SIGINT, self._handle_sigint)
585 587 signal.signal(signal.SIGTERM, self._signal_stop)
586 588 if hasattr(signal, 'SIGUSR1'):
587 589 # Windows doesn't support SIGUSR1
588 590 signal.signal(signal.SIGUSR1, self._signal_info)
589 591 if hasattr(signal, 'SIGINFO'):
590 592 # only on BSD-based systems
591 593 signal.signal(signal.SIGINFO, self._signal_info)
592 594
593 595 def _handle_sigint(self, sig, frame):
594 596 """SIGINT handler spawns confirmation dialog"""
595 597 # register more forceful signal handler for ^C^C case
596 598 signal.signal(signal.SIGINT, self._signal_stop)
597 599 # request confirmation dialog in bg thread, to avoid
598 600 # blocking the App
599 601 thread = threading.Thread(target=self._confirm_exit)
600 602 thread.daemon = True
601 603 thread.start()
602 604
603 605 def _restore_sigint_handler(self):
604 606 """callback for restoring original SIGINT handler"""
605 607 signal.signal(signal.SIGINT, self._handle_sigint)
606 608
607 609 def _confirm_exit(self):
608 610 """confirm shutdown on ^C
609 611
610 612 A second ^C, or answering 'y' within 5s will cause shutdown,
611 613 otherwise original SIGINT handler will be restored.
612 614
613 615 This doesn't work on Windows.
614 616 """
615 617 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
616 618 time.sleep(0.1)
617 619 info = self.log.info
618 620 info('interrupted')
619 621 print self.notebook_info()
620 622 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
621 623 sys.stdout.flush()
622 624 r,w,x = select.select([sys.stdin], [], [], 5)
623 625 if r:
624 626 line = sys.stdin.readline()
625 627 if line.lower().startswith('y'):
626 628 self.log.critical("Shutdown confirmed")
627 629 ioloop.IOLoop.instance().stop()
628 630 return
629 631 else:
630 632 print "No answer for 5s:",
631 633 print "resuming operation..."
632 634 # no answer, or answer is no:
633 635 # set it back to original SIGINT handler
634 636 # use IOLoop.add_callback because signal.signal must be called
635 637 # from main thread
636 638 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
637 639
638 640 def _signal_stop(self, sig, frame):
639 641 self.log.critical("received signal %s, stopping", sig)
640 642 ioloop.IOLoop.instance().stop()
641 643
642 644 def _signal_info(self, sig, frame):
643 645 print self.notebook_info()
644 646
645 647 def init_components(self):
646 648 """Check the components submodule, and warn if it's unclean"""
647 649 status = submodule.check_submodule_status()
648 650 if status == 'missing':
649 651 self.log.warn("components submodule missing, running `git submodule update`")
650 652 submodule.update_submodules(submodule.ipython_parent())
651 653 elif status == 'unclean':
652 654 self.log.warn("components submodule unclean, you may see 404s on static/components")
653 655 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
654 656
655 657
656 658 @catch_config_error
657 659 def initialize(self, argv=None):
658 660 self.init_logging()
659 661 super(NotebookApp, self).initialize(argv)
662 self.init_kernel_argv()
660 663 self.init_configurables()
661 664 self.init_components()
662 665 self.init_webapp()
663 666 self.init_signal()
664 667
665 668 def cleanup_kernels(self):
666 669 """Shutdown all kernels.
667 670
668 671 The kernels will shutdown themselves when this process no longer exists,
669 672 but explicit shutdown allows the KernelManagers to cleanup the connection files.
670 673 """
671 674 self.log.info('Shutting down kernels')
672 675 self.kernel_manager.shutdown_all()
673 676
674 677 def notebook_info(self):
675 678 "Return the current working directory and the server url information"
676 679 mgr_info = self.notebook_manager.info_string() + "\n"
677 680 return mgr_info +"The IPython Notebook is running at: %s" % self._url
678 681
679 682 def start(self):
680 683 """ Start the IPython Notebook server app, after initialization
681 684
682 685 This method takes no arguments so all configuration and initialization
683 686 must be done prior to calling this method."""
684 687 ip = self.ip if self.ip else '[all ip addresses on your system]'
685 688 proto = 'https' if self.certfile else 'http'
686 689 info = self.log.info
687 690 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
688 691 self.base_project_url)
689 692 for line in self.notebook_info().split("\n"):
690 693 info(line)
691 694 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
692 695
693 696 if self.open_browser or self.file_to_run:
694 697 ip = self.ip or LOCALHOST
695 698 try:
696 699 browser = webbrowser.get(self.browser or None)
697 700 except webbrowser.Error as e:
698 701 self.log.warn('No web browser found: %s.' % e)
699 702 browser = None
700 703
701 704 if self.file_to_run:
702 705 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
703 706 url = self.notebook_manager.rev_mapping.get(name, '')
704 707 else:
705 708 url = ''
706 709 if browser:
707 710 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
708 711 self.port, self.base_project_url, url), new=2)
709 712 threading.Thread(target=b).start()
710 713 try:
711 714 ioloop.IOLoop.instance().start()
712 715 except KeyboardInterrupt:
713 716 info("Interrupted...")
714 717 finally:
715 718 self.cleanup_kernels()
716 719
717 720
718 721 #-----------------------------------------------------------------------------
719 722 # Main entry point
720 723 #-----------------------------------------------------------------------------
721 724
722 725 launch_new_instance = NotebookApp.launch_instance
723 726
@@ -1,277 +1,276 b''
1 1 # encoding: utf-8
2 2 """
3 3 The Base Application class for IPython.parallel apps
4 4
5 5 Authors:
6 6
7 7 * Brian Granger
8 8 * Min RK
9 9
10 10 """
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008-2011 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 from __future__ import with_statement
24 24
25 25 import os
26 26 import logging
27 27 import re
28 28 import sys
29 29
30 30 from subprocess import Popen, PIPE
31 31
32 32 from IPython.config.application import catch_config_error, LevelFormatter
33 33 from IPython.core import release
34 34 from IPython.core.crashhandler import CrashHandler
35 35 from IPython.core.application import (
36 36 BaseIPythonApplication,
37 37 base_aliases as base_ip_aliases,
38 38 base_flags as base_ip_flags
39 39 )
40 40 from IPython.utils.path import expand_path
41 41
42 42 from IPython.utils.traitlets import Unicode, Bool, Instance, Dict, List
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # Module errors
46 46 #-----------------------------------------------------------------------------
47 47
48 48 class PIDFileError(Exception):
49 49 pass
50 50
51 51
52 52 #-----------------------------------------------------------------------------
53 53 # Crash handler for this application
54 54 #-----------------------------------------------------------------------------
55 55
56 56 class ParallelCrashHandler(CrashHandler):
57 57 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
58 58
59 59 def __init__(self, app):
60 60 contact_name = release.authors['Min'][0]
61 61 contact_email = release.author_email
62 62 bug_tracker = 'https://github.com/ipython/ipython/issues'
63 63 super(ParallelCrashHandler,self).__init__(
64 64 app, contact_name, contact_email, bug_tracker
65 65 )
66 66
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Main application
70 70 #-----------------------------------------------------------------------------
71 71 base_aliases = {}
72 72 base_aliases.update(base_ip_aliases)
73 73 base_aliases.update({
74 'profile-dir' : 'ProfileDir.location',
75 74 'work-dir' : 'BaseParallelApplication.work_dir',
76 75 'log-to-file' : 'BaseParallelApplication.log_to_file',
77 76 'clean-logs' : 'BaseParallelApplication.clean_logs',
78 77 'log-url' : 'BaseParallelApplication.log_url',
79 78 'cluster-id' : 'BaseParallelApplication.cluster_id',
80 79 })
81 80
82 81 base_flags = {
83 82 'log-to-file' : (
84 83 {'BaseParallelApplication' : {'log_to_file' : True}},
85 84 "send log output to a file"
86 85 )
87 86 }
88 87 base_flags.update(base_ip_flags)
89 88
90 89 class BaseParallelApplication(BaseIPythonApplication):
91 90 """The base Application for IPython.parallel apps
92 91
93 92 Principle extensions to BaseIPyythonApplication:
94 93
95 94 * work_dir
96 95 * remote logging via pyzmq
97 96 * IOLoop instance
98 97 """
99 98
100 99 crash_handler_class = ParallelCrashHandler
101 100
102 101 def _log_level_default(self):
103 102 # temporarily override default_log_level to INFO
104 103 return logging.INFO
105 104
106 105 def _log_format_default(self):
107 106 """override default log format to include time"""
108 107 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
109 108
110 109 work_dir = Unicode(os.getcwdu(), config=True,
111 110 help='Set the working dir for the process.'
112 111 )
113 112 def _work_dir_changed(self, name, old, new):
114 113 self.work_dir = unicode(expand_path(new))
115 114
116 115 log_to_file = Bool(config=True,
117 116 help="whether to log to a file")
118 117
119 118 clean_logs = Bool(False, config=True,
120 119 help="whether to cleanup old logfiles before starting")
121 120
122 121 log_url = Unicode('', config=True,
123 122 help="The ZMQ URL of the iplogger to aggregate logging.")
124 123
125 124 cluster_id = Unicode('', config=True,
126 125 help="""String id to add to runtime files, to prevent name collisions when
127 126 using multiple clusters with a single profile simultaneously.
128 127
129 128 When set, files will be named like: 'ipcontroller-<cluster_id>-engine.json'
130 129
131 130 Since this is text inserted into filenames, typical recommendations apply:
132 131 Simple character strings are ideal, and spaces are not recommended (but should
133 132 generally work).
134 133 """
135 134 )
136 135 def _cluster_id_changed(self, name, old, new):
137 136 self.name = self.__class__.name
138 137 if new:
139 138 self.name += '-%s'%new
140 139
141 140 def _config_files_default(self):
142 141 return ['ipcontroller_config.py', 'ipengine_config.py', 'ipcluster_config.py']
143 142
144 143 loop = Instance('zmq.eventloop.ioloop.IOLoop')
145 144 def _loop_default(self):
146 145 from zmq.eventloop.ioloop import IOLoop
147 146 return IOLoop.instance()
148 147
149 148 aliases = Dict(base_aliases)
150 149 flags = Dict(base_flags)
151 150
152 151 @catch_config_error
153 152 def initialize(self, argv=None):
154 153 """initialize the app"""
155 154 super(BaseParallelApplication, self).initialize(argv)
156 155 self.to_work_dir()
157 156 self.reinit_logging()
158 157
159 158 def to_work_dir(self):
160 159 wd = self.work_dir
161 160 if unicode(wd) != os.getcwdu():
162 161 os.chdir(wd)
163 162 self.log.info("Changing to working dir: %s" % wd)
164 163 # This is the working dir by now.
165 164 sys.path.insert(0, '')
166 165
167 166 def reinit_logging(self):
168 167 # Remove old log files
169 168 log_dir = self.profile_dir.log_dir
170 169 if self.clean_logs:
171 170 for f in os.listdir(log_dir):
172 171 if re.match(r'%s-\d+\.(log|err|out)' % self.name, f):
173 172 try:
174 173 os.remove(os.path.join(log_dir, f))
175 174 except (OSError, IOError):
176 175 # probably just conflict from sibling process
177 176 # already removing it
178 177 pass
179 178 if self.log_to_file:
180 179 # Start logging to the new log file
181 180 log_filename = self.name + u'-' + str(os.getpid()) + u'.log'
182 181 logfile = os.path.join(log_dir, log_filename)
183 182 open_log_file = open(logfile, 'w')
184 183 else:
185 184 open_log_file = None
186 185 if open_log_file is not None:
187 186 while self.log.handlers:
188 187 self.log.removeHandler(self.log.handlers[0])
189 188 self._log_handler = logging.StreamHandler(open_log_file)
190 189 self.log.addHandler(self._log_handler)
191 190 else:
192 191 self._log_handler = self.log.handlers[0]
193 192 # Add timestamps to log format:
194 193 self._log_formatter = LevelFormatter(self.log_format,
195 194 datefmt=self.log_datefmt)
196 195 self._log_handler.setFormatter(self._log_formatter)
197 196 # do not propagate log messages to root logger
198 197 # ipcluster app will sometimes print duplicate messages during shutdown
199 198 # if this is 1 (default):
200 199 self.log.propagate = False
201 200
202 201 def write_pid_file(self, overwrite=False):
203 202 """Create a .pid file in the pid_dir with my pid.
204 203
205 204 This must be called after pre_construct, which sets `self.pid_dir`.
206 205 This raises :exc:`PIDFileError` if the pid file exists already.
207 206 """
208 207 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
209 208 if os.path.isfile(pid_file):
210 209 pid = self.get_pid_from_file()
211 210 if not overwrite:
212 211 raise PIDFileError(
213 212 'The pid file [%s] already exists. \nThis could mean that this '
214 213 'server is already running with [pid=%s].' % (pid_file, pid)
215 214 )
216 215 with open(pid_file, 'w') as f:
217 216 self.log.info("Creating pid file: %s" % pid_file)
218 217 f.write(repr(os.getpid())+'\n')
219 218
220 219 def remove_pid_file(self):
221 220 """Remove the pid file.
222 221
223 222 This should be called at shutdown by registering a callback with
224 223 :func:`reactor.addSystemEventTrigger`. This needs to return
225 224 ``None``.
226 225 """
227 226 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
228 227 if os.path.isfile(pid_file):
229 228 try:
230 229 self.log.info("Removing pid file: %s" % pid_file)
231 230 os.remove(pid_file)
232 231 except:
233 232 self.log.warn("Error removing the pid file: %s" % pid_file)
234 233
235 234 def get_pid_from_file(self):
236 235 """Get the pid from the pid file.
237 236
238 237 If the pid file doesn't exist a :exc:`PIDFileError` is raised.
239 238 """
240 239 pid_file = os.path.join(self.profile_dir.pid_dir, self.name + u'.pid')
241 240 if os.path.isfile(pid_file):
242 241 with open(pid_file, 'r') as f:
243 242 s = f.read().strip()
244 243 try:
245 244 pid = int(s)
246 245 except:
247 246 raise PIDFileError("invalid pid file: %s (contents: %r)"%(pid_file, s))
248 247 return pid
249 248 else:
250 249 raise PIDFileError('pid file not found: %s' % pid_file)
251 250
252 251 def check_pid(self, pid):
253 252 if os.name == 'nt':
254 253 try:
255 254 import ctypes
256 255 # returns 0 if no such process (of ours) exists
257 256 # positive int otherwise
258 257 p = ctypes.windll.kernel32.OpenProcess(1,0,pid)
259 258 except Exception:
260 259 self.log.warn(
261 260 "Could not determine whether pid %i is running via `OpenProcess`. "
262 261 " Making the likely assumption that it is."%pid
263 262 )
264 263 return True
265 264 return bool(p)
266 265 else:
267 266 try:
268 267 p = Popen(['ps','x'], stdout=PIPE, stderr=PIPE)
269 268 output,_ = p.communicate()
270 269 except OSError:
271 270 self.log.warn(
272 271 "Could not determine whether pid %i is running via `ps x`. "
273 272 " Making the likely assumption that it is."%pid
274 273 )
275 274 return True
276 275 pids = map(int, re.findall(r'^\W*\d+', output, re.MULTILINE))
277 276 return pid in pids
General Comments 0
You need to be logged in to leave comments. Login now