##// END OF EJS Templates
checklink: degrade gracefully on posix when fs is readonly (issue5511)...
Augie Fackler -
r32393:38a2b9d9 default
parent child Browse files
Show More
@@ -1,649 +1,659 b''
1 1 # posix.py - Posix utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import fcntl
12 12 import getpass
13 13 import grp
14 14 import os
15 15 import pwd
16 16 import re
17 17 import select
18 18 import stat
19 19 import sys
20 20 import tempfile
21 21 import unicodedata
22 22
23 23 from .i18n import _
24 24 from . import (
25 25 encoding,
26 26 pycompat,
27 27 )
28 28
29 29 posixfile = open
30 30 normpath = os.path.normpath
31 31 samestat = os.path.samestat
32 32 try:
33 33 oslink = os.link
34 34 except AttributeError:
35 35 # Some platforms build Python without os.link on systems that are
36 36 # vaguely unix-like but don't have hardlink support. For those
37 37 # poor souls, just say we tried and that it failed so we fall back
38 38 # to copies.
39 39 def oslink(src, dst):
40 40 raise OSError(errno.EINVAL,
41 41 'hardlinks not supported: %s to %s' % (src, dst))
42 42 unlink = os.unlink
43 43 rename = os.rename
44 44 removedirs = os.removedirs
45 45 expandglobs = False
46 46
47 47 umask = os.umask(0)
48 48 os.umask(umask)
49 49
50 50 def split(p):
51 51 '''Same as posixpath.split, but faster
52 52
53 53 >>> import posixpath
54 54 >>> for f in ['/absolute/path/to/file',
55 55 ... 'relative/path/to/file',
56 56 ... 'file_alone',
57 57 ... 'path/to/directory/',
58 58 ... '/multiple/path//separators',
59 59 ... '/file_at_root',
60 60 ... '///multiple_leading_separators_at_root',
61 61 ... '']:
62 62 ... assert split(f) == posixpath.split(f), f
63 63 '''
64 64 ht = p.rsplit('/', 1)
65 65 if len(ht) == 1:
66 66 return '', p
67 67 nh = ht[0].rstrip('/')
68 68 if nh:
69 69 return nh, ht[1]
70 70 return ht[0] + '/', ht[1]
71 71
72 72 def openhardlinks():
73 73 '''return true if it is safe to hold open file handles to hardlinks'''
74 74 return True
75 75
76 76 def nlinks(name):
77 77 '''return number of hardlinks for the given file'''
78 78 return os.lstat(name).st_nlink
79 79
80 80 def parsepatchoutput(output_line):
81 81 """parses the output produced by patch and returns the filename"""
82 82 pf = output_line[14:]
83 83 if pycompat.sysplatform == 'OpenVMS':
84 84 if pf[0] == '`':
85 85 pf = pf[1:-1] # Remove the quotes
86 86 else:
87 87 if pf.startswith("'") and pf.endswith("'") and " " in pf:
88 88 pf = pf[1:-1] # Remove the quotes
89 89 return pf
90 90
91 91 def sshargs(sshcmd, host, user, port):
92 92 '''Build argument list for ssh'''
93 93 args = user and ("%s@%s" % (user, host)) or host
94 94 return port and ("%s -p %s" % (args, port)) or args
95 95
96 96 def isexec(f):
97 97 """check whether a file is executable"""
98 98 return (os.lstat(f).st_mode & 0o100 != 0)
99 99
100 100 def setflags(f, l, x):
101 101 s = os.lstat(f).st_mode
102 102 if l:
103 103 if not stat.S_ISLNK(s):
104 104 # switch file to link
105 105 fp = open(f)
106 106 data = fp.read()
107 107 fp.close()
108 108 unlink(f)
109 109 try:
110 110 os.symlink(data, f)
111 111 except OSError:
112 112 # failed to make a link, rewrite file
113 113 fp = open(f, "w")
114 114 fp.write(data)
115 115 fp.close()
116 116 # no chmod needed at this point
117 117 return
118 118 if stat.S_ISLNK(s):
119 119 # switch link to file
120 120 data = os.readlink(f)
121 121 unlink(f)
122 122 fp = open(f, "w")
123 123 fp.write(data)
124 124 fp.close()
125 125 s = 0o666 & ~umask # avoid restatting for chmod
126 126
127 127 sx = s & 0o100
128 128 if x and not sx:
129 129 # Turn on +x for every +r bit when making a file executable
130 130 # and obey umask.
131 131 os.chmod(f, s | (s & 0o444) >> 2 & ~umask)
132 132 elif not x and sx:
133 133 # Turn off all +x bits
134 134 os.chmod(f, s & 0o666)
135 135
136 136 def copymode(src, dst, mode=None):
137 137 '''Copy the file mode from the file at path src to dst.
138 138 If src doesn't exist, we're using mode instead. If mode is None, we're
139 139 using umask.'''
140 140 try:
141 141 st_mode = os.lstat(src).st_mode & 0o777
142 142 except OSError as inst:
143 143 if inst.errno != errno.ENOENT:
144 144 raise
145 145 st_mode = mode
146 146 if st_mode is None:
147 147 st_mode = ~umask
148 148 st_mode &= 0o666
149 149 os.chmod(dst, st_mode)
150 150
151 151 def checkexec(path):
152 152 """
153 153 Check whether the given path is on a filesystem with UNIX-like exec flags
154 154
155 155 Requires a directory (like /foo/.hg)
156 156 """
157 157
158 158 # VFAT on some Linux versions can flip mode but it doesn't persist
159 159 # a FS remount. Frequently we can detect it if files are created
160 160 # with exec bit on.
161 161
162 162 try:
163 163 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
164 164 cachedir = os.path.join(path, '.hg', 'cache')
165 165 if os.path.isdir(cachedir):
166 166 checkisexec = os.path.join(cachedir, 'checkisexec')
167 167 checknoexec = os.path.join(cachedir, 'checknoexec')
168 168
169 169 try:
170 170 m = os.stat(checkisexec).st_mode
171 171 except OSError as e:
172 172 if e.errno != errno.ENOENT:
173 173 raise
174 174 # checkisexec does not exist - fall through ...
175 175 else:
176 176 # checkisexec exists, check if it actually is exec
177 177 if m & EXECFLAGS != 0:
178 178 # ensure checkisexec exists, check it isn't exec
179 179 try:
180 180 m = os.stat(checknoexec).st_mode
181 181 except OSError as e:
182 182 if e.errno != errno.ENOENT:
183 183 raise
184 184 open(checknoexec, 'w').close() # might fail
185 185 m = os.stat(checknoexec).st_mode
186 186 if m & EXECFLAGS == 0:
187 187 # check-exec is exec and check-no-exec is not exec
188 188 return True
189 189 # checknoexec exists but is exec - delete it
190 190 unlink(checknoexec)
191 191 # checkisexec exists but is not exec - delete it
192 192 unlink(checkisexec)
193 193
194 194 # check using one file, leave it as checkisexec
195 195 checkdir = cachedir
196 196 else:
197 197 # check directly in path and don't leave checkisexec behind
198 198 checkdir = path
199 199 checkisexec = None
200 200 fh, fn = tempfile.mkstemp(dir=checkdir, prefix='hg-checkexec-')
201 201 try:
202 202 os.close(fh)
203 203 m = os.stat(fn).st_mode
204 204 if m & EXECFLAGS == 0:
205 205 os.chmod(fn, m & 0o777 | EXECFLAGS)
206 206 if os.stat(fn).st_mode & EXECFLAGS != 0:
207 207 if checkisexec is not None:
208 208 os.rename(fn, checkisexec)
209 209 fn = None
210 210 return True
211 211 finally:
212 212 if fn is not None:
213 213 unlink(fn)
214 214 except (IOError, OSError):
215 215 # we don't care, the user probably won't be able to commit anyway
216 216 return False
217 217
218 218 def checklink(path):
219 219 """check whether the given path is on a symlink-capable filesystem"""
220 220 # mktemp is not racy because symlink creation will fail if the
221 221 # file already exists
222 222 while True:
223 223 cachedir = os.path.join(path, '.hg', 'cache')
224 224 checklink = os.path.join(cachedir, 'checklink')
225 225 # try fast path, read only
226 226 if os.path.islink(checklink):
227 227 return True
228 228 if os.path.isdir(cachedir):
229 229 checkdir = cachedir
230 230 else:
231 231 checkdir = path
232 232 cachedir = None
233 233 fscheckdir = pycompat.fsdecode(checkdir)
234 234 name = tempfile.mktemp(dir=fscheckdir,
235 235 prefix=r'checklink-')
236 236 name = pycompat.fsencode(name)
237 237 try:
238 238 fd = None
239 239 if cachedir is None:
240 240 fd = tempfile.NamedTemporaryFile(dir=fscheckdir,
241 241 prefix=r'hg-checklink-')
242 242 target = pycompat.fsencode(os.path.basename(fd.name))
243 243 else:
244 244 # create a fixed file to link to; doesn't matter if it
245 245 # already exists.
246 246 target = 'checklink-target'
247 try:
247 248 open(os.path.join(cachedir, target), 'w').close()
249 except IOError as inst:
250 if inst[0] == errno.EACCES:
251 # If we can't write to cachedir, just pretend
252 # that the fs is readonly and by association
253 # that the fs won't support symlinks. This
254 # seems like the least dangerous way to avoid
255 # data loss.
256 return False
257 raise
248 258 try:
249 259 os.symlink(target, name)
250 260 if cachedir is None:
251 261 unlink(name)
252 262 else:
253 263 try:
254 264 os.rename(name, checklink)
255 265 except OSError:
256 266 unlink(name)
257 267 return True
258 268 except OSError as inst:
259 269 # link creation might race, try again
260 270 if inst[0] == errno.EEXIST:
261 271 continue
262 272 raise
263 273 finally:
264 274 if fd is not None:
265 275 fd.close()
266 276 except AttributeError:
267 277 return False
268 278 except OSError as inst:
269 279 # sshfs might report failure while successfully creating the link
270 280 if inst[0] == errno.EIO and os.path.exists(name):
271 281 unlink(name)
272 282 return False
273 283
274 284 def checkosfilename(path):
275 285 '''Check that the base-relative path is a valid filename on this platform.
276 286 Returns None if the path is ok, or a UI string describing the problem.'''
277 287 pass # on posix platforms, every path is ok
278 288
279 289 def setbinary(fd):
280 290 pass
281 291
282 292 def pconvert(path):
283 293 return path
284 294
285 295 def localpath(path):
286 296 return path
287 297
288 298 def samefile(fpath1, fpath2):
289 299 """Returns whether path1 and path2 refer to the same file. This is only
290 300 guaranteed to work for files, not directories."""
291 301 return os.path.samefile(fpath1, fpath2)
292 302
293 303 def samedevice(fpath1, fpath2):
294 304 """Returns whether fpath1 and fpath2 are on the same device. This is only
295 305 guaranteed to work for files, not directories."""
296 306 st1 = os.lstat(fpath1)
297 307 st2 = os.lstat(fpath2)
298 308 return st1.st_dev == st2.st_dev
299 309
300 310 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
301 311 def normcase(path):
302 312 return path.lower()
303 313
304 314 # what normcase does to ASCII strings
305 315 normcasespec = encoding.normcasespecs.lower
306 316 # fallback normcase function for non-ASCII strings
307 317 normcasefallback = normcase
308 318
309 319 if pycompat.sysplatform == 'darwin':
310 320
311 321 def normcase(path):
312 322 '''
313 323 Normalize a filename for OS X-compatible comparison:
314 324 - escape-encode invalid characters
315 325 - decompose to NFD
316 326 - lowercase
317 327 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
318 328
319 329 >>> normcase('UPPER')
320 330 'upper'
321 331 >>> normcase('Caf\xc3\xa9')
322 332 'cafe\\xcc\\x81'
323 333 >>> normcase('\xc3\x89')
324 334 'e\\xcc\\x81'
325 335 >>> normcase('\xb8\xca\xc3\xca\xbe\xc8.JPG') # issue3918
326 336 '%b8%ca%c3\\xca\\xbe%c8.jpg'
327 337 '''
328 338
329 339 try:
330 340 return encoding.asciilower(path) # exception for non-ASCII
331 341 except UnicodeDecodeError:
332 342 return normcasefallback(path)
333 343
334 344 normcasespec = encoding.normcasespecs.lower
335 345
336 346 def normcasefallback(path):
337 347 try:
338 348 u = path.decode('utf-8')
339 349 except UnicodeDecodeError:
340 350 # OS X percent-encodes any bytes that aren't valid utf-8
341 351 s = ''
342 352 pos = 0
343 353 l = len(path)
344 354 while pos < l:
345 355 try:
346 356 c = encoding.getutf8char(path, pos)
347 357 pos += len(c)
348 358 except ValueError:
349 359 c = '%%%02X' % ord(path[pos])
350 360 pos += 1
351 361 s += c
352 362
353 363 u = s.decode('utf-8')
354 364
355 365 # Decompose then lowercase (HFS+ technote specifies lower)
356 366 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
357 367 # drop HFS+ ignored characters
358 368 return encoding.hfsignoreclean(enc)
359 369
360 370 if pycompat.sysplatform == 'cygwin':
361 371 # workaround for cygwin, in which mount point part of path is
362 372 # treated as case sensitive, even though underlying NTFS is case
363 373 # insensitive.
364 374
365 375 # default mount points
366 376 cygwinmountpoints = sorted([
367 377 "/usr/bin",
368 378 "/usr/lib",
369 379 "/cygdrive",
370 380 ], reverse=True)
371 381
372 382 # use upper-ing as normcase as same as NTFS workaround
373 383 def normcase(path):
374 384 pathlen = len(path)
375 385 if (pathlen == 0) or (path[0] != pycompat.ossep):
376 386 # treat as relative
377 387 return encoding.upper(path)
378 388
379 389 # to preserve case of mountpoint part
380 390 for mp in cygwinmountpoints:
381 391 if not path.startswith(mp):
382 392 continue
383 393
384 394 mplen = len(mp)
385 395 if mplen == pathlen: # mount point itself
386 396 return mp
387 397 if path[mplen] == pycompat.ossep:
388 398 return mp + encoding.upper(path[mplen:])
389 399
390 400 return encoding.upper(path)
391 401
392 402 normcasespec = encoding.normcasespecs.other
393 403 normcasefallback = normcase
394 404
395 405 # Cygwin translates native ACLs to POSIX permissions,
396 406 # but these translations are not supported by native
397 407 # tools, so the exec bit tends to be set erroneously.
398 408 # Therefore, disable executable bit access on Cygwin.
399 409 def checkexec(path):
400 410 return False
401 411
402 412 # Similarly, Cygwin's symlink emulation is likely to create
403 413 # problems when Mercurial is used from both Cygwin and native
404 414 # Windows, with other native tools, or on shared volumes
405 415 def checklink(path):
406 416 return False
407 417
408 418 _needsshellquote = None
409 419 def shellquote(s):
410 420 if pycompat.sysplatform == 'OpenVMS':
411 421 return '"%s"' % s
412 422 global _needsshellquote
413 423 if _needsshellquote is None:
414 424 _needsshellquote = re.compile(br'[^a-zA-Z0-9._/+-]').search
415 425 if s and not _needsshellquote(s):
416 426 # "s" shouldn't have to be quoted
417 427 return s
418 428 else:
419 429 return "'%s'" % s.replace("'", "'\\''")
420 430
421 431 def quotecommand(cmd):
422 432 return cmd
423 433
424 434 def popen(command, mode='r'):
425 435 return os.popen(command, mode)
426 436
427 437 def testpid(pid):
428 438 '''return False if pid dead, True if running or not sure'''
429 439 if pycompat.sysplatform == 'OpenVMS':
430 440 return True
431 441 try:
432 442 os.kill(pid, 0)
433 443 return True
434 444 except OSError as inst:
435 445 return inst.errno != errno.ESRCH
436 446
437 447 def explainexit(code):
438 448 """return a 2-tuple (desc, code) describing a subprocess status
439 449 (codes from kill are negative - not os.system/wait encoding)"""
440 450 if code >= 0:
441 451 return _("exited with status %d") % code, code
442 452 return _("killed by signal %d") % -code, -code
443 453
444 454 def isowner(st):
445 455 """Return True if the stat object st is from the current user."""
446 456 return st.st_uid == os.getuid()
447 457
448 458 def findexe(command):
449 459 '''Find executable for command searching like which does.
450 460 If command is a basename then PATH is searched for command.
451 461 PATH isn't searched if command is an absolute or relative path.
452 462 If command isn't found None is returned.'''
453 463 if pycompat.sysplatform == 'OpenVMS':
454 464 return command
455 465
456 466 def findexisting(executable):
457 467 'Will return executable if existing file'
458 468 if os.path.isfile(executable) and os.access(executable, os.X_OK):
459 469 return executable
460 470 return None
461 471
462 472 if pycompat.ossep in command:
463 473 return findexisting(command)
464 474
465 475 if pycompat.sysplatform == 'plan9':
466 476 return findexisting(os.path.join('/bin', command))
467 477
468 478 for path in encoding.environ.get('PATH', '').split(pycompat.ospathsep):
469 479 executable = findexisting(os.path.join(path, command))
470 480 if executable is not None:
471 481 return executable
472 482 return None
473 483
474 484 def setsignalhandler():
475 485 pass
476 486
477 487 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
478 488
479 489 def statfiles(files):
480 490 '''Stat each file in files. Yield each stat, or None if a file does not
481 491 exist or has a type we don't care about.'''
482 492 lstat = os.lstat
483 493 getkind = stat.S_IFMT
484 494 for nf in files:
485 495 try:
486 496 st = lstat(nf)
487 497 if getkind(st.st_mode) not in _wantedkinds:
488 498 st = None
489 499 except OSError as err:
490 500 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
491 501 raise
492 502 st = None
493 503 yield st
494 504
495 505 def getuser():
496 506 '''return name of current user'''
497 507 return pycompat.fsencode(getpass.getuser())
498 508
499 509 def username(uid=None):
500 510 """Return the name of the user with the given uid.
501 511
502 512 If uid is None, return the name of the current user."""
503 513
504 514 if uid is None:
505 515 uid = os.getuid()
506 516 try:
507 517 return pwd.getpwuid(uid)[0]
508 518 except KeyError:
509 519 return str(uid)
510 520
511 521 def groupname(gid=None):
512 522 """Return the name of the group with the given gid.
513 523
514 524 If gid is None, return the name of the current group."""
515 525
516 526 if gid is None:
517 527 gid = os.getgid()
518 528 try:
519 529 return grp.getgrgid(gid)[0]
520 530 except KeyError:
521 531 return str(gid)
522 532
523 533 def groupmembers(name):
524 534 """Return the list of members of the group with the given
525 535 name, KeyError if the group does not exist.
526 536 """
527 537 return list(grp.getgrnam(name).gr_mem)
528 538
529 539 def spawndetached(args):
530 540 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
531 541 args[0], args)
532 542
533 543 def gethgcmd():
534 544 return sys.argv[:1]
535 545
536 546 def makedir(path, notindexed):
537 547 os.mkdir(path)
538 548
539 549 def lookupreg(key, name=None, scope=None):
540 550 return None
541 551
542 552 def hidewindow():
543 553 """Hide current shell window.
544 554
545 555 Used to hide the window opened when starting asynchronous
546 556 child process under Windows, unneeded on other systems.
547 557 """
548 558 pass
549 559
550 560 class cachestat(object):
551 561 def __init__(self, path):
552 562 self.stat = os.stat(path)
553 563
554 564 def cacheable(self):
555 565 return bool(self.stat.st_ino)
556 566
557 567 __hash__ = object.__hash__
558 568
559 569 def __eq__(self, other):
560 570 try:
561 571 # Only dev, ino, size, mtime and atime are likely to change. Out
562 572 # of these, we shouldn't compare atime but should compare the
563 573 # rest. However, one of the other fields changing indicates
564 574 # something fishy going on, so return False if anything but atime
565 575 # changes.
566 576 return (self.stat.st_mode == other.stat.st_mode and
567 577 self.stat.st_ino == other.stat.st_ino and
568 578 self.stat.st_dev == other.stat.st_dev and
569 579 self.stat.st_nlink == other.stat.st_nlink and
570 580 self.stat.st_uid == other.stat.st_uid and
571 581 self.stat.st_gid == other.stat.st_gid and
572 582 self.stat.st_size == other.stat.st_size and
573 583 self.stat.st_mtime == other.stat.st_mtime and
574 584 self.stat.st_ctime == other.stat.st_ctime)
575 585 except AttributeError:
576 586 return False
577 587
578 588 def __ne__(self, other):
579 589 return not self == other
580 590
581 591 def executablepath():
582 592 return None # available on Windows only
583 593
584 594 def statislink(st):
585 595 '''check whether a stat result is a symlink'''
586 596 return st and stat.S_ISLNK(st.st_mode)
587 597
588 598 def statisexec(st):
589 599 '''check whether a stat result is an executable file'''
590 600 return st and (st.st_mode & 0o100 != 0)
591 601
592 602 def poll(fds):
593 603 """block until something happens on any file descriptor
594 604
595 605 This is a generic helper that will check for any activity
596 606 (read, write. exception) and return the list of touched files.
597 607
598 608 In unsupported cases, it will raise a NotImplementedError"""
599 609 try:
600 610 while True:
601 611 try:
602 612 res = select.select(fds, fds, fds)
603 613 break
604 614 except select.error as inst:
605 615 if inst.args[0] == errno.EINTR:
606 616 continue
607 617 raise
608 618 except ValueError: # out of range file descriptor
609 619 raise NotImplementedError()
610 620 return sorted(list(set(sum(res, []))))
611 621
612 622 def readpipe(pipe):
613 623 """Read all available data from a pipe."""
614 624 # We can't fstat() a pipe because Linux will always report 0.
615 625 # So, we set the pipe to non-blocking mode and read everything
616 626 # that's available.
617 627 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
618 628 flags |= os.O_NONBLOCK
619 629 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
620 630
621 631 try:
622 632 chunks = []
623 633 while True:
624 634 try:
625 635 s = pipe.read()
626 636 if not s:
627 637 break
628 638 chunks.append(s)
629 639 except IOError:
630 640 break
631 641
632 642 return ''.join(chunks)
633 643 finally:
634 644 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
635 645
636 646 def bindunixsocket(sock, path):
637 647 """Bind the UNIX domain socket to the specified path"""
638 648 # use relative path instead of full path at bind() if possible, since
639 649 # AF_UNIX path has very small length limit (107 chars) on common
640 650 # platforms (see sys/un.h)
641 651 dirname, basename = os.path.split(path)
642 652 bakwdfd = None
643 653 if dirname:
644 654 bakwdfd = os.open('.', os.O_DIRECTORY)
645 655 os.chdir(dirname)
646 656 sock.bind(basename)
647 657 if bakwdfd:
648 658 os.fchdir(bakwdfd)
649 659 os.close(bakwdfd)
General Comments 0
You need to be logged in to leave comments. Login now