##// END OF EJS Templates
fix race condition in profiledir creation.
James Porter -
Show More
@@ -1,274 +1,281 b''
1 1 # encoding: utf-8
2 2 """
3 3 An object for managing IPython profile directories.
4 4
5 5 Authors:
6 6
7 7 * Brian Granger
8 8 * Fernando Perez
9 9 * Min RK
10 10
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Copyright (C) 2011 The IPython Development Team
15 15 #
16 16 # Distributed under the terms of the BSD License. The full license is in
17 17 # the file COPYING, distributed as part of this software.
18 18 #-----------------------------------------------------------------------------
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Imports
22 22 #-----------------------------------------------------------------------------
23 23
24 24 import os
25 25 import shutil
26 26 import errno
27 import time
27 28
28 29 from IPython.config.configurable import LoggingConfigurable
29 30 from IPython.utils.path import get_ipython_package_dir, expand_path
30 31 from IPython.utils import py3compat
31 32 from IPython.utils.traitlets import Unicode, Bool
32 33
33 34 #-----------------------------------------------------------------------------
34 35 # Classes and functions
35 36 #-----------------------------------------------------------------------------
36 37
37 38
38 39 #-----------------------------------------------------------------------------
39 40 # Module errors
40 41 #-----------------------------------------------------------------------------
41 42
42 43 class ProfileDirError(Exception):
43 44 pass
44 45
45 46
46 47 #-----------------------------------------------------------------------------
47 48 # Class for managing profile directories
48 49 #-----------------------------------------------------------------------------
49 50
50 51 class ProfileDir(LoggingConfigurable):
51 52 """An object to manage the profile directory and its resources.
52 53
53 54 The profile directory is used by all IPython applications, to manage
54 55 configuration, logging and security.
55 56
56 57 This object knows how to find, create and manage these directories. This
57 58 should be used by any code that wants to handle profiles.
58 59 """
59 60
60 61 security_dir_name = Unicode('security')
61 62 log_dir_name = Unicode('log')
62 63 startup_dir_name = Unicode('startup')
63 64 pid_dir_name = Unicode('pid')
64 65 static_dir_name = Unicode('static')
65 66 security_dir = Unicode(u'')
66 67 log_dir = Unicode(u'')
67 68 startup_dir = Unicode(u'')
68 69 pid_dir = Unicode(u'')
69 70 static_dir = Unicode(u'')
70 71
71 72 location = Unicode(u'', config=True,
72 73 help="""Set the profile location directly. This overrides the logic used by the
73 74 `profile` option.""",
74 75 )
75 76
76 77 _location_isset = Bool(False) # flag for detecting multiply set location
77 78
78 79 def _location_changed(self, name, old, new):
79 80 if self._location_isset:
80 81 raise RuntimeError("Cannot set profile location more than once.")
81 82 self._location_isset = True
82 if not os.path.isdir(new):
83 os.makedirs(new)
84
83 num_tries = 0
84 max_tries = 5
85 while not os.path.isdir(new):
86 try:
87 os.makedirs(new)
88 except OSError:
89 if num_tries > max_tries:
90 raise
91 num_tries += 1
92 time.sleep(0.5)
93
85 94 # ensure config files exist:
86 95 self.security_dir = os.path.join(new, self.security_dir_name)
87 96 self.log_dir = os.path.join(new, self.log_dir_name)
88 97 self.startup_dir = os.path.join(new, self.startup_dir_name)
89 98 self.pid_dir = os.path.join(new, self.pid_dir_name)
90 99 self.static_dir = os.path.join(new, self.static_dir_name)
91 100 self.check_dirs()
92 101
93 102 def _log_dir_changed(self, name, old, new):
94 103 self.check_log_dir()
95
104
96 105 def _mkdir(self, path, mode=None):
97 106 """ensure a directory exists at a given path
98
107
99 108 This is a version of os.mkdir, with the following differences:
100
109
101 110 - returns True if it created the directory, False otherwise
102 111 - ignores EEXIST, protecting against race conditions where
103 112 the dir may have been created in between the check and
104 113 the creation
105 114 - sets permissions if requested and the dir already exists
106 115 """
107 116 if os.path.exists(path):
108 117 if mode and os.stat(path).st_mode != mode:
109 118 try:
110 119 os.chmod(path, mode)
111 120 except OSError:
112 121 self.log.warn(
113 122 "Could not set permissions on %s",
114 123 path
115 124 )
116 125 return False
117 126 try:
118 127 if mode:
119 128 os.mkdir(path, mode)
120 129 else:
121 130 os.mkdir(path)
122 131 except OSError as e:
123 132 if e.errno == errno.EEXIST:
124 133 return False
125 134 else:
126 135 raise
127
136
128 137 return True
129 138
130 139 def check_log_dir(self):
131 140 self._mkdir(self.log_dir)
132 141
133 142 def _startup_dir_changed(self, name, old, new):
134 143 self.check_startup_dir()
135 144
136 145 def check_startup_dir(self):
137 146 self._mkdir(self.startup_dir)
138 147
139 148 readme = os.path.join(self.startup_dir, 'README')
140 149 src = os.path.join(get_ipython_package_dir(), u'config', u'profile', u'README_STARTUP')
141 150
142 151 if not os.path.exists(src):
143 152 self.log.warn("Could not copy README_STARTUP to startup dir. Source file %s does not exist.", src)
144 153
145 154 if os.path.exists(src) and not os.path.exists(readme):
146 155 shutil.copy(src, readme)
147 156
148 157 def _security_dir_changed(self, name, old, new):
149 158 self.check_security_dir()
150 159
151 160 def check_security_dir(self):
152 161 self._mkdir(self.security_dir, 0o40700)
153 162
154 163 def _pid_dir_changed(self, name, old, new):
155 164 self.check_pid_dir()
156 165
157 166 def check_pid_dir(self):
158 167 self._mkdir(self.pid_dir, 0o40700)
159 168
160 169 def _static_dir_changed(self, name, old, new):
161 170 self.check_startup_dir()
162 171
163 172 def check_static_dir(self):
164 173 self._mkdir(self.static_dir)
165 174 custom = os.path.join(self.static_dir, 'custom')
166 175 self._mkdir(custom)
167 176 from IPython.html import DEFAULT_STATIC_FILES_PATH
168 177 for fname in ('custom.js', 'custom.css'):
169 178 src = os.path.join(DEFAULT_STATIC_FILES_PATH, 'custom', fname)
170 179 dest = os.path.join(custom, fname)
171 180 if not os.path.exists(src):
172 181 self.log.warn("Could not copy default file to static dir. Source file %s does not exist.", src)
173 182 continue
174 183 if not os.path.exists(dest):
175 184 shutil.copy(src, dest)
176 185
177 186 def check_dirs(self):
178 187 self.check_security_dir()
179 188 self.check_log_dir()
180 189 self.check_pid_dir()
181 190 self.check_startup_dir()
182 191 self.check_static_dir()
183 192
184 193 def copy_config_file(self, config_file, path=None, overwrite=False):
185 194 """Copy a default config file into the active profile directory.
186 195
187 196 Default configuration files are kept in :mod:`IPython.config.default`.
188 197 This function moves these from that location to the working profile
189 198 directory.
190 199 """
191 200 dst = os.path.join(self.location, config_file)
192 201 if os.path.isfile(dst) and not overwrite:
193 202 return False
194 203 if path is None:
195 204 path = os.path.join(get_ipython_package_dir(), u'config', u'profile', u'default')
196 205 src = os.path.join(path, config_file)
197 206 shutil.copy(src, dst)
198 207 return True
199 208
200 209 @classmethod
201 210 def create_profile_dir(cls, profile_dir, config=None):
202 211 """Create a new profile directory given a full path.
203 212
204 213 Parameters
205 214 ----------
206 215 profile_dir : str
207 216 The full path to the profile directory. If it does exist, it will
208 217 be used. If not, it will be created.
209 218 """
210 219 return cls(location=profile_dir, config=config)
211 220
212 221 @classmethod
213 222 def create_profile_dir_by_name(cls, path, name=u'default', config=None):
214 223 """Create a profile dir by profile name and path.
215 224
216 225 Parameters
217 226 ----------
218 227 path : unicode
219 228 The path (directory) to put the profile directory in.
220 229 name : unicode
221 230 The name of the profile. The name of the profile directory will
222 231 be "profile_<profile>".
223 232 """
224 233 if not os.path.isdir(path):
225 234 raise ProfileDirError('Directory not found: %s' % path)
226 235 profile_dir = os.path.join(path, u'profile_' + name)
227 236 return cls(location=profile_dir, config=config)
228 237
229 238 @classmethod
230 239 def find_profile_dir_by_name(cls, ipython_dir, name=u'default', config=None):
231 240 """Find an existing profile dir by profile name, return its ProfileDir.
232 241
233 242 This searches through a sequence of paths for a profile dir. If it
234 243 is not found, a :class:`ProfileDirError` exception will be raised.
235 244
236 245 The search path algorithm is:
237 246 1. ``py3compat.getcwd()``
238 247 2. ``ipython_dir``
239 248
240 249 Parameters
241 250 ----------
242 251 ipython_dir : unicode or str
243 252 The IPython directory to use.
244 253 name : unicode or str
245 254 The name of the profile. The name of the profile directory
246 255 will be "profile_<profile>".
247 256 """
248 257 dirname = u'profile_' + name
249 258 paths = [py3compat.getcwd(), ipython_dir]
250 259 for p in paths:
251 260 profile_dir = os.path.join(p, dirname)
252 261 if os.path.isdir(profile_dir):
253 262 return cls(location=profile_dir, config=config)
254 263 else:
255 264 raise ProfileDirError('Profile directory not found in paths: %s' % dirname)
256 265
257 266 @classmethod
258 267 def find_profile_dir(cls, profile_dir, config=None):
259 268 """Find/create a profile dir and return its ProfileDir.
260 269
261 270 This will create the profile directory if it doesn't exist.
262 271
263 272 Parameters
264 273 ----------
265 274 profile_dir : unicode or str
266 275 The path of the profile directory. This is expanded using
267 276 :func:`IPython.utils.genutils.expand_path`.
268 277 """
269 278 profile_dir = expand_path(profile_dir)
270 279 if not os.path.isdir(profile_dir):
271 280 raise ProfileDirError('Profile directory not found: %s' % profile_dir)
272 281 return cls(location=profile_dir, config=config)
273
274
General Comments 0
You need to be logged in to leave comments. Login now