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