##// END OF EJS Templates
vfsproxy: inherit the `createmode` attribute too...
marmoute -
r51347:d1d458fb stable
parent child Browse files
Show More
@@ -1,813 +1,817 b''
1 # vfs.py - Mercurial 'vfs' classes
1 # vfs.py - Mercurial 'vfs' classes
2 #
2 #
3 # Copyright Olivia Mackall <olivia@selenic.com>
3 # Copyright Olivia Mackall <olivia@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
7
8 import contextlib
8 import contextlib
9 import os
9 import os
10 import shutil
10 import shutil
11 import stat
11 import stat
12 import threading
12 import threading
13
13
14 from typing import (
14 from typing import (
15 Optional,
15 Optional,
16 )
16 )
17
17
18 from .i18n import _
18 from .i18n import _
19 from .pycompat import (
19 from .pycompat import (
20 delattr,
20 delattr,
21 getattr,
21 getattr,
22 setattr,
22 setattr,
23 )
23 )
24 from . import (
24 from . import (
25 encoding,
25 encoding,
26 error,
26 error,
27 pathutil,
27 pathutil,
28 pycompat,
28 pycompat,
29 util,
29 util,
30 )
30 )
31
31
32
32
33 def _avoidambig(path: bytes, oldstat):
33 def _avoidambig(path: bytes, oldstat):
34 """Avoid file stat ambiguity forcibly
34 """Avoid file stat ambiguity forcibly
35
35
36 This function causes copying ``path`` file, if it is owned by
36 This function causes copying ``path`` file, if it is owned by
37 another (see issue5418 and issue5584 for detail).
37 another (see issue5418 and issue5584 for detail).
38 """
38 """
39
39
40 def checkandavoid():
40 def checkandavoid():
41 newstat = util.filestat.frompath(path)
41 newstat = util.filestat.frompath(path)
42 # return whether file stat ambiguity is (already) avoided
42 # return whether file stat ambiguity is (already) avoided
43 return not newstat.isambig(oldstat) or newstat.avoidambig(path, oldstat)
43 return not newstat.isambig(oldstat) or newstat.avoidambig(path, oldstat)
44
44
45 if not checkandavoid():
45 if not checkandavoid():
46 # simply copy to change owner of path to get privilege to
46 # simply copy to change owner of path to get privilege to
47 # advance mtime (see issue5418)
47 # advance mtime (see issue5418)
48 util.rename(util.mktempcopy(path), path)
48 util.rename(util.mktempcopy(path), path)
49 checkandavoid()
49 checkandavoid()
50
50
51
51
52 class abstractvfs:
52 class abstractvfs:
53 """Abstract base class; cannot be instantiated"""
53 """Abstract base class; cannot be instantiated"""
54
54
55 # default directory separator for vfs
55 # default directory separator for vfs
56 #
56 #
57 # Other vfs code always use `/` and this works fine because python file API
57 # Other vfs code always use `/` and this works fine because python file API
58 # abstract the use of `/` and make it work transparently. For consistency
58 # abstract the use of `/` and make it work transparently. For consistency
59 # vfs will always use `/` when joining. This avoid some confusion in
59 # vfs will always use `/` when joining. This avoid some confusion in
60 # encoded vfs (see issue6546)
60 # encoded vfs (see issue6546)
61 _dir_sep = b'/'
61 _dir_sep = b'/'
62
62
63 def __init__(self, *args, **kwargs):
63 def __init__(self, *args, **kwargs):
64 '''Prevent instantiation; don't call this from subclasses.'''
64 '''Prevent instantiation; don't call this from subclasses.'''
65 raise NotImplementedError('attempted instantiating ' + str(type(self)))
65 raise NotImplementedError('attempted instantiating ' + str(type(self)))
66
66
67 # TODO: type return, which is util.posixfile wrapped by a proxy
67 # TODO: type return, which is util.posixfile wrapped by a proxy
68 def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs):
68 def __call__(self, path: bytes, mode: bytes = b'rb', **kwargs):
69 raise NotImplementedError
69 raise NotImplementedError
70
70
71 def _auditpath(self, path: bytes, mode: bytes):
71 def _auditpath(self, path: bytes, mode: bytes):
72 raise NotImplementedError
72 raise NotImplementedError
73
73
74 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
74 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
75 raise NotImplementedError
75 raise NotImplementedError
76
76
77 def tryread(self, path: bytes) -> bytes:
77 def tryread(self, path: bytes) -> bytes:
78 '''gracefully return an empty string for missing files'''
78 '''gracefully return an empty string for missing files'''
79 try:
79 try:
80 return self.read(path)
80 return self.read(path)
81 except FileNotFoundError:
81 except FileNotFoundError:
82 pass
82 pass
83 return b""
83 return b""
84
84
85 def tryreadlines(self, path: bytes, mode: bytes = b'rb'):
85 def tryreadlines(self, path: bytes, mode: bytes = b'rb'):
86 '''gracefully return an empty array for missing files'''
86 '''gracefully return an empty array for missing files'''
87 try:
87 try:
88 return self.readlines(path, mode=mode)
88 return self.readlines(path, mode=mode)
89 except FileNotFoundError:
89 except FileNotFoundError:
90 pass
90 pass
91 return []
91 return []
92
92
93 @util.propertycache
93 @util.propertycache
94 def open(self):
94 def open(self):
95 """Open ``path`` file, which is relative to vfs root.
95 """Open ``path`` file, which is relative to vfs root.
96
96
97 Newly created directories are marked as "not to be indexed by
97 Newly created directories are marked as "not to be indexed by
98 the content indexing service", if ``notindexed`` is specified
98 the content indexing service", if ``notindexed`` is specified
99 for "write" mode access.
99 for "write" mode access.
100 """
100 """
101 return self.__call__
101 return self.__call__
102
102
103 def read(self, path: bytes) -> bytes:
103 def read(self, path: bytes) -> bytes:
104 with self(path, b'rb') as fp:
104 with self(path, b'rb') as fp:
105 return fp.read()
105 return fp.read()
106
106
107 def readlines(self, path: bytes, mode: bytes = b'rb'):
107 def readlines(self, path: bytes, mode: bytes = b'rb'):
108 with self(path, mode=mode) as fp:
108 with self(path, mode=mode) as fp:
109 return fp.readlines()
109 return fp.readlines()
110
110
111 def write(
111 def write(
112 self, path: bytes, data: bytes, backgroundclose=False, **kwargs
112 self, path: bytes, data: bytes, backgroundclose=False, **kwargs
113 ) -> int:
113 ) -> int:
114 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
114 with self(path, b'wb', backgroundclose=backgroundclose, **kwargs) as fp:
115 return fp.write(data)
115 return fp.write(data)
116
116
117 def writelines(
117 def writelines(
118 self, path: bytes, data: bytes, mode: bytes = b'wb', notindexed=False
118 self, path: bytes, data: bytes, mode: bytes = b'wb', notindexed=False
119 ) -> None:
119 ) -> None:
120 with self(path, mode=mode, notindexed=notindexed) as fp:
120 with self(path, mode=mode, notindexed=notindexed) as fp:
121 return fp.writelines(data)
121 return fp.writelines(data)
122
122
123 def append(self, path: bytes, data: bytes) -> int:
123 def append(self, path: bytes, data: bytes) -> int:
124 with self(path, b'ab') as fp:
124 with self(path, b'ab') as fp:
125 return fp.write(data)
125 return fp.write(data)
126
126
127 def basename(self, path: bytes) -> bytes:
127 def basename(self, path: bytes) -> bytes:
128 """return base element of a path (as os.path.basename would do)
128 """return base element of a path (as os.path.basename would do)
129
129
130 This exists to allow handling of strange encoding if needed."""
130 This exists to allow handling of strange encoding if needed."""
131 return os.path.basename(path)
131 return os.path.basename(path)
132
132
133 def chmod(self, path: bytes, mode: int) -> None:
133 def chmod(self, path: bytes, mode: int) -> None:
134 return os.chmod(self.join(path), mode)
134 return os.chmod(self.join(path), mode)
135
135
136 def dirname(self, path: bytes) -> bytes:
136 def dirname(self, path: bytes) -> bytes:
137 """return dirname element of a path (as os.path.dirname would do)
137 """return dirname element of a path (as os.path.dirname would do)
138
138
139 This exists to allow handling of strange encoding if needed."""
139 This exists to allow handling of strange encoding if needed."""
140 return os.path.dirname(path)
140 return os.path.dirname(path)
141
141
142 def exists(self, path: Optional[bytes] = None) -> bool:
142 def exists(self, path: Optional[bytes] = None) -> bool:
143 return os.path.exists(self.join(path))
143 return os.path.exists(self.join(path))
144
144
145 def fstat(self, fp):
145 def fstat(self, fp):
146 return util.fstat(fp)
146 return util.fstat(fp)
147
147
148 def isdir(self, path: Optional[bytes] = None) -> bool:
148 def isdir(self, path: Optional[bytes] = None) -> bool:
149 return os.path.isdir(self.join(path))
149 return os.path.isdir(self.join(path))
150
150
151 def isfile(self, path: Optional[bytes] = None) -> bool:
151 def isfile(self, path: Optional[bytes] = None) -> bool:
152 return os.path.isfile(self.join(path))
152 return os.path.isfile(self.join(path))
153
153
154 def islink(self, path: Optional[bytes] = None) -> bool:
154 def islink(self, path: Optional[bytes] = None) -> bool:
155 return os.path.islink(self.join(path))
155 return os.path.islink(self.join(path))
156
156
157 def isfileorlink(self, path: Optional[bytes] = None) -> bool:
157 def isfileorlink(self, path: Optional[bytes] = None) -> bool:
158 """return whether path is a regular file or a symlink
158 """return whether path is a regular file or a symlink
159
159
160 Unlike isfile, this doesn't follow symlinks."""
160 Unlike isfile, this doesn't follow symlinks."""
161 try:
161 try:
162 st = self.lstat(path)
162 st = self.lstat(path)
163 except OSError:
163 except OSError:
164 return False
164 return False
165 mode = st.st_mode
165 mode = st.st_mode
166 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
166 return stat.S_ISREG(mode) or stat.S_ISLNK(mode)
167
167
168 def _join(self, *paths: bytes) -> bytes:
168 def _join(self, *paths: bytes) -> bytes:
169 root_idx = 0
169 root_idx = 0
170 for idx, p in enumerate(paths):
170 for idx, p in enumerate(paths):
171 if os.path.isabs(p) or p.startswith(self._dir_sep):
171 if os.path.isabs(p) or p.startswith(self._dir_sep):
172 root_idx = idx
172 root_idx = idx
173 if root_idx != 0:
173 if root_idx != 0:
174 paths = paths[root_idx:]
174 paths = paths[root_idx:]
175 paths = [p for p in paths if p]
175 paths = [p for p in paths if p]
176 return self._dir_sep.join(paths)
176 return self._dir_sep.join(paths)
177
177
178 def reljoin(self, *paths: bytes) -> bytes:
178 def reljoin(self, *paths: bytes) -> bytes:
179 """join various elements of a path together (as os.path.join would do)
179 """join various elements of a path together (as os.path.join would do)
180
180
181 The vfs base is not injected so that path stay relative. This exists
181 The vfs base is not injected so that path stay relative. This exists
182 to allow handling of strange encoding if needed."""
182 to allow handling of strange encoding if needed."""
183 return self._join(*paths)
183 return self._join(*paths)
184
184
185 def split(self, path: bytes):
185 def split(self, path: bytes):
186 """split top-most element of a path (as os.path.split would do)
186 """split top-most element of a path (as os.path.split would do)
187
187
188 This exists to allow handling of strange encoding if needed."""
188 This exists to allow handling of strange encoding if needed."""
189 return os.path.split(path)
189 return os.path.split(path)
190
190
191 def lexists(self, path: Optional[bytes] = None) -> bool:
191 def lexists(self, path: Optional[bytes] = None) -> bool:
192 return os.path.lexists(self.join(path))
192 return os.path.lexists(self.join(path))
193
193
194 def lstat(self, path: Optional[bytes] = None):
194 def lstat(self, path: Optional[bytes] = None):
195 return os.lstat(self.join(path))
195 return os.lstat(self.join(path))
196
196
197 def listdir(self, path: Optional[bytes] = None):
197 def listdir(self, path: Optional[bytes] = None):
198 return os.listdir(self.join(path))
198 return os.listdir(self.join(path))
199
199
200 def makedir(self, path: Optional[bytes] = None, notindexed=True):
200 def makedir(self, path: Optional[bytes] = None, notindexed=True):
201 return util.makedir(self.join(path), notindexed)
201 return util.makedir(self.join(path), notindexed)
202
202
203 def makedirs(
203 def makedirs(
204 self, path: Optional[bytes] = None, mode: Optional[int] = None
204 self, path: Optional[bytes] = None, mode: Optional[int] = None
205 ):
205 ):
206 return util.makedirs(self.join(path), mode)
206 return util.makedirs(self.join(path), mode)
207
207
208 def makelock(self, info, path: bytes):
208 def makelock(self, info, path: bytes):
209 return util.makelock(info, self.join(path))
209 return util.makelock(info, self.join(path))
210
210
211 def mkdir(self, path: Optional[bytes] = None):
211 def mkdir(self, path: Optional[bytes] = None):
212 return os.mkdir(self.join(path))
212 return os.mkdir(self.join(path))
213
213
214 def mkstemp(
214 def mkstemp(
215 self,
215 self,
216 suffix: bytes = b'',
216 suffix: bytes = b'',
217 prefix: bytes = b'tmp',
217 prefix: bytes = b'tmp',
218 dir: Optional[bytes] = None,
218 dir: Optional[bytes] = None,
219 ):
219 ):
220 fd, name = pycompat.mkstemp(
220 fd, name = pycompat.mkstemp(
221 suffix=suffix, prefix=prefix, dir=self.join(dir)
221 suffix=suffix, prefix=prefix, dir=self.join(dir)
222 )
222 )
223 dname, fname = util.split(name)
223 dname, fname = util.split(name)
224 if dir:
224 if dir:
225 return fd, os.path.join(dir, fname)
225 return fd, os.path.join(dir, fname)
226 else:
226 else:
227 return fd, fname
227 return fd, fname
228
228
229 def readdir(self, path: Optional[bytes] = None, stat=None, skip=None):
229 def readdir(self, path: Optional[bytes] = None, stat=None, skip=None):
230 return util.listdir(self.join(path), stat, skip)
230 return util.listdir(self.join(path), stat, skip)
231
231
232 def readlock(self, path: bytes) -> bytes:
232 def readlock(self, path: bytes) -> bytes:
233 return util.readlock(self.join(path))
233 return util.readlock(self.join(path))
234
234
235 def rename(self, src: bytes, dst: bytes, checkambig=False):
235 def rename(self, src: bytes, dst: bytes, checkambig=False):
236 """Rename from src to dst
236 """Rename from src to dst
237
237
238 checkambig argument is used with util.filestat, and is useful
238 checkambig argument is used with util.filestat, and is useful
239 only if destination file is guarded by any lock
239 only if destination file is guarded by any lock
240 (e.g. repo.lock or repo.wlock).
240 (e.g. repo.lock or repo.wlock).
241
241
242 To avoid file stat ambiguity forcibly, checkambig=True involves
242 To avoid file stat ambiguity forcibly, checkambig=True involves
243 copying ``src`` file, if it is owned by another. Therefore, use
243 copying ``src`` file, if it is owned by another. Therefore, use
244 checkambig=True only in limited cases (see also issue5418 and
244 checkambig=True only in limited cases (see also issue5418 and
245 issue5584 for detail).
245 issue5584 for detail).
246 """
246 """
247 self._auditpath(dst, b'w')
247 self._auditpath(dst, b'w')
248 srcpath = self.join(src)
248 srcpath = self.join(src)
249 dstpath = self.join(dst)
249 dstpath = self.join(dst)
250 oldstat = checkambig and util.filestat.frompath(dstpath)
250 oldstat = checkambig and util.filestat.frompath(dstpath)
251 if oldstat and oldstat.stat:
251 if oldstat and oldstat.stat:
252 ret = util.rename(srcpath, dstpath)
252 ret = util.rename(srcpath, dstpath)
253 _avoidambig(dstpath, oldstat)
253 _avoidambig(dstpath, oldstat)
254 return ret
254 return ret
255 return util.rename(srcpath, dstpath)
255 return util.rename(srcpath, dstpath)
256
256
257 def readlink(self, path: bytes) -> bytes:
257 def readlink(self, path: bytes) -> bytes:
258 return util.readlink(self.join(path))
258 return util.readlink(self.join(path))
259
259
260 def removedirs(self, path: Optional[bytes] = None):
260 def removedirs(self, path: Optional[bytes] = None):
261 """Remove a leaf directory and all empty intermediate ones"""
261 """Remove a leaf directory and all empty intermediate ones"""
262 return util.removedirs(self.join(path))
262 return util.removedirs(self.join(path))
263
263
264 def rmdir(self, path: Optional[bytes] = None):
264 def rmdir(self, path: Optional[bytes] = None):
265 """Remove an empty directory."""
265 """Remove an empty directory."""
266 return os.rmdir(self.join(path))
266 return os.rmdir(self.join(path))
267
267
268 def rmtree(
268 def rmtree(
269 self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False
269 self, path: Optional[bytes] = None, ignore_errors=False, forcibly=False
270 ):
270 ):
271 """Remove a directory tree recursively
271 """Remove a directory tree recursively
272
272
273 If ``forcibly``, this tries to remove READ-ONLY files, too.
273 If ``forcibly``, this tries to remove READ-ONLY files, too.
274 """
274 """
275 if forcibly:
275 if forcibly:
276
276
277 def onerror(function, path, excinfo):
277 def onerror(function, path, excinfo):
278 if function is not os.remove:
278 if function is not os.remove:
279 raise
279 raise
280 # read-only files cannot be unlinked under Windows
280 # read-only files cannot be unlinked under Windows
281 s = os.stat(path)
281 s = os.stat(path)
282 if (s.st_mode & stat.S_IWRITE) != 0:
282 if (s.st_mode & stat.S_IWRITE) != 0:
283 raise
283 raise
284 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
284 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
285 os.remove(path)
285 os.remove(path)
286
286
287 else:
287 else:
288 onerror = None
288 onerror = None
289 return shutil.rmtree(
289 return shutil.rmtree(
290 self.join(path), ignore_errors=ignore_errors, onerror=onerror
290 self.join(path), ignore_errors=ignore_errors, onerror=onerror
291 )
291 )
292
292
293 def setflags(self, path: bytes, l: bool, x: bool):
293 def setflags(self, path: bytes, l: bool, x: bool):
294 return util.setflags(self.join(path), l, x)
294 return util.setflags(self.join(path), l, x)
295
295
296 def stat(self, path: Optional[bytes] = None):
296 def stat(self, path: Optional[bytes] = None):
297 return os.stat(self.join(path))
297 return os.stat(self.join(path))
298
298
299 def unlink(self, path: Optional[bytes] = None):
299 def unlink(self, path: Optional[bytes] = None):
300 return util.unlink(self.join(path))
300 return util.unlink(self.join(path))
301
301
302 def tryunlink(self, path: Optional[bytes] = None):
302 def tryunlink(self, path: Optional[bytes] = None):
303 """Attempt to remove a file, ignoring missing file errors."""
303 """Attempt to remove a file, ignoring missing file errors."""
304 util.tryunlink(self.join(path))
304 util.tryunlink(self.join(path))
305
305
306 def unlinkpath(
306 def unlinkpath(
307 self, path: Optional[bytes] = None, ignoremissing=False, rmdir=True
307 self, path: Optional[bytes] = None, ignoremissing=False, rmdir=True
308 ):
308 ):
309 return util.unlinkpath(
309 return util.unlinkpath(
310 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
310 self.join(path), ignoremissing=ignoremissing, rmdir=rmdir
311 )
311 )
312
312
313 def utime(self, path: Optional[bytes] = None, t=None):
313 def utime(self, path: Optional[bytes] = None, t=None):
314 return os.utime(self.join(path), t)
314 return os.utime(self.join(path), t)
315
315
316 def walk(self, path: Optional[bytes] = None, onerror=None):
316 def walk(self, path: Optional[bytes] = None, onerror=None):
317 """Yield (dirpath, dirs, files) tuple for each directories under path
317 """Yield (dirpath, dirs, files) tuple for each directories under path
318
318
319 ``dirpath`` is relative one from the root of this vfs. This
319 ``dirpath`` is relative one from the root of this vfs. This
320 uses ``os.sep`` as path separator, even you specify POSIX
320 uses ``os.sep`` as path separator, even you specify POSIX
321 style ``path``.
321 style ``path``.
322
322
323 "The root of this vfs" is represented as empty ``dirpath``.
323 "The root of this vfs" is represented as empty ``dirpath``.
324 """
324 """
325 root = os.path.normpath(self.join(None))
325 root = os.path.normpath(self.join(None))
326 # when dirpath == root, dirpath[prefixlen:] becomes empty
326 # when dirpath == root, dirpath[prefixlen:] becomes empty
327 # because len(dirpath) < prefixlen.
327 # because len(dirpath) < prefixlen.
328 prefixlen = len(pathutil.normasprefix(root))
328 prefixlen = len(pathutil.normasprefix(root))
329 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
329 for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror):
330 yield (dirpath[prefixlen:], dirs, files)
330 yield (dirpath[prefixlen:], dirs, files)
331
331
332 @contextlib.contextmanager
332 @contextlib.contextmanager
333 def backgroundclosing(self, ui, expectedcount=-1):
333 def backgroundclosing(self, ui, expectedcount=-1):
334 """Allow files to be closed asynchronously.
334 """Allow files to be closed asynchronously.
335
335
336 When this context manager is active, ``backgroundclose`` can be passed
336 When this context manager is active, ``backgroundclose`` can be passed
337 to ``__call__``/``open`` to result in the file possibly being closed
337 to ``__call__``/``open`` to result in the file possibly being closed
338 asynchronously, on a background thread.
338 asynchronously, on a background thread.
339 """
339 """
340 # Sharing backgroundfilecloser between threads is complex and using
340 # Sharing backgroundfilecloser between threads is complex and using
341 # multiple instances puts us at risk of running out of file descriptors
341 # multiple instances puts us at risk of running out of file descriptors
342 # only allow to use backgroundfilecloser when in main thread.
342 # only allow to use backgroundfilecloser when in main thread.
343 if not isinstance(
343 if not isinstance(
344 threading.current_thread(),
344 threading.current_thread(),
345 threading._MainThread, # pytype: disable=module-attr
345 threading._MainThread, # pytype: disable=module-attr
346 ):
346 ):
347 yield
347 yield
348 return
348 return
349 vfs = getattr(self, 'vfs', self)
349 vfs = getattr(self, 'vfs', self)
350 if getattr(vfs, '_backgroundfilecloser', None):
350 if getattr(vfs, '_backgroundfilecloser', None):
351 raise error.Abort(
351 raise error.Abort(
352 _(b'can only have 1 active background file closer')
352 _(b'can only have 1 active background file closer')
353 )
353 )
354
354
355 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
355 with backgroundfilecloser(ui, expectedcount=expectedcount) as bfc:
356 try:
356 try:
357 vfs._backgroundfilecloser = (
357 vfs._backgroundfilecloser = (
358 bfc # pytype: disable=attribute-error
358 bfc # pytype: disable=attribute-error
359 )
359 )
360 yield bfc
360 yield bfc
361 finally:
361 finally:
362 vfs._backgroundfilecloser = (
362 vfs._backgroundfilecloser = (
363 None # pytype: disable=attribute-error
363 None # pytype: disable=attribute-error
364 )
364 )
365
365
366 def register_file(self, path):
366 def register_file(self, path):
367 """generic hook point to lets fncache steer its stew"""
367 """generic hook point to lets fncache steer its stew"""
368
368
369
369
370 class vfs(abstractvfs):
370 class vfs(abstractvfs):
371 """Operate files relative to a base directory
371 """Operate files relative to a base directory
372
372
373 This class is used to hide the details of COW semantics and
373 This class is used to hide the details of COW semantics and
374 remote file access from higher level code.
374 remote file access from higher level code.
375
375
376 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
376 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or
377 (b) the base directory is managed by hg and considered sort-of append-only.
377 (b) the base directory is managed by hg and considered sort-of append-only.
378 See pathutil.pathauditor() for details.
378 See pathutil.pathauditor() for details.
379 """
379 """
380
380
381 def __init__(
381 def __init__(
382 self,
382 self,
383 base: bytes,
383 base: bytes,
384 audit=True,
384 audit=True,
385 cacheaudited=False,
385 cacheaudited=False,
386 expandpath=False,
386 expandpath=False,
387 realpath=False,
387 realpath=False,
388 ):
388 ):
389 if expandpath:
389 if expandpath:
390 base = util.expandpath(base)
390 base = util.expandpath(base)
391 if realpath:
391 if realpath:
392 base = os.path.realpath(base)
392 base = os.path.realpath(base)
393 self.base = base
393 self.base = base
394 self._audit = audit
394 self._audit = audit
395 if audit:
395 if audit:
396 self.audit = pathutil.pathauditor(self.base, cached=cacheaudited)
396 self.audit = pathutil.pathauditor(self.base, cached=cacheaudited)
397 else:
397 else:
398 self.audit = lambda path, mode=None: True
398 self.audit = lambda path, mode=None: True
399 self.createmode = None
399 self.createmode = None
400 self._trustnlink = None
400 self._trustnlink = None
401 self.options = {}
401 self.options = {}
402
402
403 @util.propertycache
403 @util.propertycache
404 def _cansymlink(self) -> bool:
404 def _cansymlink(self) -> bool:
405 return util.checklink(self.base)
405 return util.checklink(self.base)
406
406
407 @util.propertycache
407 @util.propertycache
408 def _chmod(self):
408 def _chmod(self):
409 return util.checkexec(self.base)
409 return util.checkexec(self.base)
410
410
411 def _fixfilemode(self, name):
411 def _fixfilemode(self, name):
412 if self.createmode is None or not self._chmod:
412 if self.createmode is None or not self._chmod:
413 return
413 return
414 os.chmod(name, self.createmode & 0o666)
414 os.chmod(name, self.createmode & 0o666)
415
415
416 def _auditpath(self, path, mode) -> None:
416 def _auditpath(self, path, mode) -> None:
417 if self._audit:
417 if self._audit:
418 if os.path.isabs(path) and path.startswith(self.base):
418 if os.path.isabs(path) and path.startswith(self.base):
419 path = os.path.relpath(path, self.base)
419 path = os.path.relpath(path, self.base)
420 r = util.checkosfilename(path)
420 r = util.checkosfilename(path)
421 if r:
421 if r:
422 raise error.Abort(b"%s: %r" % (r, path))
422 raise error.Abort(b"%s: %r" % (r, path))
423 self.audit(path, mode=mode)
423 self.audit(path, mode=mode)
424
424
425 def isfileorlink_checkdir(
425 def isfileorlink_checkdir(
426 self, dircache, path: Optional[bytes] = None
426 self, dircache, path: Optional[bytes] = None
427 ) -> bool:
427 ) -> bool:
428 """return True if the path is a regular file or a symlink and
428 """return True if the path is a regular file or a symlink and
429 the directories along the path are "normal", that is
429 the directories along the path are "normal", that is
430 not symlinks or nested hg repositories.
430 not symlinks or nested hg repositories.
431
431
432 Ignores the `_audit` setting, and checks the directories regardless.
432 Ignores the `_audit` setting, and checks the directories regardless.
433 `dircache` is used to cache the directory checks.
433 `dircache` is used to cache the directory checks.
434 """
434 """
435 try:
435 try:
436 for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)):
436 for prefix in pathutil.finddirs_rev_noroot(util.localpath(path)):
437 if prefix in dircache:
437 if prefix in dircache:
438 res = dircache[prefix]
438 res = dircache[prefix]
439 else:
439 else:
440 res = pathutil.pathauditor._checkfs_exists(
440 res = pathutil.pathauditor._checkfs_exists(
441 self.base, prefix, path
441 self.base, prefix, path
442 )
442 )
443 dircache[prefix] = res
443 dircache[prefix] = res
444 if not res:
444 if not res:
445 return False
445 return False
446 except (OSError, error.Abort):
446 except (OSError, error.Abort):
447 return False
447 return False
448 return self.isfileorlink(path)
448 return self.isfileorlink(path)
449
449
450 def __call__(
450 def __call__(
451 self,
451 self,
452 path: bytes,
452 path: bytes,
453 mode: bytes = b"rb",
453 mode: bytes = b"rb",
454 atomictemp=False,
454 atomictemp=False,
455 notindexed=False,
455 notindexed=False,
456 backgroundclose=False,
456 backgroundclose=False,
457 checkambig=False,
457 checkambig=False,
458 auditpath=True,
458 auditpath=True,
459 makeparentdirs=True,
459 makeparentdirs=True,
460 ):
460 ):
461 """Open ``path`` file, which is relative to vfs root.
461 """Open ``path`` file, which is relative to vfs root.
462
462
463 By default, parent directories are created as needed. Newly created
463 By default, parent directories are created as needed. Newly created
464 directories are marked as "not to be indexed by the content indexing
464 directories are marked as "not to be indexed by the content indexing
465 service", if ``notindexed`` is specified for "write" mode access.
465 service", if ``notindexed`` is specified for "write" mode access.
466 Set ``makeparentdirs=False`` to not create directories implicitly.
466 Set ``makeparentdirs=False`` to not create directories implicitly.
467
467
468 If ``backgroundclose`` is passed, the file may be closed asynchronously.
468 If ``backgroundclose`` is passed, the file may be closed asynchronously.
469 It can only be used if the ``self.backgroundclosing()`` context manager
469 It can only be used if the ``self.backgroundclosing()`` context manager
470 is active. This should only be specified if the following criteria hold:
470 is active. This should only be specified if the following criteria hold:
471
471
472 1. There is a potential for writing thousands of files. Unless you
472 1. There is a potential for writing thousands of files. Unless you
473 are writing thousands of files, the performance benefits of
473 are writing thousands of files, the performance benefits of
474 asynchronously closing files is not realized.
474 asynchronously closing files is not realized.
475 2. Files are opened exactly once for the ``backgroundclosing``
475 2. Files are opened exactly once for the ``backgroundclosing``
476 active duration and are therefore free of race conditions between
476 active duration and are therefore free of race conditions between
477 closing a file on a background thread and reopening it. (If the
477 closing a file on a background thread and reopening it. (If the
478 file were opened multiple times, there could be unflushed data
478 file were opened multiple times, there could be unflushed data
479 because the original file handle hasn't been flushed/closed yet.)
479 because the original file handle hasn't been flushed/closed yet.)
480
480
481 ``checkambig`` argument is passed to atomictempfile (valid
481 ``checkambig`` argument is passed to atomictempfile (valid
482 only for writing), and is useful only if target file is
482 only for writing), and is useful only if target file is
483 guarded by any lock (e.g. repo.lock or repo.wlock).
483 guarded by any lock (e.g. repo.lock or repo.wlock).
484
484
485 To avoid file stat ambiguity forcibly, checkambig=True involves
485 To avoid file stat ambiguity forcibly, checkambig=True involves
486 copying ``path`` file opened in "append" mode (e.g. for
486 copying ``path`` file opened in "append" mode (e.g. for
487 truncation), if it is owned by another. Therefore, use
487 truncation), if it is owned by another. Therefore, use
488 combination of append mode and checkambig=True only in limited
488 combination of append mode and checkambig=True only in limited
489 cases (see also issue5418 and issue5584 for detail).
489 cases (see also issue5418 and issue5584 for detail).
490 """
490 """
491 if auditpath:
491 if auditpath:
492 self._auditpath(path, mode)
492 self._auditpath(path, mode)
493 f = self.join(path)
493 f = self.join(path)
494
494
495 if b"b" not in mode:
495 if b"b" not in mode:
496 mode += b"b" # for that other OS
496 mode += b"b" # for that other OS
497
497
498 nlink = -1
498 nlink = -1
499 if mode not in (b'r', b'rb'):
499 if mode not in (b'r', b'rb'):
500 dirname, basename = util.split(f)
500 dirname, basename = util.split(f)
501 # If basename is empty, then the path is malformed because it points
501 # If basename is empty, then the path is malformed because it points
502 # to a directory. Let the posixfile() call below raise IOError.
502 # to a directory. Let the posixfile() call below raise IOError.
503 if basename:
503 if basename:
504 if atomictemp:
504 if atomictemp:
505 if makeparentdirs:
505 if makeparentdirs:
506 util.makedirs(dirname, self.createmode, notindexed)
506 util.makedirs(dirname, self.createmode, notindexed)
507 return util.atomictempfile(
507 return util.atomictempfile(
508 f, mode, self.createmode, checkambig=checkambig
508 f, mode, self.createmode, checkambig=checkambig
509 )
509 )
510 try:
510 try:
511 if b'w' in mode:
511 if b'w' in mode:
512 util.unlink(f)
512 util.unlink(f)
513 nlink = 0
513 nlink = 0
514 else:
514 else:
515 # nlinks() may behave differently for files on Windows
515 # nlinks() may behave differently for files on Windows
516 # shares if the file is open.
516 # shares if the file is open.
517 with util.posixfile(f):
517 with util.posixfile(f):
518 nlink = util.nlinks(f)
518 nlink = util.nlinks(f)
519 if nlink < 1:
519 if nlink < 1:
520 nlink = 2 # force mktempcopy (issue1922)
520 nlink = 2 # force mktempcopy (issue1922)
521 except FileNotFoundError:
521 except FileNotFoundError:
522 nlink = 0
522 nlink = 0
523 if makeparentdirs:
523 if makeparentdirs:
524 util.makedirs(dirname, self.createmode, notindexed)
524 util.makedirs(dirname, self.createmode, notindexed)
525 if nlink > 0:
525 if nlink > 0:
526 if self._trustnlink is None:
526 if self._trustnlink is None:
527 self._trustnlink = nlink > 1 or util.checknlink(f)
527 self._trustnlink = nlink > 1 or util.checknlink(f)
528 if nlink > 1 or not self._trustnlink:
528 if nlink > 1 or not self._trustnlink:
529 util.rename(util.mktempcopy(f), f)
529 util.rename(util.mktempcopy(f), f)
530 fp = util.posixfile(f, mode)
530 fp = util.posixfile(f, mode)
531 if nlink == 0:
531 if nlink == 0:
532 self._fixfilemode(f)
532 self._fixfilemode(f)
533
533
534 if checkambig:
534 if checkambig:
535 if mode in (b'r', b'rb'):
535 if mode in (b'r', b'rb'):
536 raise error.Abort(
536 raise error.Abort(
537 _(
537 _(
538 b'implementation error: mode %s is not'
538 b'implementation error: mode %s is not'
539 b' valid for checkambig=True'
539 b' valid for checkambig=True'
540 )
540 )
541 % mode
541 % mode
542 )
542 )
543 fp = checkambigatclosing(fp)
543 fp = checkambigatclosing(fp)
544
544
545 if backgroundclose and isinstance(
545 if backgroundclose and isinstance(
546 threading.current_thread(),
546 threading.current_thread(),
547 threading._MainThread, # pytype: disable=module-attr
547 threading._MainThread, # pytype: disable=module-attr
548 ):
548 ):
549 if (
549 if (
550 not self._backgroundfilecloser # pytype: disable=attribute-error
550 not self._backgroundfilecloser # pytype: disable=attribute-error
551 ):
551 ):
552 raise error.Abort(
552 raise error.Abort(
553 _(
553 _(
554 b'backgroundclose can only be used when a '
554 b'backgroundclose can only be used when a '
555 b'backgroundclosing context manager is active'
555 b'backgroundclosing context manager is active'
556 )
556 )
557 )
557 )
558
558
559 fp = delayclosedfile(
559 fp = delayclosedfile(
560 fp,
560 fp,
561 self._backgroundfilecloser, # pytype: disable=attribute-error
561 self._backgroundfilecloser, # pytype: disable=attribute-error
562 )
562 )
563
563
564 return fp
564 return fp
565
565
566 def symlink(self, src: bytes, dst: bytes) -> None:
566 def symlink(self, src: bytes, dst: bytes) -> None:
567 self.audit(dst)
567 self.audit(dst)
568 linkname = self.join(dst)
568 linkname = self.join(dst)
569 util.tryunlink(linkname)
569 util.tryunlink(linkname)
570
570
571 util.makedirs(os.path.dirname(linkname), self.createmode)
571 util.makedirs(os.path.dirname(linkname), self.createmode)
572
572
573 if self._cansymlink:
573 if self._cansymlink:
574 try:
574 try:
575 os.symlink(src, linkname)
575 os.symlink(src, linkname)
576 except OSError as err:
576 except OSError as err:
577 raise OSError(
577 raise OSError(
578 err.errno,
578 err.errno,
579 _(b'could not symlink to %r: %s')
579 _(b'could not symlink to %r: %s')
580 % (src, encoding.strtolocal(err.strerror)),
580 % (src, encoding.strtolocal(err.strerror)),
581 linkname,
581 linkname,
582 )
582 )
583 else:
583 else:
584 self.write(dst, src)
584 self.write(dst, src)
585
585
586 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
586 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
587 if path:
587 if path:
588 parts = [self.base, path]
588 parts = [self.base, path]
589 parts.extend(insidef)
589 parts.extend(insidef)
590 return self._join(*parts)
590 return self._join(*parts)
591 else:
591 else:
592 return self.base
592 return self.base
593
593
594
594
595 opener = vfs
595 opener = vfs
596
596
597
597
598 class proxyvfs(abstractvfs):
598 class proxyvfs(abstractvfs):
599 def __init__(self, vfs: "vfs"):
599 def __init__(self, vfs: "vfs"):
600 self.vfs = vfs
600 self.vfs = vfs
601
601
602 @property
603 def createmode(self):
604 return self.vfs.createmode
605
602 def _auditpath(self, path, mode):
606 def _auditpath(self, path, mode):
603 return self.vfs._auditpath(path, mode)
607 return self.vfs._auditpath(path, mode)
604
608
605 @property
609 @property
606 def options(self):
610 def options(self):
607 return self.vfs.options
611 return self.vfs.options
608
612
609 @options.setter
613 @options.setter
610 def options(self, value):
614 def options(self, value):
611 self.vfs.options = value
615 self.vfs.options = value
612
616
613
617
614 class filtervfs(proxyvfs, abstractvfs):
618 class filtervfs(proxyvfs, abstractvfs):
615 '''Wrapper vfs for filtering filenames with a function.'''
619 '''Wrapper vfs for filtering filenames with a function.'''
616
620
617 def __init__(self, vfs: "vfs", filter):
621 def __init__(self, vfs: "vfs", filter):
618 proxyvfs.__init__(self, vfs)
622 proxyvfs.__init__(self, vfs)
619 self._filter = filter
623 self._filter = filter
620
624
621 def __call__(self, path: bytes, *args, **kwargs):
625 def __call__(self, path: bytes, *args, **kwargs):
622 return self.vfs(self._filter(path), *args, **kwargs)
626 return self.vfs(self._filter(path), *args, **kwargs)
623
627
624 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
628 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
625 if path:
629 if path:
626 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
630 return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef)))
627 else:
631 else:
628 return self.vfs.join(path)
632 return self.vfs.join(path)
629
633
630
634
631 filteropener = filtervfs
635 filteropener = filtervfs
632
636
633
637
634 class readonlyvfs(proxyvfs):
638 class readonlyvfs(proxyvfs):
635 '''Wrapper vfs preventing any writing.'''
639 '''Wrapper vfs preventing any writing.'''
636
640
637 def __init__(self, vfs: "vfs"):
641 def __init__(self, vfs: "vfs"):
638 proxyvfs.__init__(self, vfs)
642 proxyvfs.__init__(self, vfs)
639
643
640 def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw):
644 def __call__(self, path: bytes, mode: bytes = b'rb', *args, **kw):
641 if mode not in (b'r', b'rb'):
645 if mode not in (b'r', b'rb'):
642 raise error.Abort(_(b'this vfs is read only'))
646 raise error.Abort(_(b'this vfs is read only'))
643 return self.vfs(path, mode, *args, **kw)
647 return self.vfs(path, mode, *args, **kw)
644
648
645 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
649 def join(self, path: Optional[bytes], *insidef: bytes) -> bytes:
646 return self.vfs.join(path, *insidef)
650 return self.vfs.join(path, *insidef)
647
651
648
652
649 class closewrapbase:
653 class closewrapbase:
650 """Base class of wrapper, which hooks closing
654 """Base class of wrapper, which hooks closing
651
655
652 Do not instantiate outside of the vfs layer.
656 Do not instantiate outside of the vfs layer.
653 """
657 """
654
658
655 def __init__(self, fh):
659 def __init__(self, fh):
656 object.__setattr__(self, '_origfh', fh)
660 object.__setattr__(self, '_origfh', fh)
657
661
658 def __getattr__(self, attr):
662 def __getattr__(self, attr):
659 return getattr(self._origfh, attr)
663 return getattr(self._origfh, attr)
660
664
661 def __setattr__(self, attr, value):
665 def __setattr__(self, attr, value):
662 return setattr(self._origfh, attr, value)
666 return setattr(self._origfh, attr, value)
663
667
664 def __delattr__(self, attr):
668 def __delattr__(self, attr):
665 return delattr(self._origfh, attr)
669 return delattr(self._origfh, attr)
666
670
667 def __enter__(self):
671 def __enter__(self):
668 self._origfh.__enter__()
672 self._origfh.__enter__()
669 return self
673 return self
670
674
671 def __exit__(self, exc_type, exc_value, exc_tb):
675 def __exit__(self, exc_type, exc_value, exc_tb):
672 raise NotImplementedError('attempted instantiating ' + str(type(self)))
676 raise NotImplementedError('attempted instantiating ' + str(type(self)))
673
677
674 def close(self):
678 def close(self):
675 raise NotImplementedError('attempted instantiating ' + str(type(self)))
679 raise NotImplementedError('attempted instantiating ' + str(type(self)))
676
680
677
681
678 class delayclosedfile(closewrapbase):
682 class delayclosedfile(closewrapbase):
679 """Proxy for a file object whose close is delayed.
683 """Proxy for a file object whose close is delayed.
680
684
681 Do not instantiate outside of the vfs layer.
685 Do not instantiate outside of the vfs layer.
682 """
686 """
683
687
684 def __init__(self, fh, closer):
688 def __init__(self, fh, closer):
685 super(delayclosedfile, self).__init__(fh)
689 super(delayclosedfile, self).__init__(fh)
686 object.__setattr__(self, '_closer', closer)
690 object.__setattr__(self, '_closer', closer)
687
691
688 def __exit__(self, exc_type, exc_value, exc_tb):
692 def __exit__(self, exc_type, exc_value, exc_tb):
689 self._closer.close(self._origfh)
693 self._closer.close(self._origfh)
690
694
691 def close(self):
695 def close(self):
692 self._closer.close(self._origfh)
696 self._closer.close(self._origfh)
693
697
694
698
695 class backgroundfilecloser:
699 class backgroundfilecloser:
696 """Coordinates background closing of file handles on multiple threads."""
700 """Coordinates background closing of file handles on multiple threads."""
697
701
698 def __init__(self, ui, expectedcount=-1):
702 def __init__(self, ui, expectedcount=-1):
699 self._running = False
703 self._running = False
700 self._entered = False
704 self._entered = False
701 self._threads = []
705 self._threads = []
702 self._threadexception = None
706 self._threadexception = None
703
707
704 # Only Windows/NTFS has slow file closing. So only enable by default
708 # Only Windows/NTFS has slow file closing. So only enable by default
705 # on that platform. But allow to be enabled elsewhere for testing.
709 # on that platform. But allow to be enabled elsewhere for testing.
706 defaultenabled = pycompat.iswindows
710 defaultenabled = pycompat.iswindows
707 enabled = ui.configbool(b'worker', b'backgroundclose', defaultenabled)
711 enabled = ui.configbool(b'worker', b'backgroundclose', defaultenabled)
708
712
709 if not enabled:
713 if not enabled:
710 return
714 return
711
715
712 # There is overhead to starting and stopping the background threads.
716 # There is overhead to starting and stopping the background threads.
713 # Don't do background processing unless the file count is large enough
717 # Don't do background processing unless the file count is large enough
714 # to justify it.
718 # to justify it.
715 minfilecount = ui.configint(b'worker', b'backgroundcloseminfilecount')
719 minfilecount = ui.configint(b'worker', b'backgroundcloseminfilecount')
716 # FUTURE dynamically start background threads after minfilecount closes.
720 # FUTURE dynamically start background threads after minfilecount closes.
717 # (We don't currently have any callers that don't know their file count)
721 # (We don't currently have any callers that don't know their file count)
718 if expectedcount > 0 and expectedcount < minfilecount:
722 if expectedcount > 0 and expectedcount < minfilecount:
719 return
723 return
720
724
721 maxqueue = ui.configint(b'worker', b'backgroundclosemaxqueue')
725 maxqueue = ui.configint(b'worker', b'backgroundclosemaxqueue')
722 threadcount = ui.configint(b'worker', b'backgroundclosethreadcount')
726 threadcount = ui.configint(b'worker', b'backgroundclosethreadcount')
723
727
724 ui.debug(
728 ui.debug(
725 b'starting %d threads for background file closing\n' % threadcount
729 b'starting %d threads for background file closing\n' % threadcount
726 )
730 )
727
731
728 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
732 self._queue = pycompat.queue.Queue(maxsize=maxqueue)
729 self._running = True
733 self._running = True
730
734
731 for i in range(threadcount):
735 for i in range(threadcount):
732 t = threading.Thread(target=self._worker, name='backgroundcloser')
736 t = threading.Thread(target=self._worker, name='backgroundcloser')
733 self._threads.append(t)
737 self._threads.append(t)
734 t.start()
738 t.start()
735
739
736 def __enter__(self):
740 def __enter__(self):
737 self._entered = True
741 self._entered = True
738 return self
742 return self
739
743
740 def __exit__(self, exc_type, exc_value, exc_tb):
744 def __exit__(self, exc_type, exc_value, exc_tb):
741 self._running = False
745 self._running = False
742
746
743 # Wait for threads to finish closing so open files don't linger for
747 # Wait for threads to finish closing so open files don't linger for
744 # longer than lifetime of context manager.
748 # longer than lifetime of context manager.
745 for t in self._threads:
749 for t in self._threads:
746 t.join()
750 t.join()
747
751
748 def _worker(self):
752 def _worker(self):
749 """Main routine for worker thread."""
753 """Main routine for worker thread."""
750 while True:
754 while True:
751 try:
755 try:
752 fh = self._queue.get(block=True, timeout=0.100)
756 fh = self._queue.get(block=True, timeout=0.100)
753 # Need to catch or the thread will terminate and
757 # Need to catch or the thread will terminate and
754 # we could orphan file descriptors.
758 # we could orphan file descriptors.
755 try:
759 try:
756 fh.close()
760 fh.close()
757 except Exception as e:
761 except Exception as e:
758 # Stash so can re-raise from main thread later.
762 # Stash so can re-raise from main thread later.
759 self._threadexception = e
763 self._threadexception = e
760 except pycompat.queue.Empty:
764 except pycompat.queue.Empty:
761 if not self._running:
765 if not self._running:
762 break
766 break
763
767
764 def close(self, fh):
768 def close(self, fh):
765 """Schedule a file for closing."""
769 """Schedule a file for closing."""
766 if not self._entered:
770 if not self._entered:
767 raise error.Abort(
771 raise error.Abort(
768 _(b'can only call close() when context manager active')
772 _(b'can only call close() when context manager active')
769 )
773 )
770
774
771 # If a background thread encountered an exception, raise now so we fail
775 # If a background thread encountered an exception, raise now so we fail
772 # fast. Otherwise we may potentially go on for minutes until the error
776 # fast. Otherwise we may potentially go on for minutes until the error
773 # is acted on.
777 # is acted on.
774 if self._threadexception:
778 if self._threadexception:
775 e = self._threadexception
779 e = self._threadexception
776 self._threadexception = None
780 self._threadexception = None
777 raise e
781 raise e
778
782
779 # If we're not actively running, close synchronously.
783 # If we're not actively running, close synchronously.
780 if not self._running:
784 if not self._running:
781 fh.close()
785 fh.close()
782 return
786 return
783
787
784 self._queue.put(fh, block=True, timeout=None)
788 self._queue.put(fh, block=True, timeout=None)
785
789
786
790
787 class checkambigatclosing(closewrapbase):
791 class checkambigatclosing(closewrapbase):
788 """Proxy for a file object, to avoid ambiguity of file stat
792 """Proxy for a file object, to avoid ambiguity of file stat
789
793
790 See also util.filestat for detail about "ambiguity of file stat".
794 See also util.filestat for detail about "ambiguity of file stat".
791
795
792 This proxy is useful only if the target file is guarded by any
796 This proxy is useful only if the target file is guarded by any
793 lock (e.g. repo.lock or repo.wlock)
797 lock (e.g. repo.lock or repo.wlock)
794
798
795 Do not instantiate outside of the vfs layer.
799 Do not instantiate outside of the vfs layer.
796 """
800 """
797
801
798 def __init__(self, fh):
802 def __init__(self, fh):
799 super(checkambigatclosing, self).__init__(fh)
803 super(checkambigatclosing, self).__init__(fh)
800 object.__setattr__(self, '_oldstat', util.filestat.frompath(fh.name))
804 object.__setattr__(self, '_oldstat', util.filestat.frompath(fh.name))
801
805
802 def _checkambig(self):
806 def _checkambig(self):
803 oldstat = self._oldstat
807 oldstat = self._oldstat
804 if oldstat.stat:
808 if oldstat.stat:
805 _avoidambig(self._origfh.name, oldstat)
809 _avoidambig(self._origfh.name, oldstat)
806
810
807 def __exit__(self, exc_type, exc_value, exc_tb):
811 def __exit__(self, exc_type, exc_value, exc_tb):
808 self._origfh.__exit__(exc_type, exc_value, exc_tb)
812 self._origfh.__exit__(exc_type, exc_value, exc_tb)
809 self._checkambig()
813 self._checkambig()
810
814
811 def close(self):
815 def close(self):
812 self._origfh.close()
816 self._origfh.close()
813 self._checkambig()
817 self._checkambig()
General Comments 0
You need to be logged in to leave comments. Login now