##// END OF EJS Templates
vfs: allow to pass more argument to audit...
Boris Feld -
r33435:456626e9 default
parent child Browse files
Show More
@@ -1,215 +1,215 b''
1 1 from __future__ import absolute_import
2 2
3 3 import errno
4 4 import os
5 5 import posixpath
6 6 import stat
7 7
8 8 from .i18n import _
9 9 from . import (
10 10 encoding,
11 11 error,
12 12 pycompat,
13 13 util,
14 14 )
15 15
16 16 def _lowerclean(s):
17 17 return encoding.hfsignoreclean(s.lower())
18 18
19 19 class pathauditor(object):
20 20 '''ensure that a filesystem path contains no banned components.
21 21 the following properties of a path are checked:
22 22
23 23 - ends with a directory separator
24 24 - under top-level .hg
25 25 - starts at the root of a windows drive
26 26 - contains ".."
27 27
28 28 More check are also done about the file system states:
29 29 - traverses a symlink (e.g. a/symlink_here/b)
30 30 - inside a nested repository (a callback can be used to approve
31 31 some nested repositories, e.g., subrepositories)
32 32
33 33 The file system checks are only done when 'realfs' is set to True (the
34 34 default). They should be disable then we are auditing path for operation on
35 35 stored history.
36 36 '''
37 37
38 38 def __init__(self, root, callback=None, realfs=True):
39 39 self.audited = set()
40 40 self.auditeddir = set()
41 41 self.root = root
42 42 self._realfs = realfs
43 43 self.callback = callback
44 44 if os.path.lexists(root) and not util.fscasesensitive(root):
45 45 self.normcase = util.normcase
46 46 else:
47 47 self.normcase = lambda x: x
48 48
49 def __call__(self, path):
49 def __call__(self, path, mode=None):
50 50 '''Check the relative path.
51 51 path may contain a pattern (e.g. foodir/**.txt)'''
52 52
53 53 path = util.localpath(path)
54 54 normpath = self.normcase(path)
55 55 if normpath in self.audited:
56 56 return
57 57 # AIX ignores "/" at end of path, others raise EISDIR.
58 58 if util.endswithsep(path):
59 59 raise error.Abort(_("path ends in directory separator: %s") % path)
60 60 parts = util.splitpath(path)
61 61 if (os.path.splitdrive(path)[0]
62 62 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
63 63 or os.pardir in parts):
64 64 raise error.Abort(_("path contains illegal component: %s") % path)
65 65 # Windows shortname aliases
66 66 for p in parts:
67 67 if "~" in p:
68 68 first, last = p.split("~", 1)
69 69 if last.isdigit() and first.upper() in ["HG", "HG8B6C"]:
70 70 raise error.Abort(_("path contains illegal component: %s")
71 71 % path)
72 72 if '.hg' in _lowerclean(path):
73 73 lparts = [_lowerclean(p.lower()) for p in parts]
74 74 for p in '.hg', '.hg.':
75 75 if p in lparts[1:]:
76 76 pos = lparts.index(p)
77 77 base = os.path.join(*parts[:pos])
78 78 raise error.Abort(_("path '%s' is inside nested repo %r")
79 79 % (path, base))
80 80
81 81 normparts = util.splitpath(normpath)
82 82 assert len(parts) == len(normparts)
83 83
84 84 parts.pop()
85 85 normparts.pop()
86 86 prefixes = []
87 87 # It's important that we check the path parts starting from the root.
88 88 # This means we won't accidentally traverse a symlink into some other
89 89 # filesystem (which is potentially expensive to access).
90 90 for i in range(len(parts)):
91 91 prefix = pycompat.ossep.join(parts[:i + 1])
92 92 normprefix = pycompat.ossep.join(normparts[:i + 1])
93 93 if normprefix in self.auditeddir:
94 94 continue
95 95 if self._realfs:
96 96 self._checkfs(prefix, path)
97 97 prefixes.append(normprefix)
98 98
99 99 self.audited.add(normpath)
100 100 # only add prefixes to the cache after checking everything: we don't
101 101 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
102 102 self.auditeddir.update(prefixes)
103 103
104 104 def _checkfs(self, prefix, path):
105 105 """raise exception if a file system backed check fails"""
106 106 curpath = os.path.join(self.root, prefix)
107 107 try:
108 108 st = os.lstat(curpath)
109 109 except OSError as err:
110 110 # EINVAL can be raised as invalid path syntax under win32.
111 111 # They must be ignored for patterns can be checked too.
112 112 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
113 113 raise
114 114 else:
115 115 if stat.S_ISLNK(st.st_mode):
116 116 msg = _('path %r traverses symbolic link %r') % (path, prefix)
117 117 raise error.Abort(msg)
118 118 elif (stat.S_ISDIR(st.st_mode) and
119 119 os.path.isdir(os.path.join(curpath, '.hg'))):
120 120 if not self.callback or not self.callback(curpath):
121 121 msg = _("path '%s' is inside nested repo %r")
122 122 raise error.Abort(msg % (path, prefix))
123 123
124 124 def check(self, path):
125 125 try:
126 126 self(path)
127 127 return True
128 128 except (OSError, error.Abort):
129 129 return False
130 130
131 131 def canonpath(root, cwd, myname, auditor=None):
132 132 '''return the canonical path of myname, given cwd and root'''
133 133 if util.endswithsep(root):
134 134 rootsep = root
135 135 else:
136 136 rootsep = root + pycompat.ossep
137 137 name = myname
138 138 if not os.path.isabs(name):
139 139 name = os.path.join(root, cwd, name)
140 140 name = os.path.normpath(name)
141 141 if auditor is None:
142 142 auditor = pathauditor(root)
143 143 if name != rootsep and name.startswith(rootsep):
144 144 name = name[len(rootsep):]
145 145 auditor(name)
146 146 return util.pconvert(name)
147 147 elif name == root:
148 148 return ''
149 149 else:
150 150 # Determine whether `name' is in the hierarchy at or beneath `root',
151 151 # by iterating name=dirname(name) until that causes no change (can't
152 152 # check name == '/', because that doesn't work on windows). The list
153 153 # `rel' holds the reversed list of components making up the relative
154 154 # file name we want.
155 155 rel = []
156 156 while True:
157 157 try:
158 158 s = util.samefile(name, root)
159 159 except OSError:
160 160 s = False
161 161 if s:
162 162 if not rel:
163 163 # name was actually the same as root (maybe a symlink)
164 164 return ''
165 165 rel.reverse()
166 166 name = os.path.join(*rel)
167 167 auditor(name)
168 168 return util.pconvert(name)
169 169 dirname, basename = util.split(name)
170 170 rel.append(basename)
171 171 if dirname == name:
172 172 break
173 173 name = dirname
174 174
175 175 # A common mistake is to use -R, but specify a file relative to the repo
176 176 # instead of cwd. Detect that case, and provide a hint to the user.
177 177 hint = None
178 178 try:
179 179 if cwd != root:
180 180 canonpath(root, root, myname, auditor)
181 181 hint = (_("consider using '--cwd %s'")
182 182 % os.path.relpath(root, cwd))
183 183 except error.Abort:
184 184 pass
185 185
186 186 raise error.Abort(_("%s not under root '%s'") % (myname, root),
187 187 hint=hint)
188 188
189 189 def normasprefix(path):
190 190 '''normalize the specified path as path prefix
191 191
192 192 Returned value can be used safely for "p.startswith(prefix)",
193 193 "p[len(prefix):]", and so on.
194 194
195 195 For efficiency, this expects "path" argument to be already
196 196 normalized by "os.path.normpath", "os.path.realpath", and so on.
197 197
198 198 See also issue3033 for detail about need of this function.
199 199
200 200 >>> normasprefix('/foo/bar').replace(os.sep, '/')
201 201 '/foo/bar/'
202 202 >>> normasprefix('/').replace(os.sep, '/')
203 203 '/'
204 204 '''
205 205 d, p = os.path.splitdrive(path)
206 206 if len(p) != len(pycompat.ossep):
207 207 return path + pycompat.ossep
208 208 else:
209 209 return path
210 210
211 211 # forward two methods from posixpath that do what we need, but we'd
212 212 # rather not let our internals know that we're thinking in posix terms
213 213 # - instead we'll let them be oblivious.
214 214 join = posixpath.join
215 215 dirname = posixpath.dirname
@@ -1,642 +1,642 b''
1 1 # vfs.py - Mercurial 'vfs' classes
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import errno
11 11 import os
12 12 import shutil
13 13 import stat
14 14 import tempfile
15 15 import threading
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 error,
20 20 pathutil,
21 21 pycompat,
22 22 util,
23 23 )
24 24
25 25 def _avoidambig(path, oldstat):
26 26 """Avoid file stat ambiguity forcibly
27 27
28 28 This function causes copying ``path`` file, if it is owned by
29 29 another (see issue5418 and issue5584 for detail).
30 30 """
31 31 def checkandavoid():
32 32 newstat = util.filestat.frompath(path)
33 33 # return whether file stat ambiguity is (already) avoided
34 34 return (not newstat.isambig(oldstat) or
35 35 newstat.avoidambig(path, oldstat))
36 36 if not checkandavoid():
37 37 # simply copy to change owner of path to get privilege to
38 38 # advance mtime (see issue5418)
39 39 util.rename(util.mktempcopy(path), path)
40 40 checkandavoid()
41 41
42 42 class abstractvfs(object):
43 43 """Abstract base class; cannot be instantiated"""
44 44
45 45 def __init__(self, *args, **kwargs):
46 46 '''Prevent instantiation; don't call this from subclasses.'''
47 47 raise NotImplementedError('attempted instantiating ' + str(type(self)))
48 48
49 49 def tryread(self, path):
50 50 '''gracefully return an empty string for missing files'''
51 51 try:
52 52 return self.read(path)
53 53 except IOError as inst:
54 54 if inst.errno != errno.ENOENT:
55 55 raise
56 56 return ""
57 57
58 58 def tryreadlines(self, path, mode='rb'):
59 59 '''gracefully return an empty array for missing files'''
60 60 try:
61 61 return self.readlines(path, mode=mode)
62 62 except IOError as inst:
63 63 if inst.errno != errno.ENOENT:
64 64 raise
65 65 return []
66 66
67 67 @util.propertycache
68 68 def open(self):
69 69 '''Open ``path`` file, which is relative to vfs root.
70 70
71 71 Newly created directories are marked as "not to be indexed by
72 72 the content indexing service", if ``notindexed`` is specified
73 73 for "write" mode access.
74 74 '''
75 75 return self.__call__
76 76
77 77 def read(self, path):
78 78 with self(path, 'rb') as fp:
79 79 return fp.read()
80 80
81 81 def readlines(self, path, mode='rb'):
82 82 with self(path, mode=mode) as fp:
83 83 return fp.readlines()
84 84
85 85 def write(self, path, data, backgroundclose=False):
86 86 with self(path, 'wb', backgroundclose=backgroundclose) as fp:
87 87 return fp.write(data)
88 88
89 89 def writelines(self, path, data, mode='wb', notindexed=False):
90 90 with self(path, mode=mode, notindexed=notindexed) as fp:
91 91 return fp.writelines(data)
92 92
93 93 def append(self, path, data):
94 94 with self(path, 'ab') as fp:
95 95 return fp.write(data)
96 96
97 97 def basename(self, path):
98 98 """return base element of a path (as os.path.basename would do)
99 99
100 100 This exists to allow handling of strange encoding if needed."""
101 101 return os.path.basename(path)
102 102
103 103 def chmod(self, path, mode):
104 104 return os.chmod(self.join(path), mode)
105 105
106 106 def dirname(self, path):
107 107 """return dirname element of a path (as os.path.dirname would do)
108 108
109 109 This exists to allow handling of strange encoding if needed."""
110 110 return os.path.dirname(path)
111 111
112 112 def exists(self, path=None):
113 113 return os.path.exists(self.join(path))
114 114
115 115 def fstat(self, fp):
116 116 return util.fstat(fp)
117 117
118 118 def isdir(self, path=None):
119 119 return os.path.isdir(self.join(path))
120 120
121 121 def isfile(self, path=None):
122 122 return os.path.isfile(self.join(path))
123 123
124 124 def islink(self, path=None):
125 125 return os.path.islink(self.join(path))
126 126
127 127 def isfileorlink(self, path=None):
128 128 '''return whether path is a regular file or a symlink
129 129
130 130 Unlike isfile, this doesn't follow symlinks.'''
131 131 try:
132 132 st = self.lstat(path)
133 133 except OSError:
134 134 return False
135 135 mode = st.st_mode
136 136 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
137 137
138 138 def reljoin(self, *paths):
139 139 """join various elements of a path together (as os.path.join would do)
140 140
141 141 The vfs base is not injected so that path stay relative. This exists
142 142 to allow handling of strange encoding if needed."""
143 143 return os.path.join(*paths)
144 144
145 145 def split(self, path):
146 146 """split top-most element of a path (as os.path.split would do)
147 147
148 148 This exists to allow handling of strange encoding if needed."""
149 149 return os.path.split(path)
150 150
151 151 def lexists(self, path=None):
152 152 return os.path.lexists(self.join(path))
153 153
154 154 def lstat(self, path=None):
155 155 return os.lstat(self.join(path))
156 156
157 157 def listdir(self, path=None):
158 158 return os.listdir(self.join(path))
159 159
160 160 def makedir(self, path=None, notindexed=True):
161 161 return util.makedir(self.join(path), notindexed)
162 162
163 163 def makedirs(self, path=None, mode=None):
164 164 return util.makedirs(self.join(path), mode)
165 165
166 166 def makelock(self, info, path):
167 167 return util.makelock(info, self.join(path))
168 168
169 169 def mkdir(self, path=None):
170 170 return os.mkdir(self.join(path))
171 171
172 172 def mkstemp(self, suffix='', prefix='tmp', dir=None, text=False):
173 173 fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix,
174 174 dir=self.join(dir), text=text)
175 175 dname, fname = util.split(name)
176 176 if dir:
177 177 return fd, os.path.join(dir, fname)
178 178 else:
179 179 return fd, fname
180 180
181 181 def readdir(self, path=None, stat=None, skip=None):
182 182 return util.listdir(self.join(path), stat, skip)
183 183
184 184 def readlock(self, path):
185 185 return util.readlock(self.join(path))
186 186
187 187 def rename(self, src, dst, checkambig=False):
188 188 """Rename from src to dst
189 189
190 190 checkambig argument is used with util.filestat, and is useful
191 191 only if destination file is guarded by any lock
192 192 (e.g. repo.lock or repo.wlock).
193 193
194 194 To avoid file stat ambiguity forcibly, checkambig=True involves
195 195 copying ``src`` file, if it is owned by another. Therefore, use
196 196 checkambig=True only in limited cases (see also issue5418 and
197 197 issue5584 for detail).
198 198 """
199 199 srcpath = self.join(src)
200 200 dstpath = self.join(dst)
201 201 oldstat = checkambig and util.filestat.frompath(dstpath)
202 202 if oldstat and oldstat.stat:
203 203 ret = util.rename(srcpath, dstpath)
204 204 _avoidambig(dstpath, oldstat)
205 205 return ret
206 206 return util.rename(srcpath, dstpath)
207 207
208 208 def readlink(self, path):
209 209 return os.readlink(self.join(path))
210 210
211 211 def removedirs(self, path=None):
212 212 """Remove a leaf directory and all empty intermediate ones
213 213 """
214 214 return util.removedirs(self.join(path))
215 215
216 216 def rmtree(self, path=None, ignore_errors=False, forcibly=False):
217 217 """Remove a directory tree recursively
218 218
219 219 If ``forcibly``, this tries to remove READ-ONLY files, too.
220 220 """
221 221 if forcibly:
222 222 def onerror(function, path, excinfo):
223 223 if function is not os.remove:
224 224 raise
225 225 # read-only files cannot be unlinked under Windows
226 226 s = os.stat(path)
227 227 if (s.st_mode & stat.S_IWRITE) != 0:
228 228 raise
229 229 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
230 230 os.remove(path)
231 231 else:
232 232 onerror = None
233 233 return shutil.rmtree(self.join(path),
234 234 ignore_errors=ignore_errors, onerror=onerror)
235 235
236 236 def setflags(self, path, l, x):
237 237 return util.setflags(self.join(path), l, x)
238 238
239 239 def stat(self, path=None):
240 240 return os.stat(self.join(path))
241 241
242 242 def unlink(self, path=None):
243 243 return util.unlink(self.join(path))
244 244
245 245 def tryunlink(self, path=None):
246 246 """Attempt to remove a file, ignoring missing file errors."""
247 247 util.tryunlink(self.join(path))
248 248
249 249 def unlinkpath(self, path=None, ignoremissing=False):
250 250 return util.unlinkpath(self.join(path), ignoremissing=ignoremissing)
251 251
252 252 def utime(self, path=None, t=None):
253 253 return os.utime(self.join(path), t)
254 254
255 255 def walk(self, path=None, onerror=None):
256 256 """Yield (dirpath, dirs, files) tuple for each directories under path
257 257
258 258 ``dirpath`` is relative one from the root of this vfs. This
259 259 uses ``os.sep`` as path separator, even you specify POSIX
260 260 style ``path``.
261 261
262 262 "The root of this vfs" is represented as empty ``dirpath``.
263 263 """
264 264 root = os.path.normpath(self.join(None))
265 265 # when dirpath == root, dirpath[prefixlen:] becomes empty
266 266 # because len(dirpath) < prefixlen.
267 267 prefixlen = len(pathutil.normasprefix(root))
268 268 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
269 269 yield (dirpath[prefixlen:], dirs, files)
270 270
271 271 @contextlib.contextmanager
272 272 def backgroundclosing(self, ui, expectedcount=-1):
273 273 """Allow files to be closed asynchronously.
274 274
275 275 When this context manager is active, ``backgroundclose`` can be passed
276 276 to ``__call__``/``open`` to result in the file possibly being closed
277 277 asynchronously, on a background thread.
278 278 """
279 279 # This is an arbitrary restriction and could be changed if we ever
280 280 # have a use case.
281 281 vfs = getattr(self, 'vfs', self)
282 282 if getattr(vfs, '_backgroundfilecloser', None):
283 283 raise error.Abort(
284 284 _('can only have 1 active background file closer'))
285 285
286 286 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
287 287 try:
288 288 vfs._backgroundfilecloser = bfc
289 289 yield bfc
290 290 finally:
291 291 vfs._backgroundfilecloser = None
292 292
293 293 class vfs(abstractvfs):
294 294 '''Operate files relative to a base directory
295 295
296 296 This class is used to hide the details of COW semantics and
297 297 remote file access from higher level code.
298 298 '''
299 299 def __init__(self, base, audit=True, expandpath=False, realpath=False):
300 300 if expandpath:
301 301 base = util.expandpath(base)
302 302 if realpath:
303 303 base = os.path.realpath(base)
304 304 self.base = base
305 305 self._audit = audit
306 306 if audit:
307 307 self.audit = pathutil.pathauditor(self.base)
308 308 else:
309 self.audit = util.always
309 self.audit = (lambda path, mode=None: True)
310 310 self.createmode = None
311 311 self._trustnlink = None
312 312
313 313 @util.propertycache
314 314 def _cansymlink(self):
315 315 return util.checklink(self.base)
316 316
317 317 @util.propertycache
318 318 def _chmod(self):
319 319 return util.checkexec(self.base)
320 320
321 321 def _fixfilemode(self, name):
322 322 if self.createmode is None or not self._chmod:
323 323 return
324 324 os.chmod(name, self.createmode & 0o666)
325 325
326 326 def __call__(self, path, mode="r", text=False, atomictemp=False,
327 327 notindexed=False, backgroundclose=False, checkambig=False,
328 328 auditpath=True):
329 329 '''Open ``path`` file, which is relative to vfs root.
330 330
331 331 Newly created directories are marked as "not to be indexed by
332 332 the content indexing service", if ``notindexed`` is specified
333 333 for "write" mode access.
334 334
335 335 If ``backgroundclose`` is passed, the file may be closed asynchronously.
336 336 It can only be used if the ``self.backgroundclosing()`` context manager
337 337 is active. This should only be specified if the following criteria hold:
338 338
339 339 1. There is a potential for writing thousands of files. Unless you
340 340 are writing thousands of files, the performance benefits of
341 341 asynchronously closing files is not realized.
342 342 2. Files are opened exactly once for the ``backgroundclosing``
343 343 active duration and are therefore free of race conditions between
344 344 closing a file on a background thread and reopening it. (If the
345 345 file were opened multiple times, there could be unflushed data
346 346 because the original file handle hasn't been flushed/closed yet.)
347 347
348 348 ``checkambig`` argument is passed to atomictemplfile (valid
349 349 only for writing), and is useful only if target file is
350 350 guarded by any lock (e.g. repo.lock or repo.wlock).
351 351
352 352 To avoid file stat ambiguity forcibly, checkambig=True involves
353 353 copying ``path`` file opened in "append" mode (e.g. for
354 354 truncation), if it is owned by another. Therefore, use
355 355 combination of append mode and checkambig=True only in limited
356 356 cases (see also issue5418 and issue5584 for detail).
357 357 '''
358 358 if auditpath:
359 359 if self._audit:
360 360 r = util.checkosfilename(path)
361 361 if r:
362 362 raise error.Abort("%s: %r" % (r, path))
363 self.audit(path)
363 self.audit(path, mode=mode)
364 364 f = self.join(path)
365 365
366 366 if not text and "b" not in mode:
367 367 mode += "b" # for that other OS
368 368
369 369 nlink = -1
370 370 if mode not in ('r', 'rb'):
371 371 dirname, basename = util.split(f)
372 372 # If basename is empty, then the path is malformed because it points
373 373 # to a directory. Let the posixfile() call below raise IOError.
374 374 if basename:
375 375 if atomictemp:
376 376 util.makedirs(dirname, self.createmode, notindexed)
377 377 return util.atomictempfile(f, mode, self.createmode,
378 378 checkambig=checkambig)
379 379 try:
380 380 if 'w' in mode:
381 381 util.unlink(f)
382 382 nlink = 0
383 383 else:
384 384 # nlinks() may behave differently for files on Windows
385 385 # shares if the file is open.
386 386 with util.posixfile(f):
387 387 nlink = util.nlinks(f)
388 388 if nlink < 1:
389 389 nlink = 2 # force mktempcopy (issue1922)
390 390 except (OSError, IOError) as e:
391 391 if e.errno != errno.ENOENT:
392 392 raise
393 393 nlink = 0
394 394 util.makedirs(dirname, self.createmode, notindexed)
395 395 if nlink > 0:
396 396 if self._trustnlink is None:
397 397 self._trustnlink = nlink > 1 or util.checknlink(f)
398 398 if nlink > 1 or not self._trustnlink:
399 399 util.rename(util.mktempcopy(f), f)
400 400 fp = util.posixfile(f, mode)
401 401 if nlink == 0:
402 402 self._fixfilemode(f)
403 403
404 404 if checkambig:
405 405 if mode in ('r', 'rb'):
406 406 raise error.Abort(_('implementation error: mode %s is not'
407 407 ' valid for checkambig=True') % mode)
408 408 fp = checkambigatclosing(fp)
409 409
410 410 if backgroundclose:
411 411 if not self._backgroundfilecloser:
412 412 raise error.Abort(_('backgroundclose can only be used when a '
413 413 'backgroundclosing context manager is active')
414 414 )
415 415
416 416 fp = delayclosedfile(fp, self._backgroundfilecloser)
417 417
418 418 return fp
419 419
420 420 def symlink(self, src, dst):
421 421 self.audit(dst)
422 422 linkname = self.join(dst)
423 423 util.tryunlink(linkname)
424 424
425 425 util.makedirs(os.path.dirname(linkname), self.createmode)
426 426
427 427 if self._cansymlink:
428 428 try:
429 429 os.symlink(src, linkname)
430 430 except OSError as err:
431 431 raise OSError(err.errno, _('could not symlink to %r: %s') %
432 432 (src, err.strerror), linkname)
433 433 else:
434 434 self.write(dst, src)
435 435
436 436 def join(self, path, *insidef):
437 437 if path:
438 438 return os.path.join(self.base, path, *insidef)
439 439 else:
440 440 return self.base
441 441
442 442 opener = vfs
443 443
444 444 class proxyvfs(object):
445 445 def __init__(self, vfs):
446 446 self.vfs = vfs
447 447
448 448 @property
449 449 def options(self):
450 450 return self.vfs.options
451 451
452 452 @options.setter
453 453 def options(self, value):
454 454 self.vfs.options = value
455 455
456 456 class filtervfs(abstractvfs, proxyvfs):
457 457 '''Wrapper vfs for filtering filenames with a function.'''
458 458
459 459 def __init__(self, vfs, filter):
460 460 proxyvfs.__init__(self, vfs)
461 461 self._filter = filter
462 462
463 463 def __call__(self, path, *args, **kwargs):
464 464 return self.vfs(self._filter(path), *args, **kwargs)
465 465
466 466 def join(self, path, *insidef):
467 467 if path:
468 468 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
469 469 else:
470 470 return self.vfs.join(path)
471 471
472 472 filteropener = filtervfs
473 473
474 474 class readonlyvfs(abstractvfs, proxyvfs):
475 475 '''Wrapper vfs preventing any writing.'''
476 476
477 477 def __init__(self, vfs):
478 478 proxyvfs.__init__(self, vfs)
479 479
480 480 def __call__(self, path, mode='r', *args, **kw):
481 481 if mode not in ('r', 'rb'):
482 482 raise error.Abort(_('this vfs is read only'))
483 483 return self.vfs(path, mode, *args, **kw)
484 484
485 485 def join(self, path, *insidef):
486 486 return self.vfs.join(path, *insidef)
487 487
488 488 class closewrapbase(object):
489 489 """Base class of wrapper, which hooks closing
490 490
491 491 Do not instantiate outside of the vfs layer.
492 492 """
493 493 def __init__(self, fh):
494 494 object.__setattr__(self, r'_origfh', fh)
495 495
496 496 def __getattr__(self, attr):
497 497 return getattr(self._origfh, attr)
498 498
499 499 def __setattr__(self, attr, value):
500 500 return setattr(self._origfh, attr, value)
501 501
502 502 def __delattr__(self, attr):
503 503 return delattr(self._origfh, attr)
504 504
505 505 def __enter__(self):
506 506 return self._origfh.__enter__()
507 507
508 508 def __exit__(self, exc_type, exc_value, exc_tb):
509 509 raise NotImplementedError('attempted instantiating ' + str(type(self)))
510 510
511 511 def close(self):
512 512 raise NotImplementedError('attempted instantiating ' + str(type(self)))
513 513
514 514 class delayclosedfile(closewrapbase):
515 515 """Proxy for a file object whose close is delayed.
516 516
517 517 Do not instantiate outside of the vfs layer.
518 518 """
519 519 def __init__(self, fh, closer):
520 520 super(delayclosedfile, self).__init__(fh)
521 521 object.__setattr__(self, r'_closer', closer)
522 522
523 523 def __exit__(self, exc_type, exc_value, exc_tb):
524 524 self._closer.close(self._origfh)
525 525
526 526 def close(self):
527 527 self._closer.close(self._origfh)
528 528
529 529 class backgroundfilecloser(object):
530 530 """Coordinates background closing of file handles on multiple threads."""
531 531 def __init__(self, ui, expectedcount=-1):
532 532 self._running = False
533 533 self._entered = False
534 534 self._threads = []
535 535 self._threadexception = None
536 536
537 537 # Only Windows/NTFS has slow file closing. So only enable by default
538 538 # on that platform. But allow to be enabled elsewhere for testing.
539 539 defaultenabled = pycompat.osname == 'nt'
540 540 enabled = ui.configbool('worker', 'backgroundclose', defaultenabled)
541 541
542 542 if not enabled:
543 543 return
544 544
545 545 # There is overhead to starting and stopping the background threads.
546 546 # Don't do background processing unless the file count is large enough
547 547 # to justify it.
548 548 minfilecount = ui.configint('worker', 'backgroundcloseminfilecount')
549 549 # FUTURE dynamically start background threads after minfilecount closes.
550 550 # (We don't currently have any callers that don't know their file count)
551 551 if expectedcount > 0 and expectedcount < minfilecount:
552 552 return
553 553
554 554 maxqueue = ui.configint('worker', 'backgroundclosemaxqueue')
555 555 threadcount = ui.configint('worker', 'backgroundclosethreadcount')
556 556
557 557 ui.debug('starting %d threads for background file closing\n' %
558 558 threadcount)
559 559
560 560 self._queue = util.queue(maxsize=maxqueue)
561 561 self._running = True
562 562
563 563 for i in range(threadcount):
564 564 t = threading.Thread(target=self._worker, name='backgroundcloser')
565 565 self._threads.append(t)
566 566 t.start()
567 567
568 568 def __enter__(self):
569 569 self._entered = True
570 570 return self
571 571
572 572 def __exit__(self, exc_type, exc_value, exc_tb):
573 573 self._running = False
574 574
575 575 # Wait for threads to finish closing so open files don't linger for
576 576 # longer than lifetime of context manager.
577 577 for t in self._threads:
578 578 t.join()
579 579
580 580 def _worker(self):
581 581 """Main routine for worker thread."""
582 582 while True:
583 583 try:
584 584 fh = self._queue.get(block=True, timeout=0.100)
585 585 # Need to catch or the thread will terminate and
586 586 # we could orphan file descriptors.
587 587 try:
588 588 fh.close()
589 589 except Exception as e:
590 590 # Stash so can re-raise from main thread later.
591 591 self._threadexception = e
592 592 except util.empty:
593 593 if not self._running:
594 594 break
595 595
596 596 def close(self, fh):
597 597 """Schedule a file for closing."""
598 598 if not self._entered:
599 599 raise error.Abort(_('can only call close() when context manager '
600 600 'active'))
601 601
602 602 # If a background thread encountered an exception, raise now so we fail
603 603 # fast. Otherwise we may potentially go on for minutes until the error
604 604 # is acted on.
605 605 if self._threadexception:
606 606 e = self._threadexception
607 607 self._threadexception = None
608 608 raise e
609 609
610 610 # If we're not actively running, close synchronously.
611 611 if not self._running:
612 612 fh.close()
613 613 return
614 614
615 615 self._queue.put(fh, block=True, timeout=None)
616 616
617 617 class checkambigatclosing(closewrapbase):
618 618 """Proxy for a file object, to avoid ambiguity of file stat
619 619
620 620 See also util.filestat for detail about "ambiguity of file stat".
621 621
622 622 This proxy is useful only if the target file is guarded by any
623 623 lock (e.g. repo.lock or repo.wlock)
624 624
625 625 Do not instantiate outside of the vfs layer.
626 626 """
627 627 def __init__(self, fh):
628 628 super(checkambigatclosing, self).__init__(fh)
629 629 object.__setattr__(self, r'_oldstat', util.filestat.frompath(fh.name))
630 630
631 631 def _checkambig(self):
632 632 oldstat = self._oldstat
633 633 if oldstat.stat:
634 634 _avoidambig(self._origfh.name, oldstat)
635 635
636 636 def __exit__(self, exc_type, exc_value, exc_tb):
637 637 self._origfh.__exit__(exc_type, exc_value, exc_tb)
638 638 self._checkambig()
639 639
640 640 def close(self):
641 641 self._origfh.close()
642 642 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now