##// END OF EJS Templates
First version of cluster web service....
Brian Granger -
Show More
@@ -0,0 +1,90 b''
1 """Manage IPython.parallel clusters in the notebook.
2
3 Authors:
4
5 * Brian Granger
6 """
7
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
18
19 import datetime
20 import os
21 import uuid
22 import glob
23
24 from tornado import web
25 from zmq.eventloop import ioloop
26
27 from IPython.config.configurable import LoggingConfigurable
28 from IPython.utils.traitlets import Unicode, List, Dict, Bool
29 from IPython.parallel.apps.launcher import IPClusterLauncher
30 from IPython.core.profileapp import list_profiles_in, list_bundled_profiles
31 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
32
33 #-----------------------------------------------------------------------------
34 # Classes
35 #-----------------------------------------------------------------------------
36
37 class ClusterManager(LoggingConfigurable):
38
39 profiles = Dict()
40
41
42 def list_profile_names(self):
43 """List all profiles in the ipython_dir and cwd.
44 """
45 profiles = list_profiles_in(get_ipython_dir())
46 profiles += list_profiles_in(os.getcwdu())
47 return profiles
48
49
50 def list_profiles(self):
51 profiles = self.list_profile_names()
52 result = [self.profile_info(p) for p in profiles]
53 return result
54
55
56 def profile_info(self, profile):
57 if profile not in self.list_profile_names():
58 raise web.HTTPError(404, u'profile not found')
59 result = dict(profile=profile)
60 data = self.profiles.get(profile)
61 if data is None:
62 result['status'] = 'stopped'
63 else:
64 result['status'] = 'running'
65 result['n'] = data['n']
66 return result
67
68 def start_cluster(self, profile, n=4):
69 """Start a cluster for a given profile."""
70 if profile not in self.list_profile_names():
71 raise web.HTTPError(404, u'profile not found')
72 if profile in self.profiles:
73 raise web.HTTPError(409, u'cluster already running')
74 launcher = IPClusterLauncher(ipcluster_profile=profile, ipcluster_n=n)
75 launcher.start()
76 self.profiles[profile] = {
77 'launcher': launcher,
78 'n': n
79 }
80
81 def stop_cluster(self, profile):
82 """Stop a cluster for a given profile."""
83 if profile not in self.profiles:
84 raise web.HTTPError(409, u'cluster not running')
85 launcher = self.profiles.pop(profile)['launcher']
86 launcher.stop()
87
88 def stop_all_clusters(self):
89 for p in self.profiles.values():
90 p['launcher'].stop()
@@ -1,290 +1,293 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 An application for managing IPython profiles.
3 An application for managing IPython profiles.
4
4
5 To be invoked as the `ipython profile` subcommand.
5 To be invoked as the `ipython profile` subcommand.
6
6
7 Authors:
7 Authors:
8
8
9 * Min RK
9 * Min RK
10
10
11 """
11 """
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Copyright (C) 2008-2011 The IPython Development Team
14 # Copyright (C) 2008-2011 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 import logging
24 import logging
25 import os
25 import os
26
26
27 from IPython.config.application import Application, boolean_flag
27 from IPython.config.application import Application, boolean_flag
28 from IPython.core.application import (
28 from IPython.core.application import (
29 BaseIPythonApplication, base_flags, base_aliases
29 BaseIPythonApplication, base_flags, base_aliases
30 )
30 )
31 from IPython.core.profiledir import ProfileDir
31 from IPython.core.profiledir import ProfileDir
32 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
32 from IPython.utils.path import get_ipython_dir, get_ipython_package_dir
33 from IPython.utils.traitlets import Unicode, Bool, Dict
33 from IPython.utils.traitlets import Unicode, Bool, Dict
34
34
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36 # Constants
36 # Constants
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38
38
39 create_help = """Create an IPython profile by name
39 create_help = """Create an IPython profile by name
40
40
41 Create an ipython profile directory by its name or
41 Create an ipython profile directory by its name or
42 profile directory path. Profile directories contain
42 profile directory path. Profile directories contain
43 configuration, log and security related files and are named
43 configuration, log and security related files and are named
44 using the convention 'profile_<name>'. By default they are
44 using the convention 'profile_<name>'. By default they are
45 located in your ipython directory. Once created, you will
45 located in your ipython directory. Once created, you will
46 can edit the configuration files in the profile
46 can edit the configuration files in the profile
47 directory to configure IPython. Most users will create a
47 directory to configure IPython. Most users will create a
48 profile directory by name,
48 profile directory by name,
49 `ipython profile create myprofile`, which will put the directory
49 `ipython profile create myprofile`, which will put the directory
50 in `<ipython_dir>/profile_myprofile`.
50 in `<ipython_dir>/profile_myprofile`.
51 """
51 """
52 list_help = """List available IPython profiles
52 list_help = """List available IPython profiles
53
53
54 List all available profiles, by profile location, that can
54 List all available profiles, by profile location, that can
55 be found in the current working directly or in the ipython
55 be found in the current working directly or in the ipython
56 directory. Profile directories are named using the convention
56 directory. Profile directories are named using the convention
57 'profile_<profile>'.
57 'profile_<profile>'.
58 """
58 """
59 profile_help = """Manage IPython profiles
59 profile_help = """Manage IPython profiles
60
60
61 Profile directories contain
61 Profile directories contain
62 configuration, log and security related files and are named
62 configuration, log and security related files and are named
63 using the convention 'profile_<name>'. By default they are
63 using the convention 'profile_<name>'. By default they are
64 located in your ipython directory. You can create profiles
64 located in your ipython directory. You can create profiles
65 with `ipython profile create <name>`, or see the profiles you
65 with `ipython profile create <name>`, or see the profiles you
66 already have with `ipython profile list`
66 already have with `ipython profile list`
67
67
68 To get started configuring IPython, simply do:
68 To get started configuring IPython, simply do:
69
69
70 $> ipython profile create
70 $> ipython profile create
71
71
72 and IPython will create the default profile in <ipython_dir>/profile_default,
72 and IPython will create the default profile in <ipython_dir>/profile_default,
73 where you can edit ipython_config.py to start configuring IPython.
73 where you can edit ipython_config.py to start configuring IPython.
74
74
75 """
75 """
76
76
77 _list_examples = "ipython profile list # list all profiles"
77 _list_examples = "ipython profile list # list all profiles"
78
78
79 _create_examples = """
79 _create_examples = """
80 ipython profile create foo # create profile foo w/ default config files
80 ipython profile create foo # create profile foo w/ default config files
81 ipython profile create foo --reset # restage default config files over current
81 ipython profile create foo --reset # restage default config files over current
82 ipython profile create foo --parallel # also stage parallel config files
82 ipython profile create foo --parallel # also stage parallel config files
83 """
83 """
84
84
85 _main_examples = """
85 _main_examples = """
86 ipython profile create -h # show the help string for the create subcommand
86 ipython profile create -h # show the help string for the create subcommand
87 ipython profile list -h # show the help string for the list subcommand
87 ipython profile list -h # show the help string for the list subcommand
88 """
88 """
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Profile Application Class (for `ipython profile` subcommand)
91 # Profile Application Class (for `ipython profile` subcommand)
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94
94
95 def list_profiles_in(path):
96 """list profiles in a given root directory"""
97 files = os.listdir(path)
98 profiles = []
99 for f in files:
100 full_path = os.path.join(path, f)
101 if os.path.isdir(full_path) and f.startswith('profile_'):
102 profiles.append(f.split('_',1)[-1])
103 return profiles
104
105
106 def list_bundled_profiles():
107 """list profiles that are bundled with IPython."""
108 path = os.path.join(get_ipython_package_dir(), u'config', u'profile')
109 files = os.listdir(path)
110 profiles = []
111 for profile in files:
112 full_path = os.path.join(path, profile)
113 if os.path.isdir(full_path):
114 profiles.append(profile)
115 return profiles
116
117
95 class ProfileList(Application):
118 class ProfileList(Application):
96 name = u'ipython-profile'
119 name = u'ipython-profile'
97 description = list_help
120 description = list_help
98 examples = _list_examples
121 examples = _list_examples
99
122
100 aliases = Dict({
123 aliases = Dict({
101 'ipython-dir' : 'ProfileList.ipython_dir',
124 'ipython-dir' : 'ProfileList.ipython_dir',
102 'log-level' : 'Application.log_level',
125 'log-level' : 'Application.log_level',
103 })
126 })
104 flags = Dict(dict(
127 flags = Dict(dict(
105 debug = ({'Application' : {'log_level' : 0}},
128 debug = ({'Application' : {'log_level' : 0}},
106 "Set Application.log_level to 0, maximizing log output."
129 "Set Application.log_level to 0, maximizing log output."
107 )
130 )
108 ))
131 ))
109
132
110 ipython_dir = Unicode(get_ipython_dir(), config=True,
133 ipython_dir = Unicode(get_ipython_dir(), config=True,
111 help="""
134 help="""
112 The name of the IPython directory. This directory is used for logging
135 The name of the IPython directory. This directory is used for logging
113 configuration (through profiles), history storage, etc. The default
136 configuration (through profiles), history storage, etc. The default
114 is usually $HOME/.ipython. This options can also be specified through
137 is usually $HOME/.ipython. This options can also be specified through
115 the environment variable IPYTHON_DIR.
138 the environment variable IPYTHON_DIR.
116 """
139 """
117 )
140 )
118
141
119 def _list_profiles_in(self, path):
142
120 """list profiles in a given root directory"""
121 files = os.listdir(path)
122 profiles = []
123 for f in files:
124 full_path = os.path.join(path, f)
125 if os.path.isdir(full_path) and f.startswith('profile_'):
126 profiles.append(f.split('_',1)[-1])
127 return profiles
128
129 def _list_bundled_profiles(self):
130 """list profiles in a given root directory"""
131 path = os.path.join(get_ipython_package_dir(), u'config', u'profile')
132 files = os.listdir(path)
133 profiles = []
134 for profile in files:
135 full_path = os.path.join(path, profile)
136 if os.path.isdir(full_path):
137 profiles.append(profile)
138 return profiles
139
140 def _print_profiles(self, profiles):
143 def _print_profiles(self, profiles):
141 """print list of profiles, indented."""
144 """print list of profiles, indented."""
142 for profile in profiles:
145 for profile in profiles:
143 print ' %s' % profile
146 print ' %s' % profile
144
147
145 def list_profile_dirs(self):
148 def list_profile_dirs(self):
146 profiles = self._list_bundled_profiles()
149 profiles = list_bundled_profiles()
147 if profiles:
150 if profiles:
148 print
151 print
149 print "Available profiles in IPython:"
152 print "Available profiles in IPython:"
150 self._print_profiles(profiles)
153 self._print_profiles(profiles)
151 print
154 print
152 print " The first request for a bundled profile will copy it"
155 print " The first request for a bundled profile will copy it"
153 print " into your IPython directory (%s)," % self.ipython_dir
156 print " into your IPython directory (%s)," % self.ipython_dir
154 print " where you can customize it."
157 print " where you can customize it."
155
158
156 profiles = self._list_profiles_in(self.ipython_dir)
159 profiles = list_profiles_in(self.ipython_dir)
157 if profiles:
160 if profiles:
158 print
161 print
159 print "Available profiles in %s:" % self.ipython_dir
162 print "Available profiles in %s:" % self.ipython_dir
160 self._print_profiles(profiles)
163 self._print_profiles(profiles)
161
164
162 profiles = self._list_profiles_in(os.getcwdu())
165 profiles = list_profiles_in(os.getcwdu())
163 if profiles:
166 if profiles:
164 print
167 print
165 print "Available profiles in current directory (%s):" % os.getcwdu()
168 print "Available profiles in current directory (%s):" % os.getcwdu()
166 self._print_profiles(profiles)
169 self._print_profiles(profiles)
167
170
168 print
171 print
169 print "To use any of the above profiles, start IPython with:"
172 print "To use any of the above profiles, start IPython with:"
170 print " ipython --profile=<name>"
173 print " ipython --profile=<name>"
171 print
174 print
172
175
173 def start(self):
176 def start(self):
174 self.list_profile_dirs()
177 self.list_profile_dirs()
175
178
176
179
177 create_flags = {}
180 create_flags = {}
178 create_flags.update(base_flags)
181 create_flags.update(base_flags)
179 # don't include '--init' flag, which implies running profile create in other apps
182 # don't include '--init' flag, which implies running profile create in other apps
180 create_flags.pop('init')
183 create_flags.pop('init')
181 create_flags['reset'] = ({'ProfileCreate': {'overwrite' : True}},
184 create_flags['reset'] = ({'ProfileCreate': {'overwrite' : True}},
182 "reset config files in this profile to the defaults.")
185 "reset config files in this profile to the defaults.")
183 create_flags['parallel'] = ({'ProfileCreate': {'parallel' : True}},
186 create_flags['parallel'] = ({'ProfileCreate': {'parallel' : True}},
184 "Include the config files for parallel "
187 "Include the config files for parallel "
185 "computing apps (ipengine, ipcontroller, etc.)")
188 "computing apps (ipengine, ipcontroller, etc.)")
186
189
187
190
188 class ProfileCreate(BaseIPythonApplication):
191 class ProfileCreate(BaseIPythonApplication):
189 name = u'ipython-profile'
192 name = u'ipython-profile'
190 description = create_help
193 description = create_help
191 examples = _create_examples
194 examples = _create_examples
192 auto_create = Bool(True, config=False)
195 auto_create = Bool(True, config=False)
193
196
194 def _copy_config_files_default(self):
197 def _copy_config_files_default(self):
195 return True
198 return True
196
199
197 parallel = Bool(False, config=True,
200 parallel = Bool(False, config=True,
198 help="whether to include parallel computing config files")
201 help="whether to include parallel computing config files")
199 def _parallel_changed(self, name, old, new):
202 def _parallel_changed(self, name, old, new):
200 parallel_files = [ 'ipcontroller_config.py',
203 parallel_files = [ 'ipcontroller_config.py',
201 'ipengine_config.py',
204 'ipengine_config.py',
202 'ipcluster_config.py'
205 'ipcluster_config.py'
203 ]
206 ]
204 if new:
207 if new:
205 for cf in parallel_files:
208 for cf in parallel_files:
206 self.config_files.append(cf)
209 self.config_files.append(cf)
207 else:
210 else:
208 for cf in parallel_files:
211 for cf in parallel_files:
209 if cf in self.config_files:
212 if cf in self.config_files:
210 self.config_files.remove(cf)
213 self.config_files.remove(cf)
211
214
212 def parse_command_line(self, argv):
215 def parse_command_line(self, argv):
213 super(ProfileCreate, self).parse_command_line(argv)
216 super(ProfileCreate, self).parse_command_line(argv)
214 # accept positional arg as profile name
217 # accept positional arg as profile name
215 if self.extra_args:
218 if self.extra_args:
216 self.profile = self.extra_args[0]
219 self.profile = self.extra_args[0]
217
220
218 flags = Dict(create_flags)
221 flags = Dict(create_flags)
219
222
220 classes = [ProfileDir]
223 classes = [ProfileDir]
221
224
222 def init_config_files(self):
225 def init_config_files(self):
223 super(ProfileCreate, self).init_config_files()
226 super(ProfileCreate, self).init_config_files()
224 # use local imports, since these classes may import from here
227 # use local imports, since these classes may import from here
225 from IPython.frontend.terminal.ipapp import TerminalIPythonApp
228 from IPython.frontend.terminal.ipapp import TerminalIPythonApp
226 apps = [TerminalIPythonApp]
229 apps = [TerminalIPythonApp]
227 try:
230 try:
228 from IPython.frontend.qt.console.qtconsoleapp import IPythonQtConsoleApp
231 from IPython.frontend.qt.console.qtconsoleapp import IPythonQtConsoleApp
229 except Exception:
232 except Exception:
230 # this should be ImportError, but under weird circumstances
233 # this should be ImportError, but under weird circumstances
231 # this might be an AttributeError, or possibly others
234 # this might be an AttributeError, or possibly others
232 # in any case, nothing should cause the profile creation to crash.
235 # in any case, nothing should cause the profile creation to crash.
233 pass
236 pass
234 else:
237 else:
235 apps.append(IPythonQtConsoleApp)
238 apps.append(IPythonQtConsoleApp)
236 try:
239 try:
237 from IPython.frontend.html.notebook.notebookapp import NotebookApp
240 from IPython.frontend.html.notebook.notebookapp import NotebookApp
238 except ImportError:
241 except ImportError:
239 pass
242 pass
240 except Exception:
243 except Exception:
241 self.log.debug('Unexpected error when importing NotebookApp',
244 self.log.debug('Unexpected error when importing NotebookApp',
242 exc_info=True
245 exc_info=True
243 )
246 )
244 else:
247 else:
245 apps.append(NotebookApp)
248 apps.append(NotebookApp)
246 if self.parallel:
249 if self.parallel:
247 from IPython.parallel.apps.ipcontrollerapp import IPControllerApp
250 from IPython.parallel.apps.ipcontrollerapp import IPControllerApp
248 from IPython.parallel.apps.ipengineapp import IPEngineApp
251 from IPython.parallel.apps.ipengineapp import IPEngineApp
249 from IPython.parallel.apps.ipclusterapp import IPClusterStart
252 from IPython.parallel.apps.ipclusterapp import IPClusterStart
250 from IPython.parallel.apps.iploggerapp import IPLoggerApp
253 from IPython.parallel.apps.iploggerapp import IPLoggerApp
251 apps.extend([
254 apps.extend([
252 IPControllerApp,
255 IPControllerApp,
253 IPEngineApp,
256 IPEngineApp,
254 IPClusterStart,
257 IPClusterStart,
255 IPLoggerApp,
258 IPLoggerApp,
256 ])
259 ])
257 for App in apps:
260 for App in apps:
258 app = App()
261 app = App()
259 app.config.update(self.config)
262 app.config.update(self.config)
260 app.log = self.log
263 app.log = self.log
261 app.overwrite = self.overwrite
264 app.overwrite = self.overwrite
262 app.copy_config_files=True
265 app.copy_config_files=True
263 app.profile = self.profile
266 app.profile = self.profile
264 app.init_profile_dir()
267 app.init_profile_dir()
265 app.init_config_files()
268 app.init_config_files()
266
269
267 def stage_default_config_file(self):
270 def stage_default_config_file(self):
268 pass
271 pass
269
272
270
273
271 class ProfileApp(Application):
274 class ProfileApp(Application):
272 name = u'ipython-profile'
275 name = u'ipython-profile'
273 description = profile_help
276 description = profile_help
274 examples = _main_examples
277 examples = _main_examples
275
278
276 subcommands = Dict(dict(
279 subcommands = Dict(dict(
277 create = (ProfileCreate, "Create a new profile dir with default config files"),
280 create = (ProfileCreate, "Create a new profile dir with default config files"),
278 list = (ProfileList, "List existing profiles")
281 list = (ProfileList, "List existing profiles")
279 ))
282 ))
280
283
281 def start(self):
284 def start(self):
282 if self.subapp is None:
285 if self.subapp is None:
283 print "No subcommand specified. Must specify one of: %s"%(self.subcommands.keys())
286 print "No subcommand specified. Must specify one of: %s"%(self.subcommands.keys())
284 print
287 print
285 self.print_description()
288 self.print_description()
286 self.print_subcommands()
289 self.print_subcommands()
287 self.exit(1)
290 self.exit(1)
288 else:
291 else:
289 return self.subapp.start()
292 return self.subapp.start()
290
293
@@ -1,694 +1,728 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 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 import logging
19 import logging
20 import Cookie
20 import Cookie
21 import time
21 import time
22 import uuid
22 import uuid
23
23
24 from tornado import web
24 from tornado import web
25 from tornado import websocket
25 from tornado import websocket
26
26
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
29
29
30 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
33
33
34 try:
34 try:
35 from docutils.core import publish_string
35 from docutils.core import publish_string
36 except ImportError:
36 except ImportError:
37 publish_string = None
37 publish_string = None
38
38
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
42
42
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # parts tornado cares about haven't really changed, so it's OK to continue
44 # parts tornado cares about haven't really changed, so it's OK to continue
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # version as of Oct 30/2011) the version check fails, see the issue report:
46 # version as of Oct 30/2011) the version check fails, see the issue report:
47
47
48 # https://github.com/facebook/tornado/issues/385
48 # https://github.com/facebook/tornado/issues/385
49
49
50 # This issue has been fixed in Tornado post 2.1.1:
50 # This issue has been fixed in Tornado post 2.1.1:
51
51
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53
53
54 # Here we manually apply the same patch as above so that users of IPython can
54 # Here we manually apply the same patch as above so that users of IPython can
55 # continue to work with an officially released Tornado. We make the
55 # continue to work with an officially released Tornado. We make the
56 # monkeypatch version check as narrow as possible to limit its effects; once
56 # monkeypatch version check as narrow as possible to limit its effects; once
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58
58
59 import tornado
59 import tornado
60
60
61 if tornado.version_info <= (2,1,1):
61 if tornado.version_info <= (2,1,1):
62
62
63 def _execute(self, transforms, *args, **kwargs):
63 def _execute(self, transforms, *args, **kwargs):
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65
65
66 self.open_args = args
66 self.open_args = args
67 self.open_kwargs = kwargs
67 self.open_kwargs = kwargs
68
68
69 # The difference between version 8 and 13 is that in 8 the
69 # The difference between version 8 and 13 is that in 8 the
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # simply "Origin".
71 # simply "Origin".
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection.accept_connection()
74 self.ws_connection.accept_connection()
75
75
76 elif self.request.headers.get("Sec-WebSocket-Version"):
76 elif self.request.headers.get("Sec-WebSocket-Version"):
77 self.stream.write(tornado.escape.utf8(
77 self.stream.write(tornado.escape.utf8(
78 "HTTP/1.1 426 Upgrade Required\r\n"
78 "HTTP/1.1 426 Upgrade Required\r\n"
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 self.stream.close()
80 self.stream.close()
81
81
82 else:
82 else:
83 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection.accept_connection()
84 self.ws_connection.accept_connection()
85
85
86 websocket.WebSocketHandler._execute = _execute
86 websocket.WebSocketHandler._execute = _execute
87 del _execute
87 del _execute
88
88
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90 # Decorator for disabling read-only handlers
90 # Decorator for disabling read-only handlers
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92
92
93 @decorator
93 @decorator
94 def not_if_readonly(f, self, *args, **kwargs):
94 def not_if_readonly(f, self, *args, **kwargs):
95 if self.application.read_only:
95 if self.application.read_only:
96 raise web.HTTPError(403, "Notebook server is read-only")
96 raise web.HTTPError(403, "Notebook server is read-only")
97 else:
97 else:
98 return f(self, *args, **kwargs)
98 return f(self, *args, **kwargs)
99
99
100 @decorator
100 @decorator
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 """authenticate this page *unless* readonly view is active.
102 """authenticate this page *unless* readonly view is active.
103
103
104 In read-only mode, the notebook list and print view should
104 In read-only mode, the notebook list and print view should
105 be accessible without authentication.
105 be accessible without authentication.
106 """
106 """
107
107
108 @web.authenticated
108 @web.authenticated
109 def auth_f(self, *args, **kwargs):
109 def auth_f(self, *args, **kwargs):
110 return f(self, *args, **kwargs)
110 return f(self, *args, **kwargs)
111
111
112 if self.application.read_only:
112 if self.application.read_only:
113 return f(self, *args, **kwargs)
113 return f(self, *args, **kwargs)
114 else:
114 else:
115 return auth_f(self, *args, **kwargs)
115 return auth_f(self, *args, **kwargs)
116
116
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118 # Top-level handlers
118 # Top-level handlers
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120
120
121 class RequestHandler(web.RequestHandler):
121 class RequestHandler(web.RequestHandler):
122 """RequestHandler with default variable setting."""
122 """RequestHandler with default variable setting."""
123
123
124 def render(*args, **kwargs):
124 def render(*args, **kwargs):
125 kwargs.setdefault('message', '')
125 kwargs.setdefault('message', '')
126 return web.RequestHandler.render(*args, **kwargs)
126 return web.RequestHandler.render(*args, **kwargs)
127
127
128 class AuthenticatedHandler(RequestHandler):
128 class AuthenticatedHandler(RequestHandler):
129 """A RequestHandler with an authenticated user."""
129 """A RequestHandler with an authenticated user."""
130
130
131 def get_current_user(self):
131 def get_current_user(self):
132 user_id = self.get_secure_cookie("username")
132 user_id = self.get_secure_cookie("username")
133 # For now the user_id should not return empty, but it could eventually
133 # For now the user_id should not return empty, but it could eventually
134 if user_id == '':
134 if user_id == '':
135 user_id = 'anonymous'
135 user_id = 'anonymous'
136 if user_id is None:
136 if user_id is None:
137 # prevent extra Invalid cookie sig warnings:
137 # prevent extra Invalid cookie sig warnings:
138 self.clear_cookie('username')
138 self.clear_cookie('username')
139 if not self.application.password and not self.application.read_only:
139 if not self.application.password and not self.application.read_only:
140 user_id = 'anonymous'
140 user_id = 'anonymous'
141 return user_id
141 return user_id
142
142
143 @property
143 @property
144 def logged_in(self):
144 def logged_in(self):
145 """Is a user currently logged in?
145 """Is a user currently logged in?
146
146
147 """
147 """
148 user = self.get_current_user()
148 user = self.get_current_user()
149 return (user and not user == 'anonymous')
149 return (user and not user == 'anonymous')
150
150
151 @property
151 @property
152 def login_available(self):
152 def login_available(self):
153 """May a user proceed to log in?
153 """May a user proceed to log in?
154
154
155 This returns True if login capability is available, irrespective of
155 This returns True if login capability is available, irrespective of
156 whether the user is already logged in or not.
156 whether the user is already logged in or not.
157
157
158 """
158 """
159 return bool(self.application.password)
159 return bool(self.application.password)
160
160
161 @property
161 @property
162 def read_only(self):
162 def read_only(self):
163 """Is the notebook read-only?
163 """Is the notebook read-only?
164
164
165 """
165 """
166 return self.application.read_only
166 return self.application.read_only
167
167
168 @property
168 @property
169 def ws_url(self):
169 def ws_url(self):
170 """websocket url matching the current request
170 """websocket url matching the current request
171
171
172 turns http[s]://host[:port] into
172 turns http[s]://host[:port] into
173 ws[s]://host[:port]
173 ws[s]://host[:port]
174 """
174 """
175 proto = self.request.protocol.replace('http', 'ws')
175 proto = self.request.protocol.replace('http', 'ws')
176 host = self.application.ipython_app.websocket_host # default to config value
176 host = self.application.ipython_app.websocket_host # default to config value
177 if host == '':
177 if host == '':
178 host = self.request.host # get from request
178 host = self.request.host # get from request
179 return "%s://%s" % (proto, host)
179 return "%s://%s" % (proto, host)
180
180
181
181
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
182 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 """static files should only be accessible when logged in"""
183 """static files should only be accessible when logged in"""
184
184
185 @authenticate_unless_readonly
185 @authenticate_unless_readonly
186 def get(self, path):
186 def get(self, path):
187 return web.StaticFileHandler.get(self, path)
187 return web.StaticFileHandler.get(self, path)
188
188
189
189
190 class ProjectDashboardHandler(AuthenticatedHandler):
190 class ProjectDashboardHandler(AuthenticatedHandler):
191
191
192 @authenticate_unless_readonly
192 @authenticate_unless_readonly
193 def get(self):
193 def get(self):
194 nbm = self.application.notebook_manager
194 nbm = self.application.notebook_manager
195 project = nbm.notebook_dir
195 project = nbm.notebook_dir
196 self.render(
196 self.render(
197 'projectdashboard.html', project=project,
197 'projectdashboard.html', project=project,
198 base_project_url=self.application.ipython_app.base_project_url,
198 base_project_url=self.application.ipython_app.base_project_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
199 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 read_only=self.read_only,
200 read_only=self.read_only,
201 logged_in=self.logged_in,
201 logged_in=self.logged_in,
202 login_available=self.login_available
202 login_available=self.login_available
203 )
203 )
204
204
205
205
206 class LoginHandler(AuthenticatedHandler):
206 class LoginHandler(AuthenticatedHandler):
207
207
208 def _render(self, message=None):
208 def _render(self, message=None):
209 self.render('login.html',
209 self.render('login.html',
210 next=self.get_argument('next', default='/'),
210 next=self.get_argument('next', default='/'),
211 read_only=self.read_only,
211 read_only=self.read_only,
212 logged_in=self.logged_in,
212 logged_in=self.logged_in,
213 login_available=self.login_available,
213 login_available=self.login_available,
214 message=message
214 message=message
215 )
215 )
216
216
217 def get(self):
217 def get(self):
218 if self.current_user:
218 if self.current_user:
219 self.redirect(self.get_argument('next', default='/'))
219 self.redirect(self.get_argument('next', default='/'))
220 else:
220 else:
221 self._render()
221 self._render()
222
222
223 def post(self):
223 def post(self):
224 pwd = self.get_argument('password', default=u'')
224 pwd = self.get_argument('password', default=u'')
225 if self.application.password:
225 if self.application.password:
226 if passwd_check(self.application.password, pwd):
226 if passwd_check(self.application.password, pwd):
227 self.set_secure_cookie('username', str(uuid.uuid4()))
227 self.set_secure_cookie('username', str(uuid.uuid4()))
228 else:
228 else:
229 self._render(message={'error': 'Invalid password'})
229 self._render(message={'error': 'Invalid password'})
230 return
230 return
231
231
232 self.redirect(self.get_argument('next', default='/'))
232 self.redirect(self.get_argument('next', default='/'))
233
233
234
234
235 class LogoutHandler(AuthenticatedHandler):
235 class LogoutHandler(AuthenticatedHandler):
236
236
237 def get(self):
237 def get(self):
238 self.clear_cookie('username')
238 self.clear_cookie('username')
239 if self.login_available:
239 if self.login_available:
240 message = {'info': 'Successfully logged out.'}
240 message = {'info': 'Successfully logged out.'}
241 else:
241 else:
242 message = {'warning': 'Cannot log out. Notebook authentication '
242 message = {'warning': 'Cannot log out. Notebook authentication '
243 'is disabled.'}
243 'is disabled.'}
244
244
245 self.render('logout.html',
245 self.render('logout.html',
246 read_only=self.read_only,
246 read_only=self.read_only,
247 logged_in=self.logged_in,
247 logged_in=self.logged_in,
248 login_available=self.login_available,
248 login_available=self.login_available,
249 message=message)
249 message=message)
250
250
251
251
252 class NewHandler(AuthenticatedHandler):
252 class NewHandler(AuthenticatedHandler):
253
253
254 @web.authenticated
254 @web.authenticated
255 def get(self):
255 def get(self):
256 nbm = self.application.notebook_manager
256 nbm = self.application.notebook_manager
257 project = nbm.notebook_dir
257 project = nbm.notebook_dir
258 notebook_id = nbm.new_notebook()
258 notebook_id = nbm.new_notebook()
259 self.render(
259 self.render(
260 'notebook.html', project=project,
260 'notebook.html', project=project,
261 notebook_id=notebook_id,
261 notebook_id=notebook_id,
262 base_project_url=self.application.ipython_app.base_project_url,
262 base_project_url=self.application.ipython_app.base_project_url,
263 base_kernel_url=self.application.ipython_app.base_kernel_url,
263 base_kernel_url=self.application.ipython_app.base_kernel_url,
264 kill_kernel=False,
264 kill_kernel=False,
265 read_only=False,
265 read_only=False,
266 logged_in=self.logged_in,
266 logged_in=self.logged_in,
267 login_available=self.login_available,
267 login_available=self.login_available,
268 mathjax_url=self.application.ipython_app.mathjax_url,
268 mathjax_url=self.application.ipython_app.mathjax_url,
269 )
269 )
270
270
271
271
272 class NamedNotebookHandler(AuthenticatedHandler):
272 class NamedNotebookHandler(AuthenticatedHandler):
273
273
274 @authenticate_unless_readonly
274 @authenticate_unless_readonly
275 def get(self, notebook_id):
275 def get(self, notebook_id):
276 nbm = self.application.notebook_manager
276 nbm = self.application.notebook_manager
277 project = nbm.notebook_dir
277 project = nbm.notebook_dir
278 if not nbm.notebook_exists(notebook_id):
278 if not nbm.notebook_exists(notebook_id):
279 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
279 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
280
280
281 self.render(
281 self.render(
282 'notebook.html', project=project,
282 'notebook.html', project=project,
283 notebook_id=notebook_id,
283 notebook_id=notebook_id,
284 base_project_url=self.application.ipython_app.base_project_url,
284 base_project_url=self.application.ipython_app.base_project_url,
285 base_kernel_url=self.application.ipython_app.base_kernel_url,
285 base_kernel_url=self.application.ipython_app.base_kernel_url,
286 kill_kernel=False,
286 kill_kernel=False,
287 read_only=self.read_only,
287 read_only=self.read_only,
288 logged_in=self.logged_in,
288 logged_in=self.logged_in,
289 login_available=self.login_available,
289 login_available=self.login_available,
290 mathjax_url=self.application.ipython_app.mathjax_url,
290 mathjax_url=self.application.ipython_app.mathjax_url,
291 )
291 )
292
292
293
293
294 class PrintNotebookHandler(AuthenticatedHandler):
294 class PrintNotebookHandler(AuthenticatedHandler):
295
295
296 @authenticate_unless_readonly
296 @authenticate_unless_readonly
297 def get(self, notebook_id):
297 def get(self, notebook_id):
298 nbm = self.application.notebook_manager
298 nbm = self.application.notebook_manager
299 project = nbm.notebook_dir
299 project = nbm.notebook_dir
300 if not nbm.notebook_exists(notebook_id):
300 if not nbm.notebook_exists(notebook_id):
301 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
301 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
302
302
303 self.render(
303 self.render(
304 'printnotebook.html', project=project,
304 'printnotebook.html', project=project,
305 notebook_id=notebook_id,
305 notebook_id=notebook_id,
306 base_project_url=self.application.ipython_app.base_project_url,
306 base_project_url=self.application.ipython_app.base_project_url,
307 base_kernel_url=self.application.ipython_app.base_kernel_url,
307 base_kernel_url=self.application.ipython_app.base_kernel_url,
308 kill_kernel=False,
308 kill_kernel=False,
309 read_only=self.read_only,
309 read_only=self.read_only,
310 logged_in=self.logged_in,
310 logged_in=self.logged_in,
311 login_available=self.login_available,
311 login_available=self.login_available,
312 mathjax_url=self.application.ipython_app.mathjax_url,
312 mathjax_url=self.application.ipython_app.mathjax_url,
313 )
313 )
314
314
315 #-----------------------------------------------------------------------------
315 #-----------------------------------------------------------------------------
316 # Kernel handlers
316 # Kernel handlers
317 #-----------------------------------------------------------------------------
317 #-----------------------------------------------------------------------------
318
318
319
319
320 class MainKernelHandler(AuthenticatedHandler):
320 class MainKernelHandler(AuthenticatedHandler):
321
321
322 @web.authenticated
322 @web.authenticated
323 def get(self):
323 def get(self):
324 km = self.application.kernel_manager
324 km = self.application.kernel_manager
325 self.finish(jsonapi.dumps(km.kernel_ids))
325 self.finish(jsonapi.dumps(km.kernel_ids))
326
326
327 @web.authenticated
327 @web.authenticated
328 def post(self):
328 def post(self):
329 km = self.application.kernel_manager
329 km = self.application.kernel_manager
330 notebook_id = self.get_argument('notebook', default=None)
330 notebook_id = self.get_argument('notebook', default=None)
331 kernel_id = km.start_kernel(notebook_id)
331 kernel_id = km.start_kernel(notebook_id)
332 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
332 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
333 self.set_header('Location', '/'+kernel_id)
333 self.set_header('Location', '/'+kernel_id)
334 self.finish(jsonapi.dumps(data))
334 self.finish(jsonapi.dumps(data))
335
335
336
336
337 class KernelHandler(AuthenticatedHandler):
337 class KernelHandler(AuthenticatedHandler):
338
338
339 SUPPORTED_METHODS = ('DELETE')
339 SUPPORTED_METHODS = ('DELETE')
340
340
341 @web.authenticated
341 @web.authenticated
342 def delete(self, kernel_id):
342 def delete(self, kernel_id):
343 km = self.application.kernel_manager
343 km = self.application.kernel_manager
344 km.kill_kernel(kernel_id)
344 km.kill_kernel(kernel_id)
345 self.set_status(204)
345 self.set_status(204)
346 self.finish()
346 self.finish()
347
347
348
348
349 class KernelActionHandler(AuthenticatedHandler):
349 class KernelActionHandler(AuthenticatedHandler):
350
350
351 @web.authenticated
351 @web.authenticated
352 def post(self, kernel_id, action):
352 def post(self, kernel_id, action):
353 km = self.application.kernel_manager
353 km = self.application.kernel_manager
354 if action == 'interrupt':
354 if action == 'interrupt':
355 km.interrupt_kernel(kernel_id)
355 km.interrupt_kernel(kernel_id)
356 self.set_status(204)
356 self.set_status(204)
357 if action == 'restart':
357 if action == 'restart':
358 new_kernel_id = km.restart_kernel(kernel_id)
358 new_kernel_id = km.restart_kernel(kernel_id)
359 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
359 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
360 self.set_header('Location', '/'+new_kernel_id)
360 self.set_header('Location', '/'+new_kernel_id)
361 self.write(jsonapi.dumps(data))
361 self.write(jsonapi.dumps(data))
362 self.finish()
362 self.finish()
363
363
364
364
365 class ZMQStreamHandler(websocket.WebSocketHandler):
365 class ZMQStreamHandler(websocket.WebSocketHandler):
366
366
367 def _reserialize_reply(self, msg_list):
367 def _reserialize_reply(self, msg_list):
368 """Reserialize a reply message using JSON.
368 """Reserialize a reply message using JSON.
369
369
370 This takes the msg list from the ZMQ socket, unserializes it using
370 This takes the msg list from the ZMQ socket, unserializes it using
371 self.session and then serializes the result using JSON. This method
371 self.session and then serializes the result using JSON. This method
372 should be used by self._on_zmq_reply to build messages that can
372 should be used by self._on_zmq_reply to build messages that can
373 be sent back to the browser.
373 be sent back to the browser.
374 """
374 """
375 idents, msg_list = self.session.feed_identities(msg_list)
375 idents, msg_list = self.session.feed_identities(msg_list)
376 msg = self.session.unserialize(msg_list)
376 msg = self.session.unserialize(msg_list)
377 try:
377 try:
378 msg['header'].pop('date')
378 msg['header'].pop('date')
379 except KeyError:
379 except KeyError:
380 pass
380 pass
381 try:
381 try:
382 msg['parent_header'].pop('date')
382 msg['parent_header'].pop('date')
383 except KeyError:
383 except KeyError:
384 pass
384 pass
385 msg.pop('buffers')
385 msg.pop('buffers')
386 return jsonapi.dumps(msg)
386 return jsonapi.dumps(msg)
387
387
388 def _on_zmq_reply(self, msg_list):
388 def _on_zmq_reply(self, msg_list):
389 try:
389 try:
390 msg = self._reserialize_reply(msg_list)
390 msg = self._reserialize_reply(msg_list)
391 except:
391 except:
392 self.application.log.critical("Malformed message: %r" % msg_list)
392 self.application.log.critical("Malformed message: %r" % msg_list)
393 else:
393 else:
394 self.write_message(msg)
394 self.write_message(msg)
395
395
396 def allow_draft76(self):
396 def allow_draft76(self):
397 """Allow draft 76, until browsers such as Safari update to RFC 6455.
397 """Allow draft 76, until browsers such as Safari update to RFC 6455.
398
398
399 This has been disabled by default in tornado in release 2.2.0, and
399 This has been disabled by default in tornado in release 2.2.0, and
400 support will be removed in later versions.
400 support will be removed in later versions.
401 """
401 """
402 return True
402 return True
403
403
404
404
405 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
405 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
406
406
407 def open(self, kernel_id):
407 def open(self, kernel_id):
408 self.kernel_id = kernel_id.decode('ascii')
408 self.kernel_id = kernel_id.decode('ascii')
409 try:
409 try:
410 cfg = self.application.ipython_app.config
410 cfg = self.application.ipython_app.config
411 except AttributeError:
411 except AttributeError:
412 # protect from the case where this is run from something other than
412 # protect from the case where this is run from something other than
413 # the notebook app:
413 # the notebook app:
414 cfg = None
414 cfg = None
415 self.session = Session(config=cfg)
415 self.session = Session(config=cfg)
416 self.save_on_message = self.on_message
416 self.save_on_message = self.on_message
417 self.on_message = self.on_first_message
417 self.on_message = self.on_first_message
418
418
419 def get_current_user(self):
419 def get_current_user(self):
420 user_id = self.get_secure_cookie("username")
420 user_id = self.get_secure_cookie("username")
421 if user_id == '' or (user_id is None and not self.application.password):
421 if user_id == '' or (user_id is None and not self.application.password):
422 user_id = 'anonymous'
422 user_id = 'anonymous'
423 return user_id
423 return user_id
424
424
425 def _inject_cookie_message(self, msg):
425 def _inject_cookie_message(self, msg):
426 """Inject the first message, which is the document cookie,
426 """Inject the first message, which is the document cookie,
427 for authentication."""
427 for authentication."""
428 if isinstance(msg, unicode):
428 if isinstance(msg, unicode):
429 # Cookie can't constructor doesn't accept unicode strings for some reason
429 # Cookie can't constructor doesn't accept unicode strings for some reason
430 msg = msg.encode('utf8', 'replace')
430 msg = msg.encode('utf8', 'replace')
431 try:
431 try:
432 self.request._cookies = Cookie.SimpleCookie(msg)
432 self.request._cookies = Cookie.SimpleCookie(msg)
433 except:
433 except:
434 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
434 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
435
435
436 def on_first_message(self, msg):
436 def on_first_message(self, msg):
437 self._inject_cookie_message(msg)
437 self._inject_cookie_message(msg)
438 if self.get_current_user() is None:
438 if self.get_current_user() is None:
439 logging.warn("Couldn't authenticate WebSocket connection")
439 logging.warn("Couldn't authenticate WebSocket connection")
440 raise web.HTTPError(403)
440 raise web.HTTPError(403)
441 self.on_message = self.save_on_message
441 self.on_message = self.save_on_message
442
442
443
443
444 class IOPubHandler(AuthenticatedZMQStreamHandler):
444 class IOPubHandler(AuthenticatedZMQStreamHandler):
445
445
446 def initialize(self, *args, **kwargs):
446 def initialize(self, *args, **kwargs):
447 self._kernel_alive = True
447 self._kernel_alive = True
448 self._beating = False
448 self._beating = False
449 self.iopub_stream = None
449 self.iopub_stream = None
450 self.hb_stream = None
450 self.hb_stream = None
451
451
452 def on_first_message(self, msg):
452 def on_first_message(self, msg):
453 try:
453 try:
454 super(IOPubHandler, self).on_first_message(msg)
454 super(IOPubHandler, self).on_first_message(msg)
455 except web.HTTPError:
455 except web.HTTPError:
456 self.close()
456 self.close()
457 return
457 return
458 km = self.application.kernel_manager
458 km = self.application.kernel_manager
459 self.time_to_dead = km.time_to_dead
459 self.time_to_dead = km.time_to_dead
460 self.first_beat = km.first_beat
460 self.first_beat = km.first_beat
461 kernel_id = self.kernel_id
461 kernel_id = self.kernel_id
462 try:
462 try:
463 self.iopub_stream = km.create_iopub_stream(kernel_id)
463 self.iopub_stream = km.create_iopub_stream(kernel_id)
464 self.hb_stream = km.create_hb_stream(kernel_id)
464 self.hb_stream = km.create_hb_stream(kernel_id)
465 except web.HTTPError:
465 except web.HTTPError:
466 # WebSockets don't response to traditional error codes so we
466 # WebSockets don't response to traditional error codes so we
467 # close the connection.
467 # close the connection.
468 if not self.stream.closed():
468 if not self.stream.closed():
469 self.stream.close()
469 self.stream.close()
470 self.close()
470 self.close()
471 else:
471 else:
472 self.iopub_stream.on_recv(self._on_zmq_reply)
472 self.iopub_stream.on_recv(self._on_zmq_reply)
473 self.start_hb(self.kernel_died)
473 self.start_hb(self.kernel_died)
474
474
475 def on_message(self, msg):
475 def on_message(self, msg):
476 pass
476 pass
477
477
478 def on_close(self):
478 def on_close(self):
479 # This method can be called twice, once by self.kernel_died and once
479 # This method can be called twice, once by self.kernel_died and once
480 # from the WebSocket close event. If the WebSocket connection is
480 # from the WebSocket close event. If the WebSocket connection is
481 # closed before the ZMQ streams are setup, they could be None.
481 # closed before the ZMQ streams are setup, they could be None.
482 self.stop_hb()
482 self.stop_hb()
483 if self.iopub_stream is not None and not self.iopub_stream.closed():
483 if self.iopub_stream is not None and not self.iopub_stream.closed():
484 self.iopub_stream.on_recv(None)
484 self.iopub_stream.on_recv(None)
485 self.iopub_stream.close()
485 self.iopub_stream.close()
486 if self.hb_stream is not None and not self.hb_stream.closed():
486 if self.hb_stream is not None and not self.hb_stream.closed():
487 self.hb_stream.close()
487 self.hb_stream.close()
488
488
489 def start_hb(self, callback):
489 def start_hb(self, callback):
490 """Start the heartbeating and call the callback if the kernel dies."""
490 """Start the heartbeating and call the callback if the kernel dies."""
491 if not self._beating:
491 if not self._beating:
492 self._kernel_alive = True
492 self._kernel_alive = True
493
493
494 def ping_or_dead():
494 def ping_or_dead():
495 self.hb_stream.flush()
495 self.hb_stream.flush()
496 if self._kernel_alive:
496 if self._kernel_alive:
497 self._kernel_alive = False
497 self._kernel_alive = False
498 self.hb_stream.send(b'ping')
498 self.hb_stream.send(b'ping')
499 # flush stream to force immediate socket send
499 # flush stream to force immediate socket send
500 self.hb_stream.flush()
500 self.hb_stream.flush()
501 else:
501 else:
502 try:
502 try:
503 callback()
503 callback()
504 except:
504 except:
505 pass
505 pass
506 finally:
506 finally:
507 self.stop_hb()
507 self.stop_hb()
508
508
509 def beat_received(msg):
509 def beat_received(msg):
510 self._kernel_alive = True
510 self._kernel_alive = True
511
511
512 self.hb_stream.on_recv(beat_received)
512 self.hb_stream.on_recv(beat_received)
513 loop = ioloop.IOLoop.instance()
513 loop = ioloop.IOLoop.instance()
514 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
514 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
515 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
515 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
516 self._beating= True
516 self._beating= True
517
517
518 def _really_start_hb(self):
518 def _really_start_hb(self):
519 """callback for delayed heartbeat start
519 """callback for delayed heartbeat start
520
520
521 Only start the hb loop if we haven't been closed during the wait.
521 Only start the hb loop if we haven't been closed during the wait.
522 """
522 """
523 if self._beating and not self.hb_stream.closed():
523 if self._beating and not self.hb_stream.closed():
524 self._hb_periodic_callback.start()
524 self._hb_periodic_callback.start()
525
525
526 def stop_hb(self):
526 def stop_hb(self):
527 """Stop the heartbeating and cancel all related callbacks."""
527 """Stop the heartbeating and cancel all related callbacks."""
528 if self._beating:
528 if self._beating:
529 self._beating = False
529 self._beating = False
530 self._hb_periodic_callback.stop()
530 self._hb_periodic_callback.stop()
531 if not self.hb_stream.closed():
531 if not self.hb_stream.closed():
532 self.hb_stream.on_recv(None)
532 self.hb_stream.on_recv(None)
533
533
534 def kernel_died(self):
534 def kernel_died(self):
535 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
535 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
536 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
536 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
537 self.write_message(
537 self.write_message(
538 {'header': {'msg_type': 'status'},
538 {'header': {'msg_type': 'status'},
539 'parent_header': {},
539 'parent_header': {},
540 'content': {'execution_state':'dead'}
540 'content': {'execution_state':'dead'}
541 }
541 }
542 )
542 )
543 self.on_close()
543 self.on_close()
544
544
545
545
546 class ShellHandler(AuthenticatedZMQStreamHandler):
546 class ShellHandler(AuthenticatedZMQStreamHandler):
547
547
548 def initialize(self, *args, **kwargs):
548 def initialize(self, *args, **kwargs):
549 self.shell_stream = None
549 self.shell_stream = None
550
550
551 def on_first_message(self, msg):
551 def on_first_message(self, msg):
552 try:
552 try:
553 super(ShellHandler, self).on_first_message(msg)
553 super(ShellHandler, self).on_first_message(msg)
554 except web.HTTPError:
554 except web.HTTPError:
555 self.close()
555 self.close()
556 return
556 return
557 km = self.application.kernel_manager
557 km = self.application.kernel_manager
558 self.max_msg_size = km.max_msg_size
558 self.max_msg_size = km.max_msg_size
559 kernel_id = self.kernel_id
559 kernel_id = self.kernel_id
560 try:
560 try:
561 self.shell_stream = km.create_shell_stream(kernel_id)
561 self.shell_stream = km.create_shell_stream(kernel_id)
562 except web.HTTPError:
562 except web.HTTPError:
563 # WebSockets don't response to traditional error codes so we
563 # WebSockets don't response to traditional error codes so we
564 # close the connection.
564 # close the connection.
565 if not self.stream.closed():
565 if not self.stream.closed():
566 self.stream.close()
566 self.stream.close()
567 self.close()
567 self.close()
568 else:
568 else:
569 self.shell_stream.on_recv(self._on_zmq_reply)
569 self.shell_stream.on_recv(self._on_zmq_reply)
570
570
571 def on_message(self, msg):
571 def on_message(self, msg):
572 if len(msg) < self.max_msg_size:
572 if len(msg) < self.max_msg_size:
573 msg = jsonapi.loads(msg)
573 msg = jsonapi.loads(msg)
574 self.session.send(self.shell_stream, msg)
574 self.session.send(self.shell_stream, msg)
575
575
576 def on_close(self):
576 def on_close(self):
577 # Make sure the stream exists and is not already closed.
577 # Make sure the stream exists and is not already closed.
578 if self.shell_stream is not None and not self.shell_stream.closed():
578 if self.shell_stream is not None and not self.shell_stream.closed():
579 self.shell_stream.close()
579 self.shell_stream.close()
580
580
581
581
582 #-----------------------------------------------------------------------------
582 #-----------------------------------------------------------------------------
583 # Notebook web service handlers
583 # Notebook web service handlers
584 #-----------------------------------------------------------------------------
584 #-----------------------------------------------------------------------------
585
585
586 class NotebookRootHandler(AuthenticatedHandler):
586 class NotebookRootHandler(AuthenticatedHandler):
587
587
588 @authenticate_unless_readonly
588 @authenticate_unless_readonly
589 def get(self):
589 def get(self):
590
591 nbm = self.application.notebook_manager
590 nbm = self.application.notebook_manager
592 files = nbm.list_notebooks()
591 files = nbm.list_notebooks()
593 self.finish(jsonapi.dumps(files))
592 self.finish(jsonapi.dumps(files))
594
593
595 @web.authenticated
594 @web.authenticated
596 def post(self):
595 def post(self):
597 nbm = self.application.notebook_manager
596 nbm = self.application.notebook_manager
598 body = self.request.body.strip()
597 body = self.request.body.strip()
599 format = self.get_argument('format', default='json')
598 format = self.get_argument('format', default='json')
600 name = self.get_argument('name', default=None)
599 name = self.get_argument('name', default=None)
601 if body:
600 if body:
602 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
601 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
603 else:
602 else:
604 notebook_id = nbm.new_notebook()
603 notebook_id = nbm.new_notebook()
605 self.set_header('Location', '/'+notebook_id)
604 self.set_header('Location', '/'+notebook_id)
606 self.finish(jsonapi.dumps(notebook_id))
605 self.finish(jsonapi.dumps(notebook_id))
607
606
608
607
609 class NotebookHandler(AuthenticatedHandler):
608 class NotebookHandler(AuthenticatedHandler):
610
609
611 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
610 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
612
611
613 @authenticate_unless_readonly
612 @authenticate_unless_readonly
614 def get(self, notebook_id):
613 def get(self, notebook_id):
615 nbm = self.application.notebook_manager
614 nbm = self.application.notebook_manager
616 format = self.get_argument('format', default='json')
615 format = self.get_argument('format', default='json')
617 last_mod, name, data = nbm.get_notebook(notebook_id, format)
616 last_mod, name, data = nbm.get_notebook(notebook_id, format)
618
617
619 if format == u'json':
618 if format == u'json':
620 self.set_header('Content-Type', 'application/json')
619 self.set_header('Content-Type', 'application/json')
621 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
620 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
622 elif format == u'py':
621 elif format == u'py':
623 self.set_header('Content-Type', 'application/x-python')
622 self.set_header('Content-Type', 'application/x-python')
624 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
623 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
625 self.set_header('Last-Modified', last_mod)
624 self.set_header('Last-Modified', last_mod)
626 self.finish(data)
625 self.finish(data)
627
626
628 @web.authenticated
627 @web.authenticated
629 def put(self, notebook_id):
628 def put(self, notebook_id):
630 nbm = self.application.notebook_manager
629 nbm = self.application.notebook_manager
631 format = self.get_argument('format', default='json')
630 format = self.get_argument('format', default='json')
632 name = self.get_argument('name', default=None)
631 name = self.get_argument('name', default=None)
633 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
632 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
634 self.set_status(204)
633 self.set_status(204)
635 self.finish()
634 self.finish()
636
635
637 @web.authenticated
636 @web.authenticated
638 def delete(self, notebook_id):
637 def delete(self, notebook_id):
639 nbm = self.application.notebook_manager
638 nbm = self.application.notebook_manager
640 nbm.delete_notebook(notebook_id)
639 nbm.delete_notebook(notebook_id)
641 self.set_status(204)
640 self.set_status(204)
642 self.finish()
641 self.finish()
643
642
644
643
645 class NotebookCopyHandler(AuthenticatedHandler):
644 class NotebookCopyHandler(AuthenticatedHandler):
646
645
647 @web.authenticated
646 @web.authenticated
648 def get(self, notebook_id):
647 def get(self, notebook_id):
649 nbm = self.application.notebook_manager
648 nbm = self.application.notebook_manager
650 project = nbm.notebook_dir
649 project = nbm.notebook_dir
651 notebook_id = nbm.copy_notebook(notebook_id)
650 notebook_id = nbm.copy_notebook(notebook_id)
652 self.render(
651 self.render(
653 'notebook.html', project=project,
652 'notebook.html', project=project,
654 notebook_id=notebook_id,
653 notebook_id=notebook_id,
655 base_project_url=self.application.ipython_app.base_project_url,
654 base_project_url=self.application.ipython_app.base_project_url,
656 base_kernel_url=self.application.ipython_app.base_kernel_url,
655 base_kernel_url=self.application.ipython_app.base_kernel_url,
657 kill_kernel=False,
656 kill_kernel=False,
658 read_only=False,
657 read_only=False,
659 logged_in=self.logged_in,
658 logged_in=self.logged_in,
660 login_available=self.login_available,
659 login_available=self.login_available,
661 mathjax_url=self.application.ipython_app.mathjax_url,
660 mathjax_url=self.application.ipython_app.mathjax_url,
662 )
661 )
663
662
663
664 #-----------------------------------------------------------------------------
665 # Cluster handlers
666 #-----------------------------------------------------------------------------
667
668
669 class MainClusterHandler(AuthenticatedHandler):
670
671 @web.authenticated
672 def get(self):
673 cm = self.application.cluster_manager
674 self.finish(jsonapi.dumps(cm.list_profiles()))
675
676
677 class ClusterProfileHandler(AuthenticatedHandler):
678
679 @web.authenticated
680 def get(self, profile):
681 cm = self.application.cluster_manager
682 self.finish(jsonapi.dumps(cm.profile_info(profile)))
683
684
685 class ClusterActionHandler(AuthenticatedHandler):
686
687 @web.authenticated
688 def post(self, profile, action):
689 cm = self.application.cluster_manager
690 if action == 'start':
691 n = int(self.get_argument('n', default=4))
692 cm.start_cluster(profile, n)
693 if action == 'stop':
694 cm.stop_cluster(profile)
695 self.finish()
696
697
664 #-----------------------------------------------------------------------------
698 #-----------------------------------------------------------------------------
665 # RST web service handlers
699 # RST web service handlers
666 #-----------------------------------------------------------------------------
700 #-----------------------------------------------------------------------------
667
701
668
702
669 class RSTHandler(AuthenticatedHandler):
703 class RSTHandler(AuthenticatedHandler):
670
704
671 @web.authenticated
705 @web.authenticated
672 def post(self):
706 def post(self):
673 if publish_string is None:
707 if publish_string is None:
674 raise web.HTTPError(503, u'docutils not available')
708 raise web.HTTPError(503, u'docutils not available')
675 body = self.request.body.strip()
709 body = self.request.body.strip()
676 source = body
710 source = body
677 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
711 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
678 defaults = {'file_insertion_enabled': 0,
712 defaults = {'file_insertion_enabled': 0,
679 'raw_enabled': 0,
713 'raw_enabled': 0,
680 '_disable_config': 1,
714 '_disable_config': 1,
681 'stylesheet_path': 0
715 'stylesheet_path': 0
682 # 'template': template_path
716 # 'template': template_path
683 }
717 }
684 try:
718 try:
685 html = publish_string(source, writer_name='html',
719 html = publish_string(source, writer_name='html',
686 settings_overrides=defaults
720 settings_overrides=defaults
687 )
721 )
688 except:
722 except:
689 raise web.HTTPError(400, u'Invalid RST')
723 raise web.HTTPError(400, u'Invalid RST')
690 print html
724 print html
691 self.set_header('Content-Type', 'text/html')
725 self.set_header('Content-Type', 'text/html')
692 self.finish(html)
726 self.finish(html)
693
727
694
728
@@ -1,491 +1,503 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 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 signal
23 import signal
24 import socket
24 import socket
25 import sys
25 import sys
26 import threading
26 import threading
27 import webbrowser
27 import webbrowser
28
28
29 # Third party
29 # Third party
30 import zmq
30 import zmq
31
31
32 # Install the pyzmq ioloop. This has to be done before anything else from
32 # Install the pyzmq ioloop. This has to be done before anything else from
33 # tornado is imported.
33 # tornado is imported.
34 from zmq.eventloop import ioloop
34 from zmq.eventloop import ioloop
35 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
35 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
36 # when pyzmq dependency is updated beyond that.
36 # when pyzmq dependency is updated beyond that.
37 if hasattr(ioloop, 'install'):
37 if hasattr(ioloop, 'install'):
38 ioloop.install()
38 ioloop.install()
39 else:
39 else:
40 import tornado.ioloop
40 import tornado.ioloop
41 tornado.ioloop.IOLoop = ioloop.IOLoop
41 tornado.ioloop.IOLoop = ioloop.IOLoop
42
42
43 from tornado import httpserver
43 from tornado import httpserver
44 from tornado import web
44 from tornado import web
45
45
46 # Our own libraries
46 # Our own libraries
47 from .kernelmanager import MappingKernelManager
47 from .kernelmanager import MappingKernelManager
48 from .handlers import (LoginHandler, LogoutHandler,
48 from .handlers import (LoginHandler, LogoutHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler
52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
53 )
54 )
54 from .notebookmanager import NotebookManager
55 from .notebookmanager import NotebookManager
56 from .clustermanager import ClusterManager
55
57
56 from IPython.config.application import catch_config_error, boolean_flag
58 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.core.application import BaseIPythonApplication
59 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.profiledir import ProfileDir
60 from IPython.core.profiledir import ProfileDir
59 from IPython.lib.kernel import swallow_argv
61 from IPython.lib.kernel import swallow_argv
60 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.session import Session, default_secure
61 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.zmqshell import ZMQInteractiveShell
62 from IPython.zmq.ipkernel import (
64 from IPython.zmq.ipkernel import (
63 flags as ipkernel_flags,
65 flags as ipkernel_flags,
64 aliases as ipkernel_aliases,
66 aliases as ipkernel_aliases,
65 IPKernelApp
67 IPKernelApp
66 )
68 )
67 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
68 from IPython.utils import py3compat
70 from IPython.utils import py3compat
69
71
70 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
71 # Module globals
73 # Module globals
72 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
73
75
74 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
75 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 _kernel_action_regex = r"(?P<action>restart|interrupt)"
76 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
79 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
80 _cluster_action_regex = r"(?P<action>start|stop)"
81
77
82
78 LOCALHOST = '127.0.0.1'
83 LOCALHOST = '127.0.0.1'
79
84
80 _examples = """
85 _examples = """
81 ipython notebook # start the notebook
86 ipython notebook # start the notebook
82 ipython notebook --profile=sympy # use the sympy profile
87 ipython notebook --profile=sympy # use the sympy profile
83 ipython notebook --pylab=inline # pylab in inline plotting mode
88 ipython notebook --pylab=inline # pylab in inline plotting mode
84 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
85 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
86 """
91 """
87
92
88 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
89 # Helper functions
94 # Helper functions
90 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
91
96
92 def url_path_join(a,b):
97 def url_path_join(a,b):
93 if a.endswith('/') and b.startswith('/'):
98 if a.endswith('/') and b.startswith('/'):
94 return a[:-1]+b
99 return a[:-1]+b
95 else:
100 else:
96 return a+b
101 return a+b
97
102
98 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
99 # The Tornado web application
104 # The Tornado web application
100 #-----------------------------------------------------------------------------
105 #-----------------------------------------------------------------------------
101
106
102 class NotebookWebApplication(web.Application):
107 class NotebookWebApplication(web.Application):
103
108
104 def __init__(self, ipython_app, kernel_manager, notebook_manager, log,
109 def __init__(self, ipython_app, kernel_manager, notebook_manager,
110 cluster_manager, log,
105 base_project_url, settings_overrides):
111 base_project_url, settings_overrides):
106 handlers = [
112 handlers = [
107 (r"/", ProjectDashboardHandler),
113 (r"/", ProjectDashboardHandler),
108 (r"/login", LoginHandler),
114 (r"/login", LoginHandler),
109 (r"/logout", LogoutHandler),
115 (r"/logout", LogoutHandler),
110 (r"/new", NewHandler),
116 (r"/new", NewHandler),
111 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
117 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
112 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
118 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
113 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
119 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
114 (r"/kernels", MainKernelHandler),
120 (r"/kernels", MainKernelHandler),
115 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
121 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
116 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
122 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
117 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
123 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
118 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
124 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
119 (r"/notebooks", NotebookRootHandler),
125 (r"/notebooks", NotebookRootHandler),
120 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
126 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
121 (r"/rstservice/render", RSTHandler),
127 (r"/rstservice/render", RSTHandler),
122 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
128 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
129 (r"/clusters", MainClusterHandler),
130 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
131 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
123 ]
132 ]
124 settings = dict(
133 settings = dict(
125 template_path=os.path.join(os.path.dirname(__file__), "templates"),
134 template_path=os.path.join(os.path.dirname(__file__), "templates"),
126 static_path=os.path.join(os.path.dirname(__file__), "static"),
135 static_path=os.path.join(os.path.dirname(__file__), "static"),
127 cookie_secret=os.urandom(1024),
136 cookie_secret=os.urandom(1024),
128 login_url="/login",
137 login_url="/login",
129 )
138 )
130
139
131 # allow custom overrides for the tornado web app.
140 # allow custom overrides for the tornado web app.
132 settings.update(settings_overrides)
141 settings.update(settings_overrides)
133
142
134 # 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
135 # base_project_url will always be unicode, which will in turn
144 # base_project_url will always be unicode, which will in turn
136 # make the patterns unicode, and ultimately result in unicode
145 # make the patterns unicode, and ultimately result in unicode
137 # keys in kwargs to handler._execute(**kwargs) in tornado.
146 # keys in kwargs to handler._execute(**kwargs) in tornado.
138 # This enforces that base_project_url be ascii in that situation.
147 # This enforces that base_project_url be ascii in that situation.
139 #
148 #
140 # Note that the URLs these patterns check against are escaped,
149 # Note that the URLs these patterns check against are escaped,
141 # 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'.
142 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
151 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
143
152
144 # prepend base_project_url onto the patterns that we match
153 # prepend base_project_url onto the patterns that we match
145 new_handlers = []
154 new_handlers = []
146 for handler in handlers:
155 for handler in handlers:
147 pattern = url_path_join(base_project_url, handler[0])
156 pattern = url_path_join(base_project_url, handler[0])
148 new_handler = tuple([pattern]+list(handler[1:]))
157 new_handler = tuple([pattern]+list(handler[1:]))
149 new_handlers.append( new_handler )
158 new_handlers.append( new_handler )
150
159
151 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
160 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
152
161
153 self.kernel_manager = kernel_manager
162 self.kernel_manager = kernel_manager
154 self.log = log
155 self.notebook_manager = notebook_manager
163 self.notebook_manager = notebook_manager
164 self.cluster_manager = cluster_manager
156 self.ipython_app = ipython_app
165 self.ipython_app = ipython_app
157 self.read_only = self.ipython_app.read_only
166 self.read_only = self.ipython_app.read_only
167 self.log = log
158
168
159
169
160 #-----------------------------------------------------------------------------
170 #-----------------------------------------------------------------------------
161 # Aliases and Flags
171 # Aliases and Flags
162 #-----------------------------------------------------------------------------
172 #-----------------------------------------------------------------------------
163
173
164 flags = dict(ipkernel_flags)
174 flags = dict(ipkernel_flags)
165 flags['no-browser']=(
175 flags['no-browser']=(
166 {'NotebookApp' : {'open_browser' : False}},
176 {'NotebookApp' : {'open_browser' : False}},
167 "Don't open the notebook in a browser after startup."
177 "Don't open the notebook in a browser after startup."
168 )
178 )
169 flags['no-mathjax']=(
179 flags['no-mathjax']=(
170 {'NotebookApp' : {'enable_mathjax' : False}},
180 {'NotebookApp' : {'enable_mathjax' : False}},
171 """Disable MathJax
181 """Disable MathJax
172
182
173 MathJax is the javascript library IPython uses to render math/LaTeX. It is
183 MathJax is the javascript library IPython uses to render math/LaTeX. It is
174 very large, so you may want to disable it if you have a slow internet
184 very large, so you may want to disable it if you have a slow internet
175 connection, or for offline use of the notebook.
185 connection, or for offline use of the notebook.
176
186
177 When disabled, equations etc. will appear as their untransformed TeX source.
187 When disabled, equations etc. will appear as their untransformed TeX source.
178 """
188 """
179 )
189 )
180 flags['read-only'] = (
190 flags['read-only'] = (
181 {'NotebookApp' : {'read_only' : True}},
191 {'NotebookApp' : {'read_only' : True}},
182 """Allow read-only access to notebooks.
192 """Allow read-only access to notebooks.
183
193
184 When using a password to protect the notebook server, this flag
194 When using a password to protect the notebook server, this flag
185 allows unauthenticated clients to view the notebook list, and
195 allows unauthenticated clients to view the notebook list, and
186 individual notebooks, but not edit them, start kernels, or run
196 individual notebooks, but not edit them, start kernels, or run
187 code.
197 code.
188
198
189 If no password is set, the server will be entirely read-only.
199 If no password is set, the server will be entirely read-only.
190 """
200 """
191 )
201 )
192
202
193 # Add notebook manager flags
203 # Add notebook manager flags
194 flags.update(boolean_flag('script', 'NotebookManager.save_script',
204 flags.update(boolean_flag('script', 'NotebookManager.save_script',
195 'Auto-save a .py script everytime the .ipynb notebook is saved',
205 'Auto-save a .py script everytime the .ipynb notebook is saved',
196 'Do not auto-save .py scripts for every notebook'))
206 'Do not auto-save .py scripts for every notebook'))
197
207
198 # the flags that are specific to the frontend
208 # the flags that are specific to the frontend
199 # these must be scrubbed before being passed to the kernel,
209 # these must be scrubbed before being passed to the kernel,
200 # or it will raise an error on unrecognized flags
210 # or it will raise an error on unrecognized flags
201 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
211 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
202
212
203 aliases = dict(ipkernel_aliases)
213 aliases = dict(ipkernel_aliases)
204
214
205 aliases.update({
215 aliases.update({
206 'ip': 'NotebookApp.ip',
216 'ip': 'NotebookApp.ip',
207 'port': 'NotebookApp.port',
217 'port': 'NotebookApp.port',
208 'keyfile': 'NotebookApp.keyfile',
218 'keyfile': 'NotebookApp.keyfile',
209 'certfile': 'NotebookApp.certfile',
219 'certfile': 'NotebookApp.certfile',
210 'notebook-dir': 'NotebookManager.notebook_dir',
220 'notebook-dir': 'NotebookManager.notebook_dir',
211 'browser': 'NotebookApp.browser',
221 'browser': 'NotebookApp.browser',
212 })
222 })
213
223
214 # remove ipkernel flags that are singletons, and don't make sense in
224 # remove ipkernel flags that are singletons, and don't make sense in
215 # multi-kernel evironment:
225 # multi-kernel evironment:
216 aliases.pop('f', None)
226 aliases.pop('f', None)
217
227
218 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
228 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
219 u'notebook-dir']
229 u'notebook-dir']
220
230
221 #-----------------------------------------------------------------------------
231 #-----------------------------------------------------------------------------
222 # NotebookApp
232 # NotebookApp
223 #-----------------------------------------------------------------------------
233 #-----------------------------------------------------------------------------
224
234
225 class NotebookApp(BaseIPythonApplication):
235 class NotebookApp(BaseIPythonApplication):
226
236
227 name = 'ipython-notebook'
237 name = 'ipython-notebook'
228 default_config_file_name='ipython_notebook_config.py'
238 default_config_file_name='ipython_notebook_config.py'
229
239
230 description = """
240 description = """
231 The IPython HTML Notebook.
241 The IPython HTML Notebook.
232
242
233 This launches a Tornado based HTML Notebook Server that serves up an
243 This launches a Tornado based HTML Notebook Server that serves up an
234 HTML5/Javascript Notebook client.
244 HTML5/Javascript Notebook client.
235 """
245 """
236 examples = _examples
246 examples = _examples
237
247
238 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
248 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
239 MappingKernelManager, NotebookManager]
249 MappingKernelManager, NotebookManager]
240 flags = Dict(flags)
250 flags = Dict(flags)
241 aliases = Dict(aliases)
251 aliases = Dict(aliases)
242
252
243 kernel_argv = List(Unicode)
253 kernel_argv = List(Unicode)
244
254
245 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
255 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
246 default_value=logging.INFO,
256 default_value=logging.INFO,
247 config=True,
257 config=True,
248 help="Set the log level by value or name.")
258 help="Set the log level by value or name.")
249
259
250 # create requested profiles by default, if they don't exist:
260 # create requested profiles by default, if they don't exist:
251 auto_create = Bool(True)
261 auto_create = Bool(True)
252
262
253 # Network related information.
263 # Network related information.
254
264
255 ip = Unicode(LOCALHOST, config=True,
265 ip = Unicode(LOCALHOST, config=True,
256 help="The IP address the notebook server will listen on."
266 help="The IP address the notebook server will listen on."
257 )
267 )
258
268
259 def _ip_changed(self, name, old, new):
269 def _ip_changed(self, name, old, new):
260 if new == u'*': self.ip = u''
270 if new == u'*': self.ip = u''
261
271
262 port = Integer(8888, config=True,
272 port = Integer(8888, config=True,
263 help="The port the notebook server will listen on."
273 help="The port the notebook server will listen on."
264 )
274 )
265
275
266 certfile = Unicode(u'', config=True,
276 certfile = Unicode(u'', config=True,
267 help="""The full path to an SSL/TLS certificate file."""
277 help="""The full path to an SSL/TLS certificate file."""
268 )
278 )
269
279
270 keyfile = Unicode(u'', config=True,
280 keyfile = Unicode(u'', config=True,
271 help="""The full path to a private key file for usage with SSL/TLS."""
281 help="""The full path to a private key file for usage with SSL/TLS."""
272 )
282 )
273
283
274 password = Unicode(u'', config=True,
284 password = Unicode(u'', config=True,
275 help="""Hashed password to use for web authentication.
285 help="""Hashed password to use for web authentication.
276
286
277 To generate, type in a python/IPython shell:
287 To generate, type in a python/IPython shell:
278
288
279 from IPython.lib import passwd; passwd()
289 from IPython.lib import passwd; passwd()
280
290
281 The string should be of the form type:salt:hashed-password.
291 The string should be of the form type:salt:hashed-password.
282 """
292 """
283 )
293 )
284
294
285 open_browser = Bool(True, config=True,
295 open_browser = Bool(True, config=True,
286 help="""Whether to open in a browser after starting.
296 help="""Whether to open in a browser after starting.
287 The specific browser used is platform dependent and
297 The specific browser used is platform dependent and
288 determined by the python standard library `webbrowser`
298 determined by the python standard library `webbrowser`
289 module, unless it is overridden using the --browser
299 module, unless it is overridden using the --browser
290 (NotebookApp.browser) configuration option.
300 (NotebookApp.browser) configuration option.
291 """)
301 """)
292
302
293 browser = Unicode(u'', config=True,
303 browser = Unicode(u'', config=True,
294 help="""Specify what command to use to invoke a web
304 help="""Specify what command to use to invoke a web
295 browser when opening the notebook. If not specified, the
305 browser when opening the notebook. If not specified, the
296 default browser will be determined by the `webbrowser`
306 default browser will be determined by the `webbrowser`
297 standard library module, which allows setting of the
307 standard library module, which allows setting of the
298 BROWSER environment variable to override it.
308 BROWSER environment variable to override it.
299 """)
309 """)
300
310
301 read_only = Bool(False, config=True,
311 read_only = Bool(False, config=True,
302 help="Whether to prevent editing/execution of notebooks."
312 help="Whether to prevent editing/execution of notebooks."
303 )
313 )
304
314
305 webapp_settings = Dict(config=True,
315 webapp_settings = Dict(config=True,
306 help="Supply overrides for the tornado.web.Application that the "
316 help="Supply overrides for the tornado.web.Application that the "
307 "IPython notebook uses.")
317 "IPython notebook uses.")
308
318
309 enable_mathjax = Bool(True, config=True,
319 enable_mathjax = Bool(True, config=True,
310 help="""Whether to enable MathJax for typesetting math/TeX
320 help="""Whether to enable MathJax for typesetting math/TeX
311
321
312 MathJax is the javascript library IPython uses to render math/LaTeX. It is
322 MathJax is the javascript library IPython uses to render math/LaTeX. It is
313 very large, so you may want to disable it if you have a slow internet
323 very large, so you may want to disable it if you have a slow internet
314 connection, or for offline use of the notebook.
324 connection, or for offline use of the notebook.
315
325
316 When disabled, equations etc. will appear as their untransformed TeX source.
326 When disabled, equations etc. will appear as their untransformed TeX source.
317 """
327 """
318 )
328 )
319 def _enable_mathjax_changed(self, name, old, new):
329 def _enable_mathjax_changed(self, name, old, new):
320 """set mathjax url to empty if mathjax is disabled"""
330 """set mathjax url to empty if mathjax is disabled"""
321 if not new:
331 if not new:
322 self.mathjax_url = u''
332 self.mathjax_url = u''
323
333
324 base_project_url = Unicode('/', config=True,
334 base_project_url = Unicode('/', config=True,
325 help='''The base URL for the notebook server''')
335 help='''The base URL for the notebook server''')
326 base_kernel_url = Unicode('/', config=True,
336 base_kernel_url = Unicode('/', config=True,
327 help='''The base URL for the kernel server''')
337 help='''The base URL for the kernel server''')
328 websocket_host = Unicode("", config=True,
338 websocket_host = Unicode("", config=True,
329 help="""The hostname for the websocket server."""
339 help="""The hostname for the websocket server."""
330 )
340 )
331
341
332 mathjax_url = Unicode("", config=True,
342 mathjax_url = Unicode("", config=True,
333 help="""The url for MathJax.js."""
343 help="""The url for MathJax.js."""
334 )
344 )
335 def _mathjax_url_default(self):
345 def _mathjax_url_default(self):
336 if not self.enable_mathjax:
346 if not self.enable_mathjax:
337 return u''
347 return u''
338 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
348 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
339 static_url_prefix = self.webapp_settings.get("static_url_prefix",
349 static_url_prefix = self.webapp_settings.get("static_url_prefix",
340 "/static/")
350 "/static/")
341 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
351 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
342 self.log.info("Using local MathJax")
352 self.log.info("Using local MathJax")
343 return static_url_prefix+u"mathjax/MathJax.js"
353 return static_url_prefix+u"mathjax/MathJax.js"
344 else:
354 else:
345 self.log.info("Using MathJax from CDN")
355 self.log.info("Using MathJax from CDN")
346 hostname = "cdn.mathjax.org"
356 hostname = "cdn.mathjax.org"
347 try:
357 try:
348 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
358 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
349 # only works on *.cloudfront.net
359 # only works on *.cloudfront.net
350 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
360 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
351 # I've run this on a few machines, and some put the right answer in true_host,
361 # I've run this on a few machines, and some put the right answer in true_host,
352 # while others gave it in the aliases list, so we check both.
362 # while others gave it in the aliases list, so we check both.
353 aliases.insert(0, true_host)
363 aliases.insert(0, true_host)
354 except Exception:
364 except Exception:
355 self.log.warn("Couldn't determine MathJax CDN info")
365 self.log.warn("Couldn't determine MathJax CDN info")
356 else:
366 else:
357 for alias in aliases:
367 for alias in aliases:
358 parts = alias.split('.')
368 parts = alias.split('.')
359 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
369 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
360 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
370 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
361 hostname = alias
371 hostname = alias
362 break
372 break
363
373
364 if not hostname.endswith(".cloudfront.net"):
374 if not hostname.endswith(".cloudfront.net"):
365 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
375 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
366 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
376 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
367 self.log.error("For unsecured HTTP access to MathJax use config:")
377 self.log.error("For unsecured HTTP access to MathJax use config:")
368 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
378 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
369 return u"https://%s/mathjax/latest/MathJax.js" % hostname
379 return u"https://%s/mathjax/latest/MathJax.js" % hostname
370
380
371 def _mathjax_url_changed(self, name, old, new):
381 def _mathjax_url_changed(self, name, old, new):
372 if new and not self.enable_mathjax:
382 if new and not self.enable_mathjax:
373 # enable_mathjax=False overrides mathjax_url
383 # enable_mathjax=False overrides mathjax_url
374 self.mathjax_url = u''
384 self.mathjax_url = u''
375 else:
385 else:
376 self.log.info("Using MathJax: %s", new)
386 self.log.info("Using MathJax: %s", new)
377
387
378 def parse_command_line(self, argv=None):
388 def parse_command_line(self, argv=None):
379 super(NotebookApp, self).parse_command_line(argv)
389 super(NotebookApp, self).parse_command_line(argv)
380 if argv is None:
390 if argv is None:
381 argv = sys.argv[1:]
391 argv = sys.argv[1:]
382
392
383 # Scrub frontend-specific flags
393 # Scrub frontend-specific flags
384 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
385 # Kernel should inherit default config file from frontend
395 # Kernel should inherit default config file from frontend
386 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
387
397
388 def init_configurables(self):
398 def init_configurables(self):
389 # force Session default to be secure
399 # force Session default to be secure
390 default_secure(self.config)
400 default_secure(self.config)
391 # Create a KernelManager and start a kernel.
401 # Create a KernelManager and start a kernel.
392 self.kernel_manager = MappingKernelManager(
402 self.kernel_manager = MappingKernelManager(
393 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
403 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
394 connection_dir = self.profile_dir.security_dir,
404 connection_dir = self.profile_dir.security_dir,
395 )
405 )
396 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
406 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
397 self.notebook_manager.list_notebooks()
407 self.notebook_manager.list_notebooks()
408 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
398
409
399 def init_logging(self):
410 def init_logging(self):
400 super(NotebookApp, self).init_logging()
411 super(NotebookApp, self).init_logging()
401 # This prevents double log messages because tornado use a root logger that
412 # This prevents double log messages because tornado use a root logger that
402 # self.log is a child of. The logging module dipatches log messages to a log
413 # self.log is a child of. The logging module dipatches log messages to a log
403 # and all of its ancenstors until propagate is set to False.
414 # and all of its ancenstors until propagate is set to False.
404 self.log.propagate = False
415 self.log.propagate = False
405
416
406 def init_webapp(self):
417 def init_webapp(self):
407 """initialize tornado webapp and httpserver"""
418 """initialize tornado webapp and httpserver"""
408 self.web_app = NotebookWebApplication(
419 self.web_app = NotebookWebApplication(
409 self, self.kernel_manager, self.notebook_manager, self.log,
420 self, self.kernel_manager, self.notebook_manager,
421 self.cluster_manager, self.log,
410 self.base_project_url, self.webapp_settings
422 self.base_project_url, self.webapp_settings
411 )
423 )
412 if self.certfile:
424 if self.certfile:
413 ssl_options = dict(certfile=self.certfile)
425 ssl_options = dict(certfile=self.certfile)
414 if self.keyfile:
426 if self.keyfile:
415 ssl_options['keyfile'] = self.keyfile
427 ssl_options['keyfile'] = self.keyfile
416 else:
428 else:
417 ssl_options = None
429 ssl_options = None
418 self.web_app.password = self.password
430 self.web_app.password = self.password
419 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
431 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
420 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
432 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
421 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
433 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
422 'but not using any encryption or authentication. This is highly '
434 'but not using any encryption or authentication. This is highly '
423 'insecure and not recommended.')
435 'insecure and not recommended.')
424
436
425 # Try random ports centered around the default.
437 # Try random ports centered around the default.
426 from random import randint
438 from random import randint
427 n = 50 # Max number of attempts, keep reasonably large.
439 n = 50 # Max number of attempts, keep reasonably large.
428 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
440 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
429 try:
441 try:
430 self.http_server.listen(port, self.ip)
442 self.http_server.listen(port, self.ip)
431 except socket.error, e:
443 except socket.error, e:
432 if e.errno != errno.EADDRINUSE:
444 if e.errno != errno.EADDRINUSE:
433 raise
445 raise
434 self.log.info('The port %i is already in use, trying another random port.' % port)
446 self.log.info('The port %i is already in use, trying another random port.' % port)
435 else:
447 else:
436 self.port = port
448 self.port = port
437 break
449 break
438
450
439 @catch_config_error
451 @catch_config_error
440 def initialize(self, argv=None):
452 def initialize(self, argv=None):
441 super(NotebookApp, self).initialize(argv)
453 super(NotebookApp, self).initialize(argv)
442 self.init_configurables()
454 self.init_configurables()
443 self.init_webapp()
455 self.init_webapp()
444
456
445 def cleanup_kernels(self):
457 def cleanup_kernels(self):
446 """shutdown all kernels
458 """shutdown all kernels
447
459
448 The kernels will shutdown themselves when this process no longer exists,
460 The kernels will shutdown themselves when this process no longer exists,
449 but explicit shutdown allows the KernelManagers to cleanup the connection files.
461 but explicit shutdown allows the KernelManagers to cleanup the connection files.
450 """
462 """
451 self.log.info('Shutting down kernels')
463 self.log.info('Shutting down kernels')
452 km = self.kernel_manager
464 km = self.kernel_manager
453 # copy list, since kill_kernel deletes keys
465 # copy list, since kill_kernel deletes keys
454 for kid in list(km.kernel_ids):
466 for kid in list(km.kernel_ids):
455 km.kill_kernel(kid)
467 km.kill_kernel(kid)
456
468
457 def start(self):
469 def start(self):
458 ip = self.ip if self.ip else '[all ip addresses on your system]'
470 ip = self.ip if self.ip else '[all ip addresses on your system]'
459 proto = 'https' if self.certfile else 'http'
471 proto = 'https' if self.certfile else 'http'
460 info = self.log.info
472 info = self.log.info
461 info("The IPython Notebook is running at: %s://%s:%i%s" %
473 info("The IPython Notebook is running at: %s://%s:%i%s" %
462 (proto, ip, self.port,self.base_project_url) )
474 (proto, ip, self.port,self.base_project_url) )
463 info("Use Control-C to stop this server and shut down all kernels.")
475 info("Use Control-C to stop this server and shut down all kernels.")
464
476
465 if self.open_browser:
477 if self.open_browser:
466 ip = self.ip or '127.0.0.1'
478 ip = self.ip or '127.0.0.1'
467 if self.browser:
479 if self.browser:
468 browser = webbrowser.get(self.browser)
480 browser = webbrowser.get(self.browser)
469 else:
481 else:
470 browser = webbrowser.get()
482 browser = webbrowser.get()
471 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
483 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
472 self.base_project_url),
484 self.base_project_url),
473 new=2)
485 new=2)
474 threading.Thread(target=b).start()
486 threading.Thread(target=b).start()
475 try:
487 try:
476 ioloop.IOLoop.instance().start()
488 ioloop.IOLoop.instance().start()
477 except KeyboardInterrupt:
489 except KeyboardInterrupt:
478 info("Interrupted...")
490 info("Interrupted...")
479 finally:
491 finally:
480 self.cleanup_kernels()
492 self.cleanup_kernels()
481
493
482
494
483 #-----------------------------------------------------------------------------
495 #-----------------------------------------------------------------------------
484 # Main entry point
496 # Main entry point
485 #-----------------------------------------------------------------------------
497 #-----------------------------------------------------------------------------
486
498
487 def launch_new_instance():
499 def launch_new_instance():
488 app = NotebookApp.instance()
500 app = NotebookApp.instance()
489 app.initialize()
501 app.initialize()
490 app.start()
502 app.start()
491
503
@@ -1,1221 +1,1223 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Facilities for launching IPython processes asynchronously.
3 Facilities for launching IPython processes asynchronously.
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 * MinRK
8 * MinRK
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 copy
22 import copy
23 import logging
23 import logging
24 import os
24 import os
25 import re
25 import re
26 import stat
26 import stat
27 import time
27 import time
28
28
29 # signal imports, handling various platforms, versions
29 # signal imports, handling various platforms, versions
30
30
31 from signal import SIGINT, SIGTERM
31 from signal import SIGINT, SIGTERM
32 try:
32 try:
33 from signal import SIGKILL
33 from signal import SIGKILL
34 except ImportError:
34 except ImportError:
35 # Windows
35 # Windows
36 SIGKILL=SIGTERM
36 SIGKILL=SIGTERM
37
37
38 try:
38 try:
39 # Windows >= 2.7, 3.2
39 # Windows >= 2.7, 3.2
40 from signal import CTRL_C_EVENT as SIGINT
40 from signal import CTRL_C_EVENT as SIGINT
41 except ImportError:
41 except ImportError:
42 pass
42 pass
43
43
44 from subprocess import Popen, PIPE, STDOUT
44 from subprocess import Popen, PIPE, STDOUT
45 try:
45 try:
46 from subprocess import check_output
46 from subprocess import check_output
47 except ImportError:
47 except ImportError:
48 # pre-2.7, define check_output with Popen
48 # pre-2.7, define check_output with Popen
49 def check_output(*args, **kwargs):
49 def check_output(*args, **kwargs):
50 kwargs.update(dict(stdout=PIPE))
50 kwargs.update(dict(stdout=PIPE))
51 p = Popen(*args, **kwargs)
51 p = Popen(*args, **kwargs)
52 out,err = p.communicate()
52 out,err = p.communicate()
53 return out
53 return out
54
54
55 from zmq.eventloop import ioloop
55 from zmq.eventloop import ioloop
56
56
57 from IPython.config.application import Application
57 from IPython.config.application import Application
58 from IPython.config.configurable import LoggingConfigurable
58 from IPython.config.configurable import LoggingConfigurable
59 from IPython.utils.text import EvalFormatter
59 from IPython.utils.text import EvalFormatter
60 from IPython.utils.traitlets import (
60 from IPython.utils.traitlets import (
61 Any, Integer, CFloat, List, Unicode, Dict, Instance, HasTraits,
61 Any, Integer, CFloat, List, Unicode, Dict, Instance, HasTraits,
62 )
62 )
63 from IPython.utils.path import get_ipython_module_path
63 from IPython.utils.path import get_ipython_module_path
64 from IPython.utils.process import find_cmd, pycmd2argv, FindCmdError
64 from IPython.utils.process import find_cmd, pycmd2argv, FindCmdError
65
65
66 from .win32support import forward_read_events
66 from .win32support import forward_read_events
67
67
68 from .winhpcjob import IPControllerTask, IPEngineTask, IPControllerJob, IPEngineSetJob
68 from .winhpcjob import IPControllerTask, IPEngineTask, IPControllerJob, IPEngineSetJob
69
69
70 WINDOWS = os.name == 'nt'
70 WINDOWS = os.name == 'nt'
71
71
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 # Paths to the kernel apps
73 # Paths to the kernel apps
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76
76
77 ipcluster_cmd_argv = pycmd2argv(get_ipython_module_path(
77 ipcluster_cmd_argv = pycmd2argv(get_ipython_module_path(
78 'IPython.parallel.apps.ipclusterapp'
78 'IPython.parallel.apps.ipclusterapp'
79 ))
79 ))
80
80
81 ipengine_cmd_argv = pycmd2argv(get_ipython_module_path(
81 ipengine_cmd_argv = pycmd2argv(get_ipython_module_path(
82 'IPython.parallel.apps.ipengineapp'
82 'IPython.parallel.apps.ipengineapp'
83 ))
83 ))
84
84
85 ipcontroller_cmd_argv = pycmd2argv(get_ipython_module_path(
85 ipcontroller_cmd_argv = pycmd2argv(get_ipython_module_path(
86 'IPython.parallel.apps.ipcontrollerapp'
86 'IPython.parallel.apps.ipcontrollerapp'
87 ))
87 ))
88
88
89 #-----------------------------------------------------------------------------
89 #-----------------------------------------------------------------------------
90 # Base launchers and errors
90 # Base launchers and errors
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92
92
93
93
94 class LauncherError(Exception):
94 class LauncherError(Exception):
95 pass
95 pass
96
96
97
97
98 class ProcessStateError(LauncherError):
98 class ProcessStateError(LauncherError):
99 pass
99 pass
100
100
101
101
102 class UnknownStatus(LauncherError):
102 class UnknownStatus(LauncherError):
103 pass
103 pass
104
104
105
105
106 class BaseLauncher(LoggingConfigurable):
106 class BaseLauncher(LoggingConfigurable):
107 """An asbtraction for starting, stopping and signaling a process."""
107 """An asbtraction for starting, stopping and signaling a process."""
108
108
109 # In all of the launchers, the work_dir is where child processes will be
109 # In all of the launchers, the work_dir is where child processes will be
110 # run. This will usually be the profile_dir, but may not be. any work_dir
110 # run. This will usually be the profile_dir, but may not be. any work_dir
111 # passed into the __init__ method will override the config value.
111 # passed into the __init__ method will override the config value.
112 # This should not be used to set the work_dir for the actual engine
112 # This should not be used to set the work_dir for the actual engine
113 # and controller. Instead, use their own config files or the
113 # and controller. Instead, use their own config files or the
114 # controller_args, engine_args attributes of the launchers to add
114 # controller_args, engine_args attributes of the launchers to add
115 # the work_dir option.
115 # the work_dir option.
116 work_dir = Unicode(u'.')
116 work_dir = Unicode(u'.')
117 loop = Instance('zmq.eventloop.ioloop.IOLoop')
117 loop = Instance('zmq.eventloop.ioloop.IOLoop')
118
118
119 start_data = Any()
119 start_data = Any()
120 stop_data = Any()
120 stop_data = Any()
121
121
122 def _loop_default(self):
122 def _loop_default(self):
123 return ioloop.IOLoop.instance()
123 return ioloop.IOLoop.instance()
124
124
125 def __init__(self, work_dir=u'.', config=None, **kwargs):
125 def __init__(self, work_dir=u'.', config=None, **kwargs):
126 super(BaseLauncher, self).__init__(work_dir=work_dir, config=config, **kwargs)
126 super(BaseLauncher, self).__init__(work_dir=work_dir, config=config, **kwargs)
127 self.state = 'before' # can be before, running, after
127 self.state = 'before' # can be before, running, after
128 self.stop_callbacks = []
128 self.stop_callbacks = []
129 self.start_data = None
129 self.start_data = None
130 self.stop_data = None
130 self.stop_data = None
131
131
132 @property
132 @property
133 def args(self):
133 def args(self):
134 """A list of cmd and args that will be used to start the process.
134 """A list of cmd and args that will be used to start the process.
135
135
136 This is what is passed to :func:`spawnProcess` and the first element
136 This is what is passed to :func:`spawnProcess` and the first element
137 will be the process name.
137 will be the process name.
138 """
138 """
139 return self.find_args()
139 return self.find_args()
140
140
141 def find_args(self):
141 def find_args(self):
142 """The ``.args`` property calls this to find the args list.
142 """The ``.args`` property calls this to find the args list.
143
143
144 Subcommand should implement this to construct the cmd and args.
144 Subcommand should implement this to construct the cmd and args.
145 """
145 """
146 raise NotImplementedError('find_args must be implemented in a subclass')
146 raise NotImplementedError('find_args must be implemented in a subclass')
147
147
148 @property
148 @property
149 def arg_str(self):
149 def arg_str(self):
150 """The string form of the program arguments."""
150 """The string form of the program arguments."""
151 return ' '.join(self.args)
151 return ' '.join(self.args)
152
152
153 @property
153 @property
154 def running(self):
154 def running(self):
155 """Am I running."""
155 """Am I running."""
156 if self.state == 'running':
156 if self.state == 'running':
157 return True
157 return True
158 else:
158 else:
159 return False
159 return False
160
160
161 def start(self):
161 def start(self):
162 """Start the process."""
162 """Start the process."""
163 raise NotImplementedError('start must be implemented in a subclass')
163 raise NotImplementedError('start must be implemented in a subclass')
164
164
165 def stop(self):
165 def stop(self):
166 """Stop the process and notify observers of stopping.
166 """Stop the process and notify observers of stopping.
167
167
168 This method will return None immediately.
168 This method will return None immediately.
169 To observe the actual process stopping, see :meth:`on_stop`.
169 To observe the actual process stopping, see :meth:`on_stop`.
170 """
170 """
171 raise NotImplementedError('stop must be implemented in a subclass')
171 raise NotImplementedError('stop must be implemented in a subclass')
172
172
173 def on_stop(self, f):
173 def on_stop(self, f):
174 """Register a callback to be called with this Launcher's stop_data
174 """Register a callback to be called with this Launcher's stop_data
175 when the process actually finishes.
175 when the process actually finishes.
176 """
176 """
177 if self.state=='after':
177 if self.state=='after':
178 return f(self.stop_data)
178 return f(self.stop_data)
179 else:
179 else:
180 self.stop_callbacks.append(f)
180 self.stop_callbacks.append(f)
181
181
182 def notify_start(self, data):
182 def notify_start(self, data):
183 """Call this to trigger startup actions.
183 """Call this to trigger startup actions.
184
184
185 This logs the process startup and sets the state to 'running'. It is
185 This logs the process startup and sets the state to 'running'. It is
186 a pass-through so it can be used as a callback.
186 a pass-through so it can be used as a callback.
187 """
187 """
188
188
189 self.log.debug('Process %r started: %r', self.args[0], data)
189 self.log.debug('Process %r started: %r', self.args[0], data)
190 self.start_data = data
190 self.start_data = data
191 self.state = 'running'
191 self.state = 'running'
192 return data
192 return data
193
193
194 def notify_stop(self, data):
194 def notify_stop(self, data):
195 """Call this to trigger process stop actions.
195 """Call this to trigger process stop actions.
196
196
197 This logs the process stopping and sets the state to 'after'. Call
197 This logs the process stopping and sets the state to 'after'. Call
198 this to trigger callbacks registered via :meth:`on_stop`."""
198 this to trigger callbacks registered via :meth:`on_stop`."""
199
199
200 self.log.debug('Process %r stopped: %r', self.args[0], data)
200 self.log.debug('Process %r stopped: %r', self.args[0], data)
201 self.stop_data = data
201 self.stop_data = data
202 self.state = 'after'
202 self.state = 'after'
203 for i in range(len(self.stop_callbacks)):
203 for i in range(len(self.stop_callbacks)):
204 d = self.stop_callbacks.pop()
204 d = self.stop_callbacks.pop()
205 d(data)
205 d(data)
206 return data
206 return data
207
207
208 def signal(self, sig):
208 def signal(self, sig):
209 """Signal the process.
209 """Signal the process.
210
210
211 Parameters
211 Parameters
212 ----------
212 ----------
213 sig : str or int
213 sig : str or int
214 'KILL', 'INT', etc., or any signal number
214 'KILL', 'INT', etc., or any signal number
215 """
215 """
216 raise NotImplementedError('signal must be implemented in a subclass')
216 raise NotImplementedError('signal must be implemented in a subclass')
217
217
218 class ClusterAppMixin(HasTraits):
218 class ClusterAppMixin(HasTraits):
219 """MixIn for cluster args as traits"""
219 """MixIn for cluster args as traits"""
220 cluster_args = List([])
220 cluster_args = List([])
221 profile_dir=Unicode('')
221 profile_dir=Unicode('')
222 cluster_id=Unicode('')
222 cluster_id=Unicode('')
223 def _profile_dir_changed(self, name, old, new):
223 def _profile_dir_changed(self, name, old, new):
224 self.cluster_args = []
224 self.cluster_args = []
225 if self.profile_dir:
225 if self.profile_dir:
226 self.cluster_args.extend(['--profile-dir', self.profile_dir])
226 self.cluster_args.extend(['--profile-dir', self.profile_dir])
227 if self.cluster_id:
227 if self.cluster_id:
228 self.cluster_args.extend(['--cluster-id', self.cluster_id])
228 self.cluster_args.extend(['--cluster-id', self.cluster_id])
229 _cluster_id_changed = _profile_dir_changed
229 _cluster_id_changed = _profile_dir_changed
230
230
231 class ControllerMixin(ClusterAppMixin):
231 class ControllerMixin(ClusterAppMixin):
232 controller_cmd = List(ipcontroller_cmd_argv, config=True,
232 controller_cmd = List(ipcontroller_cmd_argv, config=True,
233 help="""Popen command to launch ipcontroller.""")
233 help="""Popen command to launch ipcontroller.""")
234 # Command line arguments to ipcontroller.
234 # Command line arguments to ipcontroller.
235 controller_args = List(['--log-to-file','--log-level=%i' % logging.INFO], config=True,
235 controller_args = List(['--log-to-file','--log-level=%i' % logging.INFO], config=True,
236 help="""command-line args to pass to ipcontroller""")
236 help="""command-line args to pass to ipcontroller""")
237
237
238 class EngineMixin(ClusterAppMixin):
238 class EngineMixin(ClusterAppMixin):
239 engine_cmd = List(ipengine_cmd_argv, config=True,
239 engine_cmd = List(ipengine_cmd_argv, config=True,
240 help="""command to launch the Engine.""")
240 help="""command to launch the Engine.""")
241 # Command line arguments for ipengine.
241 # Command line arguments for ipengine.
242 engine_args = List(['--log-to-file','--log-level=%i' % logging.INFO], config=True,
242 engine_args = List(['--log-to-file','--log-level=%i' % logging.INFO], config=True,
243 help="command-line arguments to pass to ipengine"
243 help="command-line arguments to pass to ipengine"
244 )
244 )
245
245
246 #-----------------------------------------------------------------------------
246 #-----------------------------------------------------------------------------
247 # Local process launchers
247 # Local process launchers
248 #-----------------------------------------------------------------------------
248 #-----------------------------------------------------------------------------
249
249
250
250
251 class LocalProcessLauncher(BaseLauncher):
251 class LocalProcessLauncher(BaseLauncher):
252 """Start and stop an external process in an asynchronous manner.
252 """Start and stop an external process in an asynchronous manner.
253
253
254 This will launch the external process with a working directory of
254 This will launch the external process with a working directory of
255 ``self.work_dir``.
255 ``self.work_dir``.
256 """
256 """
257
257
258 # This is used to to construct self.args, which is passed to
258 # This is used to to construct self.args, which is passed to
259 # spawnProcess.
259 # spawnProcess.
260 cmd_and_args = List([])
260 cmd_and_args = List([])
261 poll_frequency = Integer(100) # in ms
261 poll_frequency = Integer(100) # in ms
262
262
263 def __init__(self, work_dir=u'.', config=None, **kwargs):
263 def __init__(self, work_dir=u'.', config=None, **kwargs):
264 super(LocalProcessLauncher, self).__init__(
264 super(LocalProcessLauncher, self).__init__(
265 work_dir=work_dir, config=config, **kwargs
265 work_dir=work_dir, config=config, **kwargs
266 )
266 )
267 self.process = None
267 self.process = None
268 self.poller = None
268 self.poller = None
269
269
270 def find_args(self):
270 def find_args(self):
271 return self.cmd_and_args
271 return self.cmd_and_args
272
272
273 def start(self):
273 def start(self):
274 self.log.debug("Starting %s: %r", self.__class__.__name__, self.args)
274 self.log.debug("Starting %s: %r", self.__class__.__name__, self.args)
275 if self.state == 'before':
275 if self.state == 'before':
276 self.process = Popen(self.args,
276 self.process = Popen(self.args,
277 stdout=PIPE,stderr=PIPE,stdin=PIPE,
277 stdout=PIPE,stderr=PIPE,stdin=PIPE,
278 env=os.environ,
278 env=os.environ,
279 cwd=self.work_dir
279 cwd=self.work_dir
280 )
280 )
281 if WINDOWS:
281 if WINDOWS:
282 self.stdout = forward_read_events(self.process.stdout)
282 self.stdout = forward_read_events(self.process.stdout)
283 self.stderr = forward_read_events(self.process.stderr)
283 self.stderr = forward_read_events(self.process.stderr)
284 else:
284 else:
285 self.stdout = self.process.stdout.fileno()
285 self.stdout = self.process.stdout.fileno()
286 self.stderr = self.process.stderr.fileno()
286 self.stderr = self.process.stderr.fileno()
287 self.loop.add_handler(self.stdout, self.handle_stdout, self.loop.READ)
287 self.loop.add_handler(self.stdout, self.handle_stdout, self.loop.READ)
288 self.loop.add_handler(self.stderr, self.handle_stderr, self.loop.READ)
288 self.loop.add_handler(self.stderr, self.handle_stderr, self.loop.READ)
289 self.poller = ioloop.PeriodicCallback(self.poll, self.poll_frequency, self.loop)
289 self.poller = ioloop.PeriodicCallback(self.poll, self.poll_frequency, self.loop)
290 self.poller.start()
290 self.poller.start()
291 self.notify_start(self.process.pid)
291 self.notify_start(self.process.pid)
292 else:
292 else:
293 s = 'The process was already started and has state: %r' % self.state
293 s = 'The process was already started and has state: %r' % self.state
294 raise ProcessStateError(s)
294 raise ProcessStateError(s)
295
295
296 def stop(self):
296 def stop(self):
297 return self.interrupt_then_kill()
297 return self.interrupt_then_kill()
298
298
299 def signal(self, sig):
299 def signal(self, sig):
300 if self.state == 'running':
300 if self.state == 'running':
301 if WINDOWS and sig != SIGINT:
301 if WINDOWS and sig != SIGINT:
302 # use Windows tree-kill for better child cleanup
302 # use Windows tree-kill for better child cleanup
303 check_output(['taskkill', '-pid', str(self.process.pid), '-t', '-f'])
303 check_output(['taskkill', '-pid', str(self.process.pid), '-t', '-f'])
304 else:
304 else:
305 self.process.send_signal(sig)
305 self.process.send_signal(sig)
306
306
307 def interrupt_then_kill(self, delay=2.0):
307 def interrupt_then_kill(self, delay=2.0):
308 """Send INT, wait a delay and then send KILL."""
308 """Send INT, wait a delay and then send KILL."""
309 try:
309 try:
310 self.signal(SIGINT)
310 self.signal(SIGINT)
311 except Exception:
311 except Exception:
312 self.log.debug("interrupt failed")
312 self.log.debug("interrupt failed")
313 pass
313 pass
314 self.killer = ioloop.DelayedCallback(lambda : self.signal(SIGKILL), delay*1000, self.loop)
314 self.killer = ioloop.DelayedCallback(lambda : self.signal(SIGKILL), delay*1000, self.loop)
315 self.killer.start()
315 self.killer.start()
316
316
317 # callbacks, etc:
317 # callbacks, etc:
318
318
319 def handle_stdout(self, fd, events):
319 def handle_stdout(self, fd, events):
320 if WINDOWS:
320 if WINDOWS:
321 line = self.stdout.recv()
321 line = self.stdout.recv()
322 else:
322 else:
323 line = self.process.stdout.readline()
323 line = self.process.stdout.readline()
324 # a stopped process will be readable but return empty strings
324 # a stopped process will be readable but return empty strings
325 if line:
325 if line:
326 self.log.debug(line[:-1])
326 self.log.debug(line[:-1])
327 else:
327 else:
328 self.poll()
328 self.poll()
329
329
330 def handle_stderr(self, fd, events):
330 def handle_stderr(self, fd, events):
331 if WINDOWS:
331 if WINDOWS:
332 line = self.stderr.recv()
332 line = self.stderr.recv()
333 else:
333 else:
334 line = self.process.stderr.readline()
334 line = self.process.stderr.readline()
335 # a stopped process will be readable but return empty strings
335 # a stopped process will be readable but return empty strings
336 if line:
336 if line:
337 self.log.debug(line[:-1])
337 self.log.debug(line[:-1])
338 else:
338 else:
339 self.poll()
339 self.poll()
340
340
341 def poll(self):
341 def poll(self):
342 status = self.process.poll()
342 status = self.process.poll()
343 if status is not None:
343 if status is not None:
344 self.poller.stop()
344 self.poller.stop()
345 self.loop.remove_handler(self.stdout)
345 self.loop.remove_handler(self.stdout)
346 self.loop.remove_handler(self.stderr)
346 self.loop.remove_handler(self.stderr)
347 self.notify_stop(dict(exit_code=status, pid=self.process.pid))
347 self.notify_stop(dict(exit_code=status, pid=self.process.pid))
348 return status
348 return status
349
349
350 class LocalControllerLauncher(LocalProcessLauncher, ControllerMixin):
350 class LocalControllerLauncher(LocalProcessLauncher, ControllerMixin):
351 """Launch a controller as a regular external process."""
351 """Launch a controller as a regular external process."""
352
352
353 def find_args(self):
353 def find_args(self):
354 return self.controller_cmd + self.cluster_args + self.controller_args
354 return self.controller_cmd + self.cluster_args + self.controller_args
355
355
356 def start(self):
356 def start(self):
357 """Start the controller by profile_dir."""
357 """Start the controller by profile_dir."""
358 return super(LocalControllerLauncher, self).start()
358 return super(LocalControllerLauncher, self).start()
359
359
360
360
361 class LocalEngineLauncher(LocalProcessLauncher, EngineMixin):
361 class LocalEngineLauncher(LocalProcessLauncher, EngineMixin):
362 """Launch a single engine as a regular externall process."""
362 """Launch a single engine as a regular externall process."""
363
363
364 def find_args(self):
364 def find_args(self):
365 return self.engine_cmd + self.cluster_args + self.engine_args
365 return self.engine_cmd + self.cluster_args + self.engine_args
366
366
367
367
368 class LocalEngineSetLauncher(LocalEngineLauncher):
368 class LocalEngineSetLauncher(LocalEngineLauncher):
369 """Launch a set of engines as regular external processes."""
369 """Launch a set of engines as regular external processes."""
370
370
371 delay = CFloat(0.1, config=True,
371 delay = CFloat(0.1, config=True,
372 help="""delay (in seconds) between starting each engine after the first.
372 help="""delay (in seconds) between starting each engine after the first.
373 This can help force the engines to get their ids in order, or limit
373 This can help force the engines to get their ids in order, or limit
374 process flood when starting many engines."""
374 process flood when starting many engines."""
375 )
375 )
376
376
377 # launcher class
377 # launcher class
378 launcher_class = LocalEngineLauncher
378 launcher_class = LocalEngineLauncher
379
379
380 launchers = Dict()
380 launchers = Dict()
381 stop_data = Dict()
381 stop_data = Dict()
382
382
383 def __init__(self, work_dir=u'.', config=None, **kwargs):
383 def __init__(self, work_dir=u'.', config=None, **kwargs):
384 super(LocalEngineSetLauncher, self).__init__(
384 super(LocalEngineSetLauncher, self).__init__(
385 work_dir=work_dir, config=config, **kwargs
385 work_dir=work_dir, config=config, **kwargs
386 )
386 )
387 self.stop_data = {}
387 self.stop_data = {}
388
388
389 def start(self, n):
389 def start(self, n):
390 """Start n engines by profile or profile_dir."""
390 """Start n engines by profile or profile_dir."""
391 dlist = []
391 dlist = []
392 for i in range(n):
392 for i in range(n):
393 if i > 0:
393 if i > 0:
394 time.sleep(self.delay)
394 time.sleep(self.delay)
395 el = self.launcher_class(work_dir=self.work_dir, config=self.config, log=self.log,
395 el = self.launcher_class(work_dir=self.work_dir, config=self.config, log=self.log,
396 profile_dir=self.profile_dir, cluster_id=self.cluster_id,
396 profile_dir=self.profile_dir, cluster_id=self.cluster_id,
397 )
397 )
398
398
399 # Copy the engine args over to each engine launcher.
399 # Copy the engine args over to each engine launcher.
400 el.engine_cmd = copy.deepcopy(self.engine_cmd)
400 el.engine_cmd = copy.deepcopy(self.engine_cmd)
401 el.engine_args = copy.deepcopy(self.engine_args)
401 el.engine_args = copy.deepcopy(self.engine_args)
402 el.on_stop(self._notice_engine_stopped)
402 el.on_stop(self._notice_engine_stopped)
403 d = el.start()
403 d = el.start()
404 self.launchers[i] = el
404 self.launchers[i] = el
405 dlist.append(d)
405 dlist.append(d)
406 self.notify_start(dlist)
406 self.notify_start(dlist)
407 return dlist
407 return dlist
408
408
409 def find_args(self):
409 def find_args(self):
410 return ['engine set']
410 return ['engine set']
411
411
412 def signal(self, sig):
412 def signal(self, sig):
413 dlist = []
413 dlist = []
414 for el in self.launchers.itervalues():
414 for el in self.launchers.itervalues():
415 d = el.signal(sig)
415 d = el.signal(sig)
416 dlist.append(d)
416 dlist.append(d)
417 return dlist
417 return dlist
418
418
419 def interrupt_then_kill(self, delay=1.0):
419 def interrupt_then_kill(self, delay=1.0):
420 dlist = []
420 dlist = []
421 for el in self.launchers.itervalues():
421 for el in self.launchers.itervalues():
422 d = el.interrupt_then_kill(delay)
422 d = el.interrupt_then_kill(delay)
423 dlist.append(d)
423 dlist.append(d)
424 return dlist
424 return dlist
425
425
426 def stop(self):
426 def stop(self):
427 return self.interrupt_then_kill()
427 return self.interrupt_then_kill()
428
428
429 def _notice_engine_stopped(self, data):
429 def _notice_engine_stopped(self, data):
430 pid = data['pid']
430 pid = data['pid']
431 for idx,el in self.launchers.iteritems():
431 for idx,el in self.launchers.iteritems():
432 if el.process.pid == pid:
432 if el.process.pid == pid:
433 break
433 break
434 self.launchers.pop(idx)
434 self.launchers.pop(idx)
435 self.stop_data[idx] = data
435 self.stop_data[idx] = data
436 if not self.launchers:
436 if not self.launchers:
437 self.notify_stop(self.stop_data)
437 self.notify_stop(self.stop_data)
438
438
439
439
440 #-----------------------------------------------------------------------------
440 #-----------------------------------------------------------------------------
441 # MPI launchers
441 # MPI launchers
442 #-----------------------------------------------------------------------------
442 #-----------------------------------------------------------------------------
443
443
444
444
445 class MPILauncher(LocalProcessLauncher):
445 class MPILauncher(LocalProcessLauncher):
446 """Launch an external process using mpiexec."""
446 """Launch an external process using mpiexec."""
447
447
448 mpi_cmd = List(['mpiexec'], config=True,
448 mpi_cmd = List(['mpiexec'], config=True,
449 help="The mpiexec command to use in starting the process."
449 help="The mpiexec command to use in starting the process."
450 )
450 )
451 mpi_args = List([], config=True,
451 mpi_args = List([], config=True,
452 help="The command line arguments to pass to mpiexec."
452 help="The command line arguments to pass to mpiexec."
453 )
453 )
454 program = List(['date'],
454 program = List(['date'],
455 help="The program to start via mpiexec.")
455 help="The program to start via mpiexec.")
456 program_args = List([],
456 program_args = List([],
457 help="The command line argument to the program."
457 help="The command line argument to the program."
458 )
458 )
459 n = Integer(1)
459 n = Integer(1)
460
460
461 def __init__(self, *args, **kwargs):
461 def __init__(self, *args, **kwargs):
462 # deprecation for old MPIExec names:
462 # deprecation for old MPIExec names:
463 config = kwargs.get('config', {})
463 config = kwargs.get('config', {})
464 for oldname in ('MPIExecLauncher', 'MPIExecControllerLauncher', 'MPIExecEngineSetLauncher'):
464 for oldname in ('MPIExecLauncher', 'MPIExecControllerLauncher', 'MPIExecEngineSetLauncher'):
465 deprecated = config.get(oldname)
465 deprecated = config.get(oldname)
466 if deprecated:
466 if deprecated:
467 newname = oldname.replace('MPIExec', 'MPI')
467 newname = oldname.replace('MPIExec', 'MPI')
468 config[newname].update(deprecated)
468 config[newname].update(deprecated)
469 self.log.warn("WARNING: %s name has been deprecated, use %s", oldname, newname)
469 self.log.warn("WARNING: %s name has been deprecated, use %s", oldname, newname)
470
470
471 super(MPILauncher, self).__init__(*args, **kwargs)
471 super(MPILauncher, self).__init__(*args, **kwargs)
472
472
473 def find_args(self):
473 def find_args(self):
474 """Build self.args using all the fields."""
474 """Build self.args using all the fields."""
475 return self.mpi_cmd + ['-n', str(self.n)] + self.mpi_args + \
475 return self.mpi_cmd + ['-n', str(self.n)] + self.mpi_args + \
476 self.program + self.program_args
476 self.program + self.program_args
477
477
478 def start(self, n):
478 def start(self, n):
479 """Start n instances of the program using mpiexec."""
479 """Start n instances of the program using mpiexec."""
480 self.n = n
480 self.n = n
481 return super(MPILauncher, self).start()
481 return super(MPILauncher, self).start()
482
482
483
483
484 class MPIControllerLauncher(MPILauncher, ControllerMixin):
484 class MPIControllerLauncher(MPILauncher, ControllerMixin):
485 """Launch a controller using mpiexec."""
485 """Launch a controller using mpiexec."""
486
486
487 # alias back to *non-configurable* program[_args] for use in find_args()
487 # alias back to *non-configurable* program[_args] for use in find_args()
488 # this way all Controller/EngineSetLaunchers have the same form, rather
488 # this way all Controller/EngineSetLaunchers have the same form, rather
489 # than *some* having `program_args` and others `controller_args`
489 # than *some* having `program_args` and others `controller_args`
490 @property
490 @property
491 def program(self):
491 def program(self):
492 return self.controller_cmd
492 return self.controller_cmd
493
493
494 @property
494 @property
495 def program_args(self):
495 def program_args(self):
496 return self.cluster_args + self.controller_args
496 return self.cluster_args + self.controller_args
497
497
498 def start(self):
498 def start(self):
499 """Start the controller by profile_dir."""
499 """Start the controller by profile_dir."""
500 return super(MPIControllerLauncher, self).start(1)
500 return super(MPIControllerLauncher, self).start(1)
501
501
502
502
503 class MPIEngineSetLauncher(MPILauncher, EngineMixin):
503 class MPIEngineSetLauncher(MPILauncher, EngineMixin):
504 """Launch engines using mpiexec"""
504 """Launch engines using mpiexec"""
505
505
506 # alias back to *non-configurable* program[_args] for use in find_args()
506 # alias back to *non-configurable* program[_args] for use in find_args()
507 # this way all Controller/EngineSetLaunchers have the same form, rather
507 # this way all Controller/EngineSetLaunchers have the same form, rather
508 # than *some* having `program_args` and others `controller_args`
508 # than *some* having `program_args` and others `controller_args`
509 @property
509 @property
510 def program(self):
510 def program(self):
511 return self.engine_cmd
511 return self.engine_cmd
512
512
513 @property
513 @property
514 def program_args(self):
514 def program_args(self):
515 return self.cluster_args + self.engine_args
515 return self.cluster_args + self.engine_args
516
516
517 def start(self, n):
517 def start(self, n):
518 """Start n engines by profile or profile_dir."""
518 """Start n engines by profile or profile_dir."""
519 self.n = n
519 self.n = n
520 return super(MPIEngineSetLauncher, self).start(n)
520 return super(MPIEngineSetLauncher, self).start(n)
521
521
522 # deprecated MPIExec names
522 # deprecated MPIExec names
523 class DeprecatedMPILauncher(object):
523 class DeprecatedMPILauncher(object):
524 def warn(self):
524 def warn(self):
525 oldname = self.__class__.__name__
525 oldname = self.__class__.__name__
526 newname = oldname.replace('MPIExec', 'MPI')
526 newname = oldname.replace('MPIExec', 'MPI')
527 self.log.warn("WARNING: %s name is deprecated, use %s", oldname, newname)
527 self.log.warn("WARNING: %s name is deprecated, use %s", oldname, newname)
528
528
529 class MPIExecLauncher(MPILauncher, DeprecatedMPILauncher):
529 class MPIExecLauncher(MPILauncher, DeprecatedMPILauncher):
530 """Deprecated, use MPILauncher"""
530 """Deprecated, use MPILauncher"""
531 def __init__(self, *args, **kwargs):
531 def __init__(self, *args, **kwargs):
532 super(MPIExecLauncher, self).__init__(*args, **kwargs)
532 super(MPIExecLauncher, self).__init__(*args, **kwargs)
533 self.warn()
533 self.warn()
534
534
535 class MPIExecControllerLauncher(MPIControllerLauncher, DeprecatedMPILauncher):
535 class MPIExecControllerLauncher(MPIControllerLauncher, DeprecatedMPILauncher):
536 """Deprecated, use MPIControllerLauncher"""
536 """Deprecated, use MPIControllerLauncher"""
537 def __init__(self, *args, **kwargs):
537 def __init__(self, *args, **kwargs):
538 super(MPIExecControllerLauncher, self).__init__(*args, **kwargs)
538 super(MPIExecControllerLauncher, self).__init__(*args, **kwargs)
539 self.warn()
539 self.warn()
540
540
541 class MPIExecEngineSetLauncher(MPIEngineSetLauncher, DeprecatedMPILauncher):
541 class MPIExecEngineSetLauncher(MPIEngineSetLauncher, DeprecatedMPILauncher):
542 """Deprecated, use MPIEngineSetLauncher"""
542 """Deprecated, use MPIEngineSetLauncher"""
543 def __init__(self, *args, **kwargs):
543 def __init__(self, *args, **kwargs):
544 super(MPIExecEngineSetLauncher, self).__init__(*args, **kwargs)
544 super(MPIExecEngineSetLauncher, self).__init__(*args, **kwargs)
545 self.warn()
545 self.warn()
546
546
547
547
548 #-----------------------------------------------------------------------------
548 #-----------------------------------------------------------------------------
549 # SSH launchers
549 # SSH launchers
550 #-----------------------------------------------------------------------------
550 #-----------------------------------------------------------------------------
551
551
552 # TODO: Get SSH Launcher back to level of sshx in 0.10.2
552 # TODO: Get SSH Launcher back to level of sshx in 0.10.2
553
553
554 class SSHLauncher(LocalProcessLauncher):
554 class SSHLauncher(LocalProcessLauncher):
555 """A minimal launcher for ssh.
555 """A minimal launcher for ssh.
556
556
557 To be useful this will probably have to be extended to use the ``sshx``
557 To be useful this will probably have to be extended to use the ``sshx``
558 idea for environment variables. There could be other things this needs
558 idea for environment variables. There could be other things this needs
559 as well.
559 as well.
560 """
560 """
561
561
562 ssh_cmd = List(['ssh'], config=True,
562 ssh_cmd = List(['ssh'], config=True,
563 help="command for starting ssh")
563 help="command for starting ssh")
564 ssh_args = List(['-tt'], config=True,
564 ssh_args = List(['-tt'], config=True,
565 help="args to pass to ssh")
565 help="args to pass to ssh")
566 program = List(['date'],
566 program = List(['date'],
567 help="Program to launch via ssh")
567 help="Program to launch via ssh")
568 program_args = List([],
568 program_args = List([],
569 help="args to pass to remote program")
569 help="args to pass to remote program")
570 hostname = Unicode('', config=True,
570 hostname = Unicode('', config=True,
571 help="hostname on which to launch the program")
571 help="hostname on which to launch the program")
572 user = Unicode('', config=True,
572 user = Unicode('', config=True,
573 help="username for ssh")
573 help="username for ssh")
574 location = Unicode('', config=True,
574 location = Unicode('', config=True,
575 help="user@hostname location for ssh in one setting")
575 help="user@hostname location for ssh in one setting")
576
576
577 def _hostname_changed(self, name, old, new):
577 def _hostname_changed(self, name, old, new):
578 if self.user:
578 if self.user:
579 self.location = u'%s@%s' % (self.user, new)
579 self.location = u'%s@%s' % (self.user, new)
580 else:
580 else:
581 self.location = new
581 self.location = new
582
582
583 def _user_changed(self, name, old, new):
583 def _user_changed(self, name, old, new):
584 self.location = u'%s@%s' % (new, self.hostname)
584 self.location = u'%s@%s' % (new, self.hostname)
585
585
586 def find_args(self):
586 def find_args(self):
587 return self.ssh_cmd + self.ssh_args + [self.location] + \
587 return self.ssh_cmd + self.ssh_args + [self.location] + \
588 self.program + self.program_args
588 self.program + self.program_args
589
589
590 def start(self, hostname=None, user=None):
590 def start(self, hostname=None, user=None):
591 if hostname is not None:
591 if hostname is not None:
592 self.hostname = hostname
592 self.hostname = hostname
593 if user is not None:
593 if user is not None:
594 self.user = user
594 self.user = user
595
595
596 return super(SSHLauncher, self).start()
596 return super(SSHLauncher, self).start()
597
597
598 def signal(self, sig):
598 def signal(self, sig):
599 if self.state == 'running':
599 if self.state == 'running':
600 # send escaped ssh connection-closer
600 # send escaped ssh connection-closer
601 self.process.stdin.write('~.')
601 self.process.stdin.write('~.')
602 self.process.stdin.flush()
602 self.process.stdin.flush()
603
603
604
604
605
605
606 class SSHControllerLauncher(SSHLauncher, ControllerMixin):
606 class SSHControllerLauncher(SSHLauncher, ControllerMixin):
607
607
608 # alias back to *non-configurable* program[_args] for use in find_args()
608 # alias back to *non-configurable* program[_args] for use in find_args()
609 # this way all Controller/EngineSetLaunchers have the same form, rather
609 # this way all Controller/EngineSetLaunchers have the same form, rather
610 # than *some* having `program_args` and others `controller_args`
610 # than *some* having `program_args` and others `controller_args`
611 @property
611 @property
612 def program(self):
612 def program(self):
613 return self.controller_cmd
613 return self.controller_cmd
614
614
615 @property
615 @property
616 def program_args(self):
616 def program_args(self):
617 return self.cluster_args + self.controller_args
617 return self.cluster_args + self.controller_args
618
618
619
619
620 class SSHEngineLauncher(SSHLauncher, EngineMixin):
620 class SSHEngineLauncher(SSHLauncher, EngineMixin):
621
621
622 # alias back to *non-configurable* program[_args] for use in find_args()
622 # alias back to *non-configurable* program[_args] for use in find_args()
623 # this way all Controller/EngineSetLaunchers have the same form, rather
623 # this way all Controller/EngineSetLaunchers have the same form, rather
624 # than *some* having `program_args` and others `controller_args`
624 # than *some* having `program_args` and others `controller_args`
625 @property
625 @property
626 def program(self):
626 def program(self):
627 return self.engine_cmd
627 return self.engine_cmd
628
628
629 @property
629 @property
630 def program_args(self):
630 def program_args(self):
631 return self.cluster_args + self.engine_args
631 return self.cluster_args + self.engine_args
632
632
633
633
634 class SSHEngineSetLauncher(LocalEngineSetLauncher):
634 class SSHEngineSetLauncher(LocalEngineSetLauncher):
635 launcher_class = SSHEngineLauncher
635 launcher_class = SSHEngineLauncher
636 engines = Dict(config=True,
636 engines = Dict(config=True,
637 help="""dict of engines to launch. This is a dict by hostname of ints,
637 help="""dict of engines to launch. This is a dict by hostname of ints,
638 corresponding to the number of engines to start on that host.""")
638 corresponding to the number of engines to start on that host.""")
639
639
640 @property
640 @property
641 def engine_count(self):
641 def engine_count(self):
642 """determine engine count from `engines` dict"""
642 """determine engine count from `engines` dict"""
643 count = 0
643 count = 0
644 for n in self.engines.itervalues():
644 for n in self.engines.itervalues():
645 if isinstance(n, (tuple,list)):
645 if isinstance(n, (tuple,list)):
646 n,args = n
646 n,args = n
647 count += n
647 count += n
648 return count
648 return count
649
649
650 def start(self, n):
650 def start(self, n):
651 """Start engines by profile or profile_dir.
651 """Start engines by profile or profile_dir.
652 `n` is ignored, and the `engines` config property is used instead.
652 `n` is ignored, and the `engines` config property is used instead.
653 """
653 """
654
654
655 dlist = []
655 dlist = []
656 for host, n in self.engines.iteritems():
656 for host, n in self.engines.iteritems():
657 if isinstance(n, (tuple, list)):
657 if isinstance(n, (tuple, list)):
658 n, args = n
658 n, args = n
659 else:
659 else:
660 args = copy.deepcopy(self.engine_args)
660 args = copy.deepcopy(self.engine_args)
661
661
662 if '@' in host:
662 if '@' in host:
663 user,host = host.split('@',1)
663 user,host = host.split('@',1)
664 else:
664 else:
665 user=None
665 user=None
666 for i in range(n):
666 for i in range(n):
667 if i > 0:
667 if i > 0:
668 time.sleep(self.delay)
668 time.sleep(self.delay)
669 el = self.launcher_class(work_dir=self.work_dir, config=self.config, log=self.log,
669 el = self.launcher_class(work_dir=self.work_dir, config=self.config, log=self.log,
670 profile_dir=self.profile_dir, cluster_id=self.cluster_id,
670 profile_dir=self.profile_dir, cluster_id=self.cluster_id,
671 )
671 )
672
672
673 # Copy the engine args over to each engine launcher.
673 # Copy the engine args over to each engine launcher.
674 el.engine_cmd = self.engine_cmd
674 el.engine_cmd = self.engine_cmd
675 el.engine_args = args
675 el.engine_args = args
676 el.on_stop(self._notice_engine_stopped)
676 el.on_stop(self._notice_engine_stopped)
677 d = el.start(user=user, hostname=host)
677 d = el.start(user=user, hostname=host)
678 self.launchers[ "%s/%i" % (host,i) ] = el
678 self.launchers[ "%s/%i" % (host,i) ] = el
679 dlist.append(d)
679 dlist.append(d)
680 self.notify_start(dlist)
680 self.notify_start(dlist)
681 return dlist
681 return dlist
682
682
683
683
684
684
685 #-----------------------------------------------------------------------------
685 #-----------------------------------------------------------------------------
686 # Windows HPC Server 2008 scheduler launchers
686 # Windows HPC Server 2008 scheduler launchers
687 #-----------------------------------------------------------------------------
687 #-----------------------------------------------------------------------------
688
688
689
689
690 # This is only used on Windows.
690 # This is only used on Windows.
691 def find_job_cmd():
691 def find_job_cmd():
692 if WINDOWS:
692 if WINDOWS:
693 try:
693 try:
694 return find_cmd('job')
694 return find_cmd('job')
695 except (FindCmdError, ImportError):
695 except (FindCmdError, ImportError):
696 # ImportError will be raised if win32api is not installed
696 # ImportError will be raised if win32api is not installed
697 return 'job'
697 return 'job'
698 else:
698 else:
699 return 'job'
699 return 'job'
700
700
701
701
702 class WindowsHPCLauncher(BaseLauncher):
702 class WindowsHPCLauncher(BaseLauncher):
703
703
704 job_id_regexp = Unicode(r'\d+', config=True,
704 job_id_regexp = Unicode(r'\d+', config=True,
705 help="""A regular expression used to get the job id from the output of the
705 help="""A regular expression used to get the job id from the output of the
706 submit_command. """
706 submit_command. """
707 )
707 )
708 job_file_name = Unicode(u'ipython_job.xml', config=True,
708 job_file_name = Unicode(u'ipython_job.xml', config=True,
709 help="The filename of the instantiated job script.")
709 help="The filename of the instantiated job script.")
710 # The full path to the instantiated job script. This gets made dynamically
710 # The full path to the instantiated job script. This gets made dynamically
711 # by combining the work_dir with the job_file_name.
711 # by combining the work_dir with the job_file_name.
712 job_file = Unicode(u'')
712 job_file = Unicode(u'')
713 scheduler = Unicode('', config=True,
713 scheduler = Unicode('', config=True,
714 help="The hostname of the scheduler to submit the job to.")
714 help="The hostname of the scheduler to submit the job to.")
715 job_cmd = Unicode(find_job_cmd(), config=True,
715 job_cmd = Unicode(find_job_cmd(), config=True,
716 help="The command for submitting jobs.")
716 help="The command for submitting jobs.")
717
717
718 def __init__(self, work_dir=u'.', config=None, **kwargs):
718 def __init__(self, work_dir=u'.', config=None, **kwargs):
719 super(WindowsHPCLauncher, self).__init__(
719 super(WindowsHPCLauncher, self).__init__(
720 work_dir=work_dir, config=config, **kwargs
720 work_dir=work_dir, config=config, **kwargs
721 )
721 )
722
722
723 @property
723 @property
724 def job_file(self):
724 def job_file(self):
725 return os.path.join(self.work_dir, self.job_file_name)
725 return os.path.join(self.work_dir, self.job_file_name)
726
726
727 def write_job_file(self, n):
727 def write_job_file(self, n):
728 raise NotImplementedError("Implement write_job_file in a subclass.")
728 raise NotImplementedError("Implement write_job_file in a subclass.")
729
729
730 def find_args(self):
730 def find_args(self):
731 return [u'job.exe']
731 return [u'job.exe']
732
732
733 def parse_job_id(self, output):
733 def parse_job_id(self, output):
734 """Take the output of the submit command and return the job id."""
734 """Take the output of the submit command and return the job id."""
735 m = re.search(self.job_id_regexp, output)
735 m = re.search(self.job_id_regexp, output)
736 if m is not None:
736 if m is not None:
737 job_id = m.group()
737 job_id = m.group()
738 else:
738 else:
739 raise LauncherError("Job id couldn't be determined: %s" % output)
739 raise LauncherError("Job id couldn't be determined: %s" % output)
740 self.job_id = job_id
740 self.job_id = job_id
741 self.log.info('Job started with id: %r', job_id)
741 self.log.info('Job started with id: %r', job_id)
742 return job_id
742 return job_id
743
743
744 def start(self, n):
744 def start(self, n):
745 """Start n copies of the process using the Win HPC job scheduler."""
745 """Start n copies of the process using the Win HPC job scheduler."""
746 self.write_job_file(n)
746 self.write_job_file(n)
747 args = [
747 args = [
748 'submit',
748 'submit',
749 '/jobfile:%s' % self.job_file,
749 '/jobfile:%s' % self.job_file,
750 '/scheduler:%s' % self.scheduler
750 '/scheduler:%s' % self.scheduler
751 ]
751 ]
752 self.log.debug("Starting Win HPC Job: %s" % (self.job_cmd + ' ' + ' '.join(args),))
752 self.log.debug("Starting Win HPC Job: %s" % (self.job_cmd + ' ' + ' '.join(args),))
753
753
754 output = check_output([self.job_cmd]+args,
754 output = check_output([self.job_cmd]+args,
755 env=os.environ,
755 env=os.environ,
756 cwd=self.work_dir,
756 cwd=self.work_dir,
757 stderr=STDOUT
757 stderr=STDOUT
758 )
758 )
759 job_id = self.parse_job_id(output)
759 job_id = self.parse_job_id(output)
760 self.notify_start(job_id)
760 self.notify_start(job_id)
761 return job_id
761 return job_id
762
762
763 def stop(self):
763 def stop(self):
764 args = [
764 args = [
765 'cancel',
765 'cancel',
766 self.job_id,
766 self.job_id,
767 '/scheduler:%s' % self.scheduler
767 '/scheduler:%s' % self.scheduler
768 ]
768 ]
769 self.log.info("Stopping Win HPC Job: %s" % (self.job_cmd + ' ' + ' '.join(args),))
769 self.log.info("Stopping Win HPC Job: %s" % (self.job_cmd + ' ' + ' '.join(args),))
770 try:
770 try:
771 output = check_output([self.job_cmd]+args,
771 output = check_output([self.job_cmd]+args,
772 env=os.environ,
772 env=os.environ,
773 cwd=self.work_dir,
773 cwd=self.work_dir,
774 stderr=STDOUT
774 stderr=STDOUT
775 )
775 )
776 except:
776 except:
777 output = 'The job already appears to be stoppped: %r' % self.job_id
777 output = 'The job already appears to be stoppped: %r' % self.job_id
778 self.notify_stop(dict(job_id=self.job_id, output=output)) # Pass the output of the kill cmd
778 self.notify_stop(dict(job_id=self.job_id, output=output)) # Pass the output of the kill cmd
779 return output
779 return output
780
780
781
781
782 class WindowsHPCControllerLauncher(WindowsHPCLauncher, ClusterAppMixin):
782 class WindowsHPCControllerLauncher(WindowsHPCLauncher, ClusterAppMixin):
783
783
784 job_file_name = Unicode(u'ipcontroller_job.xml', config=True,
784 job_file_name = Unicode(u'ipcontroller_job.xml', config=True,
785 help="WinHPC xml job file.")
785 help="WinHPC xml job file.")
786 controller_args = List([], config=False,
786 controller_args = List([], config=False,
787 help="extra args to pass to ipcontroller")
787 help="extra args to pass to ipcontroller")
788
788
789 def write_job_file(self, n):
789 def write_job_file(self, n):
790 job = IPControllerJob(config=self.config)
790 job = IPControllerJob(config=self.config)
791
791
792 t = IPControllerTask(config=self.config)
792 t = IPControllerTask(config=self.config)
793 # The tasks work directory is *not* the actual work directory of
793 # The tasks work directory is *not* the actual work directory of
794 # the controller. It is used as the base path for the stdout/stderr
794 # the controller. It is used as the base path for the stdout/stderr
795 # files that the scheduler redirects to.
795 # files that the scheduler redirects to.
796 t.work_directory = self.profile_dir
796 t.work_directory = self.profile_dir
797 # Add the profile_dir and from self.start().
797 # Add the profile_dir and from self.start().
798 t.controller_args.extend(self.cluster_args)
798 t.controller_args.extend(self.cluster_args)
799 t.controller_args.extend(self.controller_args)
799 t.controller_args.extend(self.controller_args)
800 job.add_task(t)
800 job.add_task(t)
801
801
802 self.log.debug("Writing job description file: %s", self.job_file)
802 self.log.debug("Writing job description file: %s", self.job_file)
803 job.write(self.job_file)
803 job.write(self.job_file)
804
804
805 @property
805 @property
806 def job_file(self):
806 def job_file(self):
807 return os.path.join(self.profile_dir, self.job_file_name)
807 return os.path.join(self.profile_dir, self.job_file_name)
808
808
809 def start(self):
809 def start(self):
810 """Start the controller by profile_dir."""
810 """Start the controller by profile_dir."""
811 return super(WindowsHPCControllerLauncher, self).start(1)
811 return super(WindowsHPCControllerLauncher, self).start(1)
812
812
813
813
814 class WindowsHPCEngineSetLauncher(WindowsHPCLauncher, ClusterAppMixin):
814 class WindowsHPCEngineSetLauncher(WindowsHPCLauncher, ClusterAppMixin):
815
815
816 job_file_name = Unicode(u'ipengineset_job.xml', config=True,
816 job_file_name = Unicode(u'ipengineset_job.xml', config=True,
817 help="jobfile for ipengines job")
817 help="jobfile for ipengines job")
818 engine_args = List([], config=False,
818 engine_args = List([], config=False,
819 help="extra args to pas to ipengine")
819 help="extra args to pas to ipengine")
820
820
821 def write_job_file(self, n):
821 def write_job_file(self, n):
822 job = IPEngineSetJob(config=self.config)
822 job = IPEngineSetJob(config=self.config)
823
823
824 for i in range(n):
824 for i in range(n):
825 t = IPEngineTask(config=self.config)
825 t = IPEngineTask(config=self.config)
826 # The tasks work directory is *not* the actual work directory of
826 # The tasks work directory is *not* the actual work directory of
827 # the engine. It is used as the base path for the stdout/stderr
827 # the engine. It is used as the base path for the stdout/stderr
828 # files that the scheduler redirects to.
828 # files that the scheduler redirects to.
829 t.work_directory = self.profile_dir
829 t.work_directory = self.profile_dir
830 # Add the profile_dir and from self.start().
830 # Add the profile_dir and from self.start().
831 t.engine_args.extend(self.cluster_args)
831 t.engine_args.extend(self.cluster_args)
832 t.engine_args.extend(self.engine_args)
832 t.engine_args.extend(self.engine_args)
833 job.add_task(t)
833 job.add_task(t)
834
834
835 self.log.debug("Writing job description file: %s", self.job_file)
835 self.log.debug("Writing job description file: %s", self.job_file)
836 job.write(self.job_file)
836 job.write(self.job_file)
837
837
838 @property
838 @property
839 def job_file(self):
839 def job_file(self):
840 return os.path.join(self.profile_dir, self.job_file_name)
840 return os.path.join(self.profile_dir, self.job_file_name)
841
841
842 def start(self, n):
842 def start(self, n):
843 """Start the controller by profile_dir."""
843 """Start the controller by profile_dir."""
844 return super(WindowsHPCEngineSetLauncher, self).start(n)
844 return super(WindowsHPCEngineSetLauncher, self).start(n)
845
845
846
846
847 #-----------------------------------------------------------------------------
847 #-----------------------------------------------------------------------------
848 # Batch (PBS) system launchers
848 # Batch (PBS) system launchers
849 #-----------------------------------------------------------------------------
849 #-----------------------------------------------------------------------------
850
850
851 class BatchClusterAppMixin(ClusterAppMixin):
851 class BatchClusterAppMixin(ClusterAppMixin):
852 """ClusterApp mixin that updates the self.context dict, rather than cl-args."""
852 """ClusterApp mixin that updates the self.context dict, rather than cl-args."""
853 def _profile_dir_changed(self, name, old, new):
853 def _profile_dir_changed(self, name, old, new):
854 self.context[name] = new
854 self.context[name] = new
855 _cluster_id_changed = _profile_dir_changed
855 _cluster_id_changed = _profile_dir_changed
856
856
857 def _profile_dir_default(self):
857 def _profile_dir_default(self):
858 self.context['profile_dir'] = ''
858 self.context['profile_dir'] = ''
859 return ''
859 return ''
860 def _cluster_id_default(self):
860 def _cluster_id_default(self):
861 self.context['cluster_id'] = ''
861 self.context['cluster_id'] = ''
862 return ''
862 return ''
863
863
864
864
865 class BatchSystemLauncher(BaseLauncher):
865 class BatchSystemLauncher(BaseLauncher):
866 """Launch an external process using a batch system.
866 """Launch an external process using a batch system.
867
867
868 This class is designed to work with UNIX batch systems like PBS, LSF,
868 This class is designed to work with UNIX batch systems like PBS, LSF,
869 GridEngine, etc. The overall model is that there are different commands
869 GridEngine, etc. The overall model is that there are different commands
870 like qsub, qdel, etc. that handle the starting and stopping of the process.
870 like qsub, qdel, etc. that handle the starting and stopping of the process.
871
871
872 This class also has the notion of a batch script. The ``batch_template``
872 This class also has the notion of a batch script. The ``batch_template``
873 attribute can be set to a string that is a template for the batch script.
873 attribute can be set to a string that is a template for the batch script.
874 This template is instantiated using string formatting. Thus the template can
874 This template is instantiated using string formatting. Thus the template can
875 use {n} fot the number of instances. Subclasses can add additional variables
875 use {n} fot the number of instances. Subclasses can add additional variables
876 to the template dict.
876 to the template dict.
877 """
877 """
878
878
879 # Subclasses must fill these in. See PBSEngineSet
879 # Subclasses must fill these in. See PBSEngineSet
880 submit_command = List([''], config=True,
880 submit_command = List([''], config=True,
881 help="The name of the command line program used to submit jobs.")
881 help="The name of the command line program used to submit jobs.")
882 delete_command = List([''], config=True,
882 delete_command = List([''], config=True,
883 help="The name of the command line program used to delete jobs.")
883 help="The name of the command line program used to delete jobs.")
884 job_id_regexp = Unicode('', config=True,
884 job_id_regexp = Unicode('', config=True,
885 help="""A regular expression used to get the job id from the output of the
885 help="""A regular expression used to get the job id from the output of the
886 submit_command.""")
886 submit_command.""")
887 batch_template = Unicode('', config=True,
887 batch_template = Unicode('', config=True,
888 help="The string that is the batch script template itself.")
888 help="The string that is the batch script template itself.")
889 batch_template_file = Unicode(u'', config=True,
889 batch_template_file = Unicode(u'', config=True,
890 help="The file that contains the batch template.")
890 help="The file that contains the batch template.")
891 batch_file_name = Unicode(u'batch_script', config=True,
891 batch_file_name = Unicode(u'batch_script', config=True,
892 help="The filename of the instantiated batch script.")
892 help="The filename of the instantiated batch script.")
893 queue = Unicode(u'', config=True,
893 queue = Unicode(u'', config=True,
894 help="The PBS Queue.")
894 help="The PBS Queue.")
895
895
896 def _queue_changed(self, name, old, new):
896 def _queue_changed(self, name, old, new):
897 self.context[name] = new
897 self.context[name] = new
898
898
899 n = Integer(1)
899 n = Integer(1)
900 _n_changed = _queue_changed
900 _n_changed = _queue_changed
901
901
902 # not configurable, override in subclasses
902 # not configurable, override in subclasses
903 # PBS Job Array regex
903 # PBS Job Array regex
904 job_array_regexp = Unicode('')
904 job_array_regexp = Unicode('')
905 job_array_template = Unicode('')
905 job_array_template = Unicode('')
906 # PBS Queue regex
906 # PBS Queue regex
907 queue_regexp = Unicode('')
907 queue_regexp = Unicode('')
908 queue_template = Unicode('')
908 queue_template = Unicode('')
909 # The default batch template, override in subclasses
909 # The default batch template, override in subclasses
910 default_template = Unicode('')
910 default_template = Unicode('')
911 # The full path to the instantiated batch script.
911 # The full path to the instantiated batch script.
912 batch_file = Unicode(u'')
912 batch_file = Unicode(u'')
913 # the format dict used with batch_template:
913 # the format dict used with batch_template:
914 context = Dict()
914 context = Dict()
915 def _context_default(self):
915 def _context_default(self):
916 """load the default context with the default values for the basic keys
916 """load the default context with the default values for the basic keys
917
917
918 because the _trait_changed methods only load the context if they
918 because the _trait_changed methods only load the context if they
919 are set to something other than the default value.
919 are set to something other than the default value.
920 """
920 """
921 return dict(n=1, queue=u'', profile_dir=u'', cluster_id=u'')
921 return dict(n=1, queue=u'', profile_dir=u'', cluster_id=u'')
922
922
923 # the Formatter instance for rendering the templates:
923 # the Formatter instance for rendering the templates:
924 formatter = Instance(EvalFormatter, (), {})
924 formatter = Instance(EvalFormatter, (), {})
925
925
926
926
927 def find_args(self):
927 def find_args(self):
928 return self.submit_command + [self.batch_file]
928 return self.submit_command + [self.batch_file]
929
929
930 def __init__(self, work_dir=u'.', config=None, **kwargs):
930 def __init__(self, work_dir=u'.', config=None, **kwargs):
931 super(BatchSystemLauncher, self).__init__(
931 super(BatchSystemLauncher, self).__init__(
932 work_dir=work_dir, config=config, **kwargs
932 work_dir=work_dir, config=config, **kwargs
933 )
933 )
934 self.batch_file = os.path.join(self.work_dir, self.batch_file_name)
934 self.batch_file = os.path.join(self.work_dir, self.batch_file_name)
935
935
936 def parse_job_id(self, output):
936 def parse_job_id(self, output):
937 """Take the output of the submit command and return the job id."""
937 """Take the output of the submit command and return the job id."""
938 m = re.search(self.job_id_regexp, output)
938 m = re.search(self.job_id_regexp, output)
939 if m is not None:
939 if m is not None:
940 job_id = m.group()
940 job_id = m.group()
941 else:
941 else:
942 raise LauncherError("Job id couldn't be determined: %s" % output)
942 raise LauncherError("Job id couldn't be determined: %s" % output)
943 self.job_id = job_id
943 self.job_id = job_id
944 self.log.info('Job submitted with job id: %r', job_id)
944 self.log.info('Job submitted with job id: %r', job_id)
945 return job_id
945 return job_id
946
946
947 def write_batch_script(self, n):
947 def write_batch_script(self, n):
948 """Instantiate and write the batch script to the work_dir."""
948 """Instantiate and write the batch script to the work_dir."""
949 self.n = n
949 self.n = n
950 # first priority is batch_template if set
950 # first priority is batch_template if set
951 if self.batch_template_file and not self.batch_template:
951 if self.batch_template_file and not self.batch_template:
952 # second priority is batch_template_file
952 # second priority is batch_template_file
953 with open(self.batch_template_file) as f:
953 with open(self.batch_template_file) as f:
954 self.batch_template = f.read()
954 self.batch_template = f.read()
955 if not self.batch_template:
955 if not self.batch_template:
956 # third (last) priority is default_template
956 # third (last) priority is default_template
957 self.batch_template = self.default_template
957 self.batch_template = self.default_template
958
958
959 # add jobarray or queue lines to user-specified template
959 # add jobarray or queue lines to user-specified template
960 # note that this is *only* when user did not specify a template.
960 # note that this is *only* when user did not specify a template.
961 regex = re.compile(self.job_array_regexp)
961 regex = re.compile(self.job_array_regexp)
962 # print regex.search(self.batch_template)
962 # print regex.search(self.batch_template)
963 if not regex.search(self.batch_template):
963 if not regex.search(self.batch_template):
964 self.log.debug("adding job array settings to batch script")
964 self.log.debug("adding job array settings to batch script")
965 firstline, rest = self.batch_template.split('\n',1)
965 firstline, rest = self.batch_template.split('\n',1)
966 self.batch_template = u'\n'.join([firstline, self.job_array_template, rest])
966 self.batch_template = u'\n'.join([firstline, self.job_array_template, rest])
967
967
968 regex = re.compile(self.queue_regexp)
968 regex = re.compile(self.queue_regexp)
969 # print regex.search(self.batch_template)
969 # print regex.search(self.batch_template)
970 if self.queue and not regex.search(self.batch_template):
970 if self.queue and not regex.search(self.batch_template):
971 self.log.debug("adding PBS queue settings to batch script")
971 self.log.debug("adding PBS queue settings to batch script")
972 firstline, rest = self.batch_template.split('\n',1)
972 firstline, rest = self.batch_template.split('\n',1)
973 self.batch_template = u'\n'.join([firstline, self.queue_template, rest])
973 self.batch_template = u'\n'.join([firstline, self.queue_template, rest])
974
974
975 script_as_string = self.formatter.format(self.batch_template, **self.context)
975 script_as_string = self.formatter.format(self.batch_template, **self.context)
976 self.log.debug('Writing batch script: %s', self.batch_file)
976 self.log.debug('Writing batch script: %s', self.batch_file)
977
977
978 with open(self.batch_file, 'w') as f:
978 with open(self.batch_file, 'w') as f:
979 f.write(script_as_string)
979 f.write(script_as_string)
980 os.chmod(self.batch_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
980 os.chmod(self.batch_file, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
981
981
982 def start(self, n):
982 def start(self, n):
983 """Start n copies of the process using a batch system."""
983 """Start n copies of the process using a batch system."""
984 self.log.debug("Starting %s: %r", self.__class__.__name__, self.args)
984 self.log.debug("Starting %s: %r", self.__class__.__name__, self.args)
985 # Here we save profile_dir in the context so they
985 # Here we save profile_dir in the context so they
986 # can be used in the batch script template as {profile_dir}
986 # can be used in the batch script template as {profile_dir}
987 self.write_batch_script(n)
987 self.write_batch_script(n)
988 output = check_output(self.args, env=os.environ)
988 output = check_output(self.args, env=os.environ)
989
989
990 job_id = self.parse_job_id(output)
990 job_id = self.parse_job_id(output)
991 self.notify_start(job_id)
991 self.notify_start(job_id)
992 return job_id
992 return job_id
993
993
994 def stop(self):
994 def stop(self):
995 output = check_output(self.delete_command+[self.job_id], env=os.environ)
995 output = check_output(self.delete_command+[self.job_id], env=os.environ)
996 self.notify_stop(dict(job_id=self.job_id, output=output)) # Pass the output of the kill cmd
996 self.notify_stop(dict(job_id=self.job_id, output=output)) # Pass the output of the kill cmd
997 return output
997 return output
998
998
999
999
1000 class PBSLauncher(BatchSystemLauncher):
1000 class PBSLauncher(BatchSystemLauncher):
1001 """A BatchSystemLauncher subclass for PBS."""
1001 """A BatchSystemLauncher subclass for PBS."""
1002
1002
1003 submit_command = List(['qsub'], config=True,
1003 submit_command = List(['qsub'], config=True,
1004 help="The PBS submit command ['qsub']")
1004 help="The PBS submit command ['qsub']")
1005 delete_command = List(['qdel'], config=True,
1005 delete_command = List(['qdel'], config=True,
1006 help="The PBS delete command ['qsub']")
1006 help="The PBS delete command ['qsub']")
1007 job_id_regexp = Unicode(r'\d+', config=True,
1007 job_id_regexp = Unicode(r'\d+', config=True,
1008 help="Regular expresion for identifying the job ID [r'\d+']")
1008 help="Regular expresion for identifying the job ID [r'\d+']")
1009
1009
1010 batch_file = Unicode(u'')
1010 batch_file = Unicode(u'')
1011 job_array_regexp = Unicode('#PBS\W+-t\W+[\w\d\-\$]+')
1011 job_array_regexp = Unicode('#PBS\W+-t\W+[\w\d\-\$]+')
1012 job_array_template = Unicode('#PBS -t 1-{n}')
1012 job_array_template = Unicode('#PBS -t 1-{n}')
1013 queue_regexp = Unicode('#PBS\W+-q\W+\$?\w+')
1013 queue_regexp = Unicode('#PBS\W+-q\W+\$?\w+')
1014 queue_template = Unicode('#PBS -q {queue}')
1014 queue_template = Unicode('#PBS -q {queue}')
1015
1015
1016
1016
1017 class PBSControllerLauncher(PBSLauncher, BatchClusterAppMixin):
1017 class PBSControllerLauncher(PBSLauncher, BatchClusterAppMixin):
1018 """Launch a controller using PBS."""
1018 """Launch a controller using PBS."""
1019
1019
1020 batch_file_name = Unicode(u'pbs_controller', config=True,
1020 batch_file_name = Unicode(u'pbs_controller', config=True,
1021 help="batch file name for the controller job.")
1021 help="batch file name for the controller job.")
1022 default_template= Unicode("""#!/bin/sh
1022 default_template= Unicode("""#!/bin/sh
1023 #PBS -V
1023 #PBS -V
1024 #PBS -N ipcontroller
1024 #PBS -N ipcontroller
1025 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1025 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1026 """%(' '.join(ipcontroller_cmd_argv)))
1026 """%(' '.join(ipcontroller_cmd_argv)))
1027
1027
1028
1028
1029 def start(self):
1029 def start(self):
1030 """Start the controller by profile or profile_dir."""
1030 """Start the controller by profile or profile_dir."""
1031 return super(PBSControllerLauncher, self).start(1)
1031 return super(PBSControllerLauncher, self).start(1)
1032
1032
1033
1033
1034 class PBSEngineSetLauncher(PBSLauncher, BatchClusterAppMixin):
1034 class PBSEngineSetLauncher(PBSLauncher, BatchClusterAppMixin):
1035 """Launch Engines using PBS"""
1035 """Launch Engines using PBS"""
1036 batch_file_name = Unicode(u'pbs_engines', config=True,
1036 batch_file_name = Unicode(u'pbs_engines', config=True,
1037 help="batch file name for the engine(s) job.")
1037 help="batch file name for the engine(s) job.")
1038 default_template= Unicode(u"""#!/bin/sh
1038 default_template= Unicode(u"""#!/bin/sh
1039 #PBS -V
1039 #PBS -V
1040 #PBS -N ipengine
1040 #PBS -N ipengine
1041 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1041 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1042 """%(' '.join(ipengine_cmd_argv)))
1042 """%(' '.join(ipengine_cmd_argv)))
1043
1043
1044 def start(self, n):
1044 def start(self, n):
1045 """Start n engines by profile or profile_dir."""
1045 """Start n engines by profile or profile_dir."""
1046 return super(PBSEngineSetLauncher, self).start(n)
1046 return super(PBSEngineSetLauncher, self).start(n)
1047
1047
1048 #SGE is very similar to PBS
1048 #SGE is very similar to PBS
1049
1049
1050 class SGELauncher(PBSLauncher):
1050 class SGELauncher(PBSLauncher):
1051 """Sun GridEngine is a PBS clone with slightly different syntax"""
1051 """Sun GridEngine is a PBS clone with slightly different syntax"""
1052 job_array_regexp = Unicode('#\$\W+\-t')
1052 job_array_regexp = Unicode('#\$\W+\-t')
1053 job_array_template = Unicode('#$ -t 1-{n}')
1053 job_array_template = Unicode('#$ -t 1-{n}')
1054 queue_regexp = Unicode('#\$\W+-q\W+\$?\w+')
1054 queue_regexp = Unicode('#\$\W+-q\W+\$?\w+')
1055 queue_template = Unicode('#$ -q {queue}')
1055 queue_template = Unicode('#$ -q {queue}')
1056
1056
1057 class SGEControllerLauncher(SGELauncher, BatchClusterAppMixin):
1057 class SGEControllerLauncher(SGELauncher, BatchClusterAppMixin):
1058 """Launch a controller using SGE."""
1058 """Launch a controller using SGE."""
1059
1059
1060 batch_file_name = Unicode(u'sge_controller', config=True,
1060 batch_file_name = Unicode(u'sge_controller', config=True,
1061 help="batch file name for the ipontroller job.")
1061 help="batch file name for the ipontroller job.")
1062 default_template= Unicode(u"""#$ -V
1062 default_template= Unicode(u"""#$ -V
1063 #$ -S /bin/sh
1063 #$ -S /bin/sh
1064 #$ -N ipcontroller
1064 #$ -N ipcontroller
1065 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1065 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1066 """%(' '.join(ipcontroller_cmd_argv)))
1066 """%(' '.join(ipcontroller_cmd_argv)))
1067
1067
1068 def start(self):
1068 def start(self):
1069 """Start the controller by profile or profile_dir."""
1069 """Start the controller by profile or profile_dir."""
1070 return super(SGEControllerLauncher, self).start(1)
1070 return super(SGEControllerLauncher, self).start(1)
1071
1071
1072 class SGEEngineSetLauncher(SGELauncher, BatchClusterAppMixin):
1072 class SGEEngineSetLauncher(SGELauncher, BatchClusterAppMixin):
1073 """Launch Engines with SGE"""
1073 """Launch Engines with SGE"""
1074 batch_file_name = Unicode(u'sge_engines', config=True,
1074 batch_file_name = Unicode(u'sge_engines', config=True,
1075 help="batch file name for the engine(s) job.")
1075 help="batch file name for the engine(s) job.")
1076 default_template = Unicode("""#$ -V
1076 default_template = Unicode("""#$ -V
1077 #$ -S /bin/sh
1077 #$ -S /bin/sh
1078 #$ -N ipengine
1078 #$ -N ipengine
1079 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1079 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1080 """%(' '.join(ipengine_cmd_argv)))
1080 """%(' '.join(ipengine_cmd_argv)))
1081
1081
1082 def start(self, n):
1082 def start(self, n):
1083 """Start n engines by profile or profile_dir."""
1083 """Start n engines by profile or profile_dir."""
1084 return super(SGEEngineSetLauncher, self).start(n)
1084 return super(SGEEngineSetLauncher, self).start(n)
1085
1085
1086
1086
1087 # LSF launchers
1087 # LSF launchers
1088
1088
1089 class LSFLauncher(BatchSystemLauncher):
1089 class LSFLauncher(BatchSystemLauncher):
1090 """A BatchSystemLauncher subclass for LSF."""
1090 """A BatchSystemLauncher subclass for LSF."""
1091
1091
1092 submit_command = List(['bsub'], config=True,
1092 submit_command = List(['bsub'], config=True,
1093 help="The PBS submit command ['bsub']")
1093 help="The PBS submit command ['bsub']")
1094 delete_command = List(['bkill'], config=True,
1094 delete_command = List(['bkill'], config=True,
1095 help="The PBS delete command ['bkill']")
1095 help="The PBS delete command ['bkill']")
1096 job_id_regexp = Unicode(r'\d+', config=True,
1096 job_id_regexp = Unicode(r'\d+', config=True,
1097 help="Regular expresion for identifying the job ID [r'\d+']")
1097 help="Regular expresion for identifying the job ID [r'\d+']")
1098
1098
1099 batch_file = Unicode(u'')
1099 batch_file = Unicode(u'')
1100 job_array_regexp = Unicode('#BSUB[ \t]-J+\w+\[\d+-\d+\]')
1100 job_array_regexp = Unicode('#BSUB[ \t]-J+\w+\[\d+-\d+\]')
1101 job_array_template = Unicode('#BSUB -J ipengine[1-{n}]')
1101 job_array_template = Unicode('#BSUB -J ipengine[1-{n}]')
1102 queue_regexp = Unicode('#BSUB[ \t]+-q[ \t]+\w+')
1102 queue_regexp = Unicode('#BSUB[ \t]+-q[ \t]+\w+')
1103 queue_template = Unicode('#BSUB -q {queue}')
1103 queue_template = Unicode('#BSUB -q {queue}')
1104
1104
1105 def start(self, n):
1105 def start(self, n):
1106 """Start n copies of the process using LSF batch system.
1106 """Start n copies of the process using LSF batch system.
1107 This cant inherit from the base class because bsub expects
1107 This cant inherit from the base class because bsub expects
1108 to be piped a shell script in order to honor the #BSUB directives :
1108 to be piped a shell script in order to honor the #BSUB directives :
1109 bsub < script
1109 bsub < script
1110 """
1110 """
1111 # Here we save profile_dir in the context so they
1111 # Here we save profile_dir in the context so they
1112 # can be used in the batch script template as {profile_dir}
1112 # can be used in the batch script template as {profile_dir}
1113 self.write_batch_script(n)
1113 self.write_batch_script(n)
1114 #output = check_output(self.args, env=os.environ)
1114 #output = check_output(self.args, env=os.environ)
1115 piped_cmd = self.args[0]+'<\"'+self.args[1]+'\"'
1115 piped_cmd = self.args[0]+'<\"'+self.args[1]+'\"'
1116 self.log.debug("Starting %s: %s", self.__class__.__name__, piped_cmd)
1116 self.log.debug("Starting %s: %s", self.__class__.__name__, piped_cmd)
1117 p = Popen(piped_cmd, shell=True,env=os.environ,stdout=PIPE)
1117 p = Popen(piped_cmd, shell=True,env=os.environ,stdout=PIPE)
1118 output,err = p.communicate()
1118 output,err = p.communicate()
1119 job_id = self.parse_job_id(output)
1119 job_id = self.parse_job_id(output)
1120 self.notify_start(job_id)
1120 self.notify_start(job_id)
1121 return job_id
1121 return job_id
1122
1122
1123
1123
1124 class LSFControllerLauncher(LSFLauncher, BatchClusterAppMixin):
1124 class LSFControllerLauncher(LSFLauncher, BatchClusterAppMixin):
1125 """Launch a controller using LSF."""
1125 """Launch a controller using LSF."""
1126
1126
1127 batch_file_name = Unicode(u'lsf_controller', config=True,
1127 batch_file_name = Unicode(u'lsf_controller', config=True,
1128 help="batch file name for the controller job.")
1128 help="batch file name for the controller job.")
1129 default_template= Unicode("""#!/bin/sh
1129 default_template= Unicode("""#!/bin/sh
1130 #BSUB -J ipcontroller
1130 #BSUB -J ipcontroller
1131 #BSUB -oo ipcontroller.o.%%J
1131 #BSUB -oo ipcontroller.o.%%J
1132 #BSUB -eo ipcontroller.e.%%J
1132 #BSUB -eo ipcontroller.e.%%J
1133 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1133 %s --log-to-file --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1134 """%(' '.join(ipcontroller_cmd_argv)))
1134 """%(' '.join(ipcontroller_cmd_argv)))
1135
1135
1136 def start(self):
1136 def start(self):
1137 """Start the controller by profile or profile_dir."""
1137 """Start the controller by profile or profile_dir."""
1138 return super(LSFControllerLauncher, self).start(1)
1138 return super(LSFControllerLauncher, self).start(1)
1139
1139
1140
1140
1141 class LSFEngineSetLauncher(LSFLauncher, BatchClusterAppMixin):
1141 class LSFEngineSetLauncher(LSFLauncher, BatchClusterAppMixin):
1142 """Launch Engines using LSF"""
1142 """Launch Engines using LSF"""
1143 batch_file_name = Unicode(u'lsf_engines', config=True,
1143 batch_file_name = Unicode(u'lsf_engines', config=True,
1144 help="batch file name for the engine(s) job.")
1144 help="batch file name for the engine(s) job.")
1145 default_template= Unicode(u"""#!/bin/sh
1145 default_template= Unicode(u"""#!/bin/sh
1146 #BSUB -oo ipengine.o.%%J
1146 #BSUB -oo ipengine.o.%%J
1147 #BSUB -eo ipengine.e.%%J
1147 #BSUB -eo ipengine.e.%%J
1148 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1148 %s --profile-dir="{profile_dir}" --cluster-id="{cluster_id}"
1149 """%(' '.join(ipengine_cmd_argv)))
1149 """%(' '.join(ipengine_cmd_argv)))
1150
1150
1151 def start(self, n):
1151 def start(self, n):
1152 """Start n engines by profile or profile_dir."""
1152 """Start n engines by profile or profile_dir."""
1153 return super(LSFEngineSetLauncher, self).start(n)
1153 return super(LSFEngineSetLauncher, self).start(n)
1154
1154
1155
1155
1156 #-----------------------------------------------------------------------------
1156 #-----------------------------------------------------------------------------
1157 # A launcher for ipcluster itself!
1157 # A launcher for ipcluster itself!
1158 #-----------------------------------------------------------------------------
1158 #-----------------------------------------------------------------------------
1159
1159
1160
1160
1161 class IPClusterLauncher(LocalProcessLauncher):
1161 class IPClusterLauncher(LocalProcessLauncher):
1162 """Launch the ipcluster program in an external process."""
1162 """Launch the ipcluster program in an external process."""
1163
1163
1164 ipcluster_cmd = List(ipcluster_cmd_argv, config=True,
1164 ipcluster_cmd = List(ipcluster_cmd_argv, config=True,
1165 help="Popen command for ipcluster")
1165 help="Popen command for ipcluster")
1166 ipcluster_args = List(
1166 ipcluster_args = List(
1167 ['--clean-logs', '--log-to-file', '--log-level=%i'%logging.INFO], config=True,
1167 ['--clean-logs=True', '--log-to-file', '--log-level=%i'%logging.INFO], config=True,
1168 help="Command line arguments to pass to ipcluster.")
1168 help="Command line arguments to pass to ipcluster.")
1169 ipcluster_subcommand = Unicode('start')
1169 ipcluster_subcommand = Unicode('start')
1170 ipcluster_profile = Unicode('default')
1170 ipcluster_n = Integer(2)
1171 ipcluster_n = Integer(2)
1171
1172
1172 def find_args(self):
1173 def find_args(self):
1173 return self.ipcluster_cmd + [self.ipcluster_subcommand] + \
1174 return self.ipcluster_cmd + [self.ipcluster_subcommand] + \
1174 ['--n=%i'%self.ipcluster_n] + self.ipcluster_args
1175 ['--n=%i'%self.ipcluster_n, '--profile=%s'%self.ipcluster_profile] + \
1176 self.ipcluster_args
1175
1177
1176 def start(self):
1178 def start(self):
1177 return super(IPClusterLauncher, self).start()
1179 return super(IPClusterLauncher, self).start()
1178
1180
1179 #-----------------------------------------------------------------------------
1181 #-----------------------------------------------------------------------------
1180 # Collections of launchers
1182 # Collections of launchers
1181 #-----------------------------------------------------------------------------
1183 #-----------------------------------------------------------------------------
1182
1184
1183 local_launchers = [
1185 local_launchers = [
1184 LocalControllerLauncher,
1186 LocalControllerLauncher,
1185 LocalEngineLauncher,
1187 LocalEngineLauncher,
1186 LocalEngineSetLauncher,
1188 LocalEngineSetLauncher,
1187 ]
1189 ]
1188 mpi_launchers = [
1190 mpi_launchers = [
1189 MPILauncher,
1191 MPILauncher,
1190 MPIControllerLauncher,
1192 MPIControllerLauncher,
1191 MPIEngineSetLauncher,
1193 MPIEngineSetLauncher,
1192 ]
1194 ]
1193 ssh_launchers = [
1195 ssh_launchers = [
1194 SSHLauncher,
1196 SSHLauncher,
1195 SSHControllerLauncher,
1197 SSHControllerLauncher,
1196 SSHEngineLauncher,
1198 SSHEngineLauncher,
1197 SSHEngineSetLauncher,
1199 SSHEngineSetLauncher,
1198 ]
1200 ]
1199 winhpc_launchers = [
1201 winhpc_launchers = [
1200 WindowsHPCLauncher,
1202 WindowsHPCLauncher,
1201 WindowsHPCControllerLauncher,
1203 WindowsHPCControllerLauncher,
1202 WindowsHPCEngineSetLauncher,
1204 WindowsHPCEngineSetLauncher,
1203 ]
1205 ]
1204 pbs_launchers = [
1206 pbs_launchers = [
1205 PBSLauncher,
1207 PBSLauncher,
1206 PBSControllerLauncher,
1208 PBSControllerLauncher,
1207 PBSEngineSetLauncher,
1209 PBSEngineSetLauncher,
1208 ]
1210 ]
1209 sge_launchers = [
1211 sge_launchers = [
1210 SGELauncher,
1212 SGELauncher,
1211 SGEControllerLauncher,
1213 SGEControllerLauncher,
1212 SGEEngineSetLauncher,
1214 SGEEngineSetLauncher,
1213 ]
1215 ]
1214 lsf_launchers = [
1216 lsf_launchers = [
1215 LSFLauncher,
1217 LSFLauncher,
1216 LSFControllerLauncher,
1218 LSFControllerLauncher,
1217 LSFEngineSetLauncher,
1219 LSFEngineSetLauncher,
1218 ]
1220 ]
1219 all_launchers = local_launchers + mpi_launchers + ssh_launchers + winhpc_launchers\
1221 all_launchers = local_launchers + mpi_launchers + ssh_launchers + winhpc_launchers\
1220 + pbs_launchers + sge_launchers + lsf_launchers
1222 + pbs_launchers + sge_launchers + lsf_launchers
1221
1223
General Comments 0
You need to be logged in to leave comments. Login now