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