##// END OF EJS Templates
windows: handle file-like objects without isatty() method...
Manuel Jacob -
r45717:74b48622 default
parent child Browse files
Show More
@@ -1,685 +1,693 b''
1 1 # windows.py - Windows 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 getpass
12 12 import msvcrt
13 13 import os
14 14 import re
15 15 import stat
16 16 import string
17 17 import sys
18 18
19 19 from .i18n import _
20 20 from .pycompat import getattr
21 21 from . import (
22 22 encoding,
23 23 error,
24 24 policy,
25 25 pycompat,
26 26 win32,
27 27 )
28 28
29 29 try:
30 30 import _winreg as winreg # pytype: disable=import-error
31 31
32 32 winreg.CloseKey
33 33 except ImportError:
34 34 # py2 only
35 35 import winreg # pytype: disable=import-error
36 36
37 37 osutil = policy.importmod('osutil')
38 38
39 39 getfsmountpoint = win32.getvolumename
40 40 getfstype = win32.getfstype
41 41 getuser = win32.getuser
42 42 hidewindow = win32.hidewindow
43 43 makedir = win32.makedir
44 44 nlinks = win32.nlinks
45 45 oslink = win32.oslink
46 46 samedevice = win32.samedevice
47 47 samefile = win32.samefile
48 48 setsignalhandler = win32.setsignalhandler
49 49 spawndetached = win32.spawndetached
50 50 split = os.path.split
51 51 testpid = win32.testpid
52 52 unlink = win32.unlink
53 53
54 54 umask = 0o022
55 55
56 56
57 57 class mixedfilemodewrapper(object):
58 58 """Wraps a file handle when it is opened in read/write mode.
59 59
60 60 fopen() and fdopen() on Windows have a specific-to-Windows requirement
61 61 that files opened with mode r+, w+, or a+ make a call to a file positioning
62 62 function when switching between reads and writes. Without this extra call,
63 63 Python will raise a not very intuitive "IOError: [Errno 0] Error."
64 64
65 65 This class wraps posixfile instances when the file is opened in read/write
66 66 mode and automatically adds checks or inserts appropriate file positioning
67 67 calls when necessary.
68 68 """
69 69
70 70 OPNONE = 0
71 71 OPREAD = 1
72 72 OPWRITE = 2
73 73
74 74 def __init__(self, fp):
75 75 object.__setattr__(self, '_fp', fp)
76 76 object.__setattr__(self, '_lastop', 0)
77 77
78 78 def __enter__(self):
79 79 self._fp.__enter__()
80 80 return self
81 81
82 82 def __exit__(self, exc_type, exc_val, exc_tb):
83 83 self._fp.__exit__(exc_type, exc_val, exc_tb)
84 84
85 85 def __getattr__(self, name):
86 86 return getattr(self._fp, name)
87 87
88 88 def __setattr__(self, name, value):
89 89 return self._fp.__setattr__(name, value)
90 90
91 91 def _noopseek(self):
92 92 self._fp.seek(0, os.SEEK_CUR)
93 93
94 94 def seek(self, *args, **kwargs):
95 95 object.__setattr__(self, '_lastop', self.OPNONE)
96 96 return self._fp.seek(*args, **kwargs)
97 97
98 98 def write(self, d):
99 99 if self._lastop == self.OPREAD:
100 100 self._noopseek()
101 101
102 102 object.__setattr__(self, '_lastop', self.OPWRITE)
103 103 return self._fp.write(d)
104 104
105 105 def writelines(self, *args, **kwargs):
106 106 if self._lastop == self.OPREAD:
107 107 self._noopeseek()
108 108
109 109 object.__setattr__(self, '_lastop', self.OPWRITE)
110 110 return self._fp.writelines(*args, **kwargs)
111 111
112 112 def read(self, *args, **kwargs):
113 113 if self._lastop == self.OPWRITE:
114 114 self._noopseek()
115 115
116 116 object.__setattr__(self, '_lastop', self.OPREAD)
117 117 return self._fp.read(*args, **kwargs)
118 118
119 119 def readline(self, *args, **kwargs):
120 120 if self._lastop == self.OPWRITE:
121 121 self._noopseek()
122 122
123 123 object.__setattr__(self, '_lastop', self.OPREAD)
124 124 return self._fp.readline(*args, **kwargs)
125 125
126 126 def readlines(self, *args, **kwargs):
127 127 if self._lastop == self.OPWRITE:
128 128 self._noopseek()
129 129
130 130 object.__setattr__(self, '_lastop', self.OPREAD)
131 131 return self._fp.readlines(*args, **kwargs)
132 132
133 133
134 134 class fdproxy(object):
135 135 """Wraps osutil.posixfile() to override the name attribute to reflect the
136 136 underlying file name.
137 137 """
138 138
139 139 def __init__(self, name, fp):
140 140 self.name = name
141 141 self._fp = fp
142 142
143 143 def __enter__(self):
144 144 self._fp.__enter__()
145 145 # Return this wrapper for the context manager so that the name is
146 146 # still available.
147 147 return self
148 148
149 149 def __exit__(self, exc_type, exc_value, traceback):
150 150 self._fp.__exit__(exc_type, exc_value, traceback)
151 151
152 152 def __iter__(self):
153 153 return iter(self._fp)
154 154
155 155 def __getattr__(self, name):
156 156 return getattr(self._fp, name)
157 157
158 158
159 159 def posixfile(name, mode=b'r', buffering=-1):
160 160 '''Open a file with even more POSIX-like semantics'''
161 161 try:
162 162 fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError
163 163
164 164 # PyFile_FromFd() ignores the name, and seems to report fp.name as the
165 165 # underlying file descriptor.
166 166 if pycompat.ispy3:
167 167 fp = fdproxy(name, fp)
168 168
169 169 # The position when opening in append mode is implementation defined, so
170 170 # make it consistent with other platforms, which position at EOF.
171 171 if b'a' in mode:
172 172 fp.seek(0, os.SEEK_END)
173 173
174 174 if b'+' in mode:
175 175 return mixedfilemodewrapper(fp)
176 176
177 177 return fp
178 178 except WindowsError as err:
179 179 # convert to a friendlier exception
180 180 raise IOError(
181 181 err.errno, '%s: %s' % (encoding.strfromlocal(name), err.strerror)
182 182 )
183 183
184 184
185 185 # may be wrapped by win32mbcs extension
186 186 listdir = osutil.listdir
187 187
188 188
189 # copied from .utils.procutil, remove after Python 2 support was dropped
190 def _isatty(fp):
191 try:
192 return fp.isatty()
193 except AttributeError:
194 return False
195
196
189 197 class winstdout(object):
190 198 '''Some files on Windows misbehave.
191 199
192 200 When writing to a broken pipe, EINVAL instead of EPIPE may be raised.
193 201
194 202 When writing too many bytes to a console at the same, a "Not enough space"
195 203 error may happen. Python 3 already works around that.
196 204 '''
197 205
198 206 def __init__(self, fp):
199 207 self.fp = fp
200 self.throttle = not pycompat.ispy3 and fp.isatty()
208 self.throttle = not pycompat.ispy3 and _isatty(fp)
201 209
202 210 def __getattr__(self, key):
203 211 return getattr(self.fp, key)
204 212
205 213 def close(self):
206 214 try:
207 215 self.fp.close()
208 216 except IOError:
209 217 pass
210 218
211 219 def write(self, s):
212 220 if not pycompat.ispy3:
213 221 self.softspace = 0
214 222 try:
215 223 if not self.throttle:
216 224 return self.fp.write(s)
217 225 # This is workaround for "Not enough space" error on
218 226 # writing large size of data to console.
219 227 limit = 16000
220 228 l = len(s)
221 229 start = 0
222 230 while start < l:
223 231 end = start + limit
224 232 self.fp.write(s[start:end])
225 233 start = end
226 234 except IOError as inst:
227 235 if inst.errno != 0 and not win32.lasterrorwaspipeerror(inst):
228 236 raise
229 237 self.close()
230 238 raise IOError(errno.EPIPE, 'Broken pipe')
231 239
232 240 def flush(self):
233 241 try:
234 242 return self.fp.flush()
235 243 except IOError as inst:
236 244 if not win32.lasterrorwaspipeerror(inst):
237 245 raise
238 246 raise IOError(errno.EPIPE, 'Broken pipe')
239 247
240 248
241 249 def openhardlinks():
242 250 return True
243 251
244 252
245 253 def parsepatchoutput(output_line):
246 254 """parses the output produced by patch and returns the filename"""
247 255 pf = output_line[14:]
248 256 if pf[0] == b'`':
249 257 pf = pf[1:-1] # Remove the quotes
250 258 return pf
251 259
252 260
253 261 def sshargs(sshcmd, host, user, port):
254 262 '''Build argument list for ssh or Plink'''
255 263 pflag = b'plink' in sshcmd.lower() and b'-P' or b'-p'
256 264 args = user and (b"%s@%s" % (user, host)) or host
257 265 if args.startswith(b'-') or args.startswith(b'/'):
258 266 raise error.Abort(
259 267 _(b'illegal ssh hostname or username starting with - or /: %s')
260 268 % args
261 269 )
262 270 args = shellquote(args)
263 271 if port:
264 272 args = b'%s %s %s' % (pflag, shellquote(port), args)
265 273 return args
266 274
267 275
268 276 def setflags(f, l, x):
269 277 pass
270 278
271 279
272 280 def copymode(src, dst, mode=None, enforcewritable=False):
273 281 pass
274 282
275 283
276 284 def checkexec(path):
277 285 return False
278 286
279 287
280 288 def checklink(path):
281 289 return False
282 290
283 291
284 292 def setbinary(fd):
285 293 # When run without console, pipes may expose invalid
286 294 # fileno(), usually set to -1.
287 295 fno = getattr(fd, 'fileno', None)
288 296 if fno is not None and fno() >= 0:
289 297 msvcrt.setmode(fno(), os.O_BINARY) # pytype: disable=module-attr
290 298
291 299
292 300 def pconvert(path):
293 301 return path.replace(pycompat.ossep, b'/')
294 302
295 303
296 304 def localpath(path):
297 305 return path.replace(b'/', b'\\')
298 306
299 307
300 308 def normpath(path):
301 309 return pconvert(os.path.normpath(path))
302 310
303 311
304 312 def normcase(path):
305 313 return encoding.upper(path) # NTFS compares via upper()
306 314
307 315
308 316 # see posix.py for definitions
309 317 normcasespec = encoding.normcasespecs.upper
310 318 normcasefallback = encoding.upperfallback
311 319
312 320
313 321 def samestat(s1, s2):
314 322 return False
315 323
316 324
317 325 def shelltocmdexe(path, env):
318 326 r"""Convert shell variables in the form $var and ${var} inside ``path``
319 327 to %var% form. Existing Windows style variables are left unchanged.
320 328
321 329 The variables are limited to the given environment. Unknown variables are
322 330 left unchanged.
323 331
324 332 >>> e = {b'var1': b'v1', b'var2': b'v2', b'var3': b'v3'}
325 333 >>> # Only valid values are expanded
326 334 >>> shelltocmdexe(b'cmd $var1 ${var2} %var3% $missing ${missing} %missing%',
327 335 ... e)
328 336 'cmd %var1% %var2% %var3% $missing ${missing} %missing%'
329 337 >>> # Single quote prevents expansion, as does \$ escaping
330 338 >>> shelltocmdexe(b"cmd '$var1 ${var2} %var3%' \$var1 \${var2} \\", e)
331 339 'cmd "$var1 ${var2} %var3%" $var1 ${var2} \\'
332 340 >>> # $$ is not special. %% is not special either, but can be the end and
333 341 >>> # start of consecutive variables
334 342 >>> shelltocmdexe(b"cmd $$ %% %var1%%var2%", e)
335 343 'cmd $$ %% %var1%%var2%'
336 344 >>> # No double substitution
337 345 >>> shelltocmdexe(b"$var1 %var1%", {b'var1': b'%var2%', b'var2': b'boom'})
338 346 '%var1% %var1%'
339 347 >>> # Tilde expansion
340 348 >>> shelltocmdexe(b"~/dir ~\dir2 ~tmpfile \~/", {})
341 349 '%USERPROFILE%/dir %USERPROFILE%\\dir2 ~tmpfile ~/'
342 350 """
343 351 if not any(c in path for c in b"$'~"):
344 352 return path
345 353
346 354 varchars = pycompat.sysbytes(string.ascii_letters + string.digits) + b'_-'
347 355
348 356 res = b''
349 357 index = 0
350 358 pathlen = len(path)
351 359 while index < pathlen:
352 360 c = path[index : index + 1]
353 361 if c == b'\'': # no expansion within single quotes
354 362 path = path[index + 1 :]
355 363 pathlen = len(path)
356 364 try:
357 365 index = path.index(b'\'')
358 366 res += b'"' + path[:index] + b'"'
359 367 except ValueError:
360 368 res += c + path
361 369 index = pathlen - 1
362 370 elif c == b'%': # variable
363 371 path = path[index + 1 :]
364 372 pathlen = len(path)
365 373 try:
366 374 index = path.index(b'%')
367 375 except ValueError:
368 376 res += b'%' + path
369 377 index = pathlen - 1
370 378 else:
371 379 var = path[:index]
372 380 res += b'%' + var + b'%'
373 381 elif c == b'$': # variable
374 382 if path[index + 1 : index + 2] == b'{':
375 383 path = path[index + 2 :]
376 384 pathlen = len(path)
377 385 try:
378 386 index = path.index(b'}')
379 387 var = path[:index]
380 388
381 389 # See below for why empty variables are handled specially
382 390 if env.get(var, b'') != b'':
383 391 res += b'%' + var + b'%'
384 392 else:
385 393 res += b'${' + var + b'}'
386 394 except ValueError:
387 395 res += b'${' + path
388 396 index = pathlen - 1
389 397 else:
390 398 var = b''
391 399 index += 1
392 400 c = path[index : index + 1]
393 401 while c != b'' and c in varchars:
394 402 var += c
395 403 index += 1
396 404 c = path[index : index + 1]
397 405 # Some variables (like HG_OLDNODE) may be defined, but have an
398 406 # empty value. Those need to be skipped because when spawning
399 407 # cmd.exe to run the hook, it doesn't replace %VAR% for an empty
400 408 # VAR, and that really confuses things like revset expressions.
401 409 # OTOH, if it's left in Unix format and the hook runs sh.exe, it
402 410 # will substitute to an empty string, and everything is happy.
403 411 if env.get(var, b'') != b'':
404 412 res += b'%' + var + b'%'
405 413 else:
406 414 res += b'$' + var
407 415
408 416 if c != b'':
409 417 index -= 1
410 418 elif (
411 419 c == b'~'
412 420 and index + 1 < pathlen
413 421 and path[index + 1 : index + 2] in (b'\\', b'/')
414 422 ):
415 423 res += b"%USERPROFILE%"
416 424 elif (
417 425 c == b'\\'
418 426 and index + 1 < pathlen
419 427 and path[index + 1 : index + 2] in (b'$', b'~')
420 428 ):
421 429 # Skip '\', but only if it is escaping $ or ~
422 430 res += path[index + 1 : index + 2]
423 431 index += 1
424 432 else:
425 433 res += c
426 434
427 435 index += 1
428 436 return res
429 437
430 438
431 439 # A sequence of backslashes is special iff it precedes a double quote:
432 440 # - if there's an even number of backslashes, the double quote is not
433 441 # quoted (i.e. it ends the quoted region)
434 442 # - if there's an odd number of backslashes, the double quote is quoted
435 443 # - in both cases, every pair of backslashes is unquoted into a single
436 444 # backslash
437 445 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
438 446 # So, to quote a string, we must surround it in double quotes, double
439 447 # the number of backslashes that precede double quotes and add another
440 448 # backslash before every double quote (being careful with the double
441 449 # quote we've appended to the end)
442 450 _quotere = None
443 451 _needsshellquote = None
444 452
445 453
446 454 def shellquote(s):
447 455 r"""
448 456 >>> shellquote(br'C:\Users\xyz')
449 457 '"C:\\Users\\xyz"'
450 458 >>> shellquote(br'C:\Users\xyz/mixed')
451 459 '"C:\\Users\\xyz/mixed"'
452 460 >>> # Would be safe not to quote too, since it is all double backslashes
453 461 >>> shellquote(br'C:\\Users\\xyz')
454 462 '"C:\\\\Users\\\\xyz"'
455 463 >>> # But this must be quoted
456 464 >>> shellquote(br'C:\\Users\\xyz/abc')
457 465 '"C:\\\\Users\\\\xyz/abc"'
458 466 """
459 467 global _quotere
460 468 if _quotere is None:
461 469 _quotere = re.compile(br'(\\*)("|\\$)')
462 470 global _needsshellquote
463 471 if _needsshellquote is None:
464 472 # ":" is also treated as "safe character", because it is used as a part
465 473 # of path name on Windows. "\" is also part of a path name, but isn't
466 474 # safe because shlex.split() (kind of) treats it as an escape char and
467 475 # drops it. It will leave the next character, even if it is another
468 476 # "\".
469 477 _needsshellquote = re.compile(br'[^a-zA-Z0-9._:/-]').search
470 478 if s and not _needsshellquote(s) and not _quotere.search(s):
471 479 # "s" shouldn't have to be quoted
472 480 return s
473 481 return b'"%s"' % _quotere.sub(br'\1\1\\\2', s)
474 482
475 483
476 484 def _unquote(s):
477 485 if s.startswith(b'"') and s.endswith(b'"'):
478 486 return s[1:-1]
479 487 return s
480 488
481 489
482 490 def shellsplit(s):
483 491 """Parse a command string in cmd.exe way (best-effort)"""
484 492 return pycompat.maplist(_unquote, pycompat.shlexsplit(s, posix=False))
485 493
486 494
487 495 # if you change this stub into a real check, please try to implement the
488 496 # username and groupname functions above, too.
489 497 def isowner(st):
490 498 return True
491 499
492 500
493 501 def findexe(command):
494 502 '''Find executable for command searching like cmd.exe does.
495 503 If command is a basename then PATH is searched for command.
496 504 PATH isn't searched if command is an absolute or relative path.
497 505 An extension from PATHEXT is found and added if not present.
498 506 If command isn't found None is returned.'''
499 507 pathext = encoding.environ.get(b'PATHEXT', b'.COM;.EXE;.BAT;.CMD')
500 508 pathexts = [ext for ext in pathext.lower().split(pycompat.ospathsep)]
501 509 if os.path.splitext(command)[1].lower() in pathexts:
502 510 pathexts = [b'']
503 511
504 512 def findexisting(pathcommand):
505 513 """Will append extension (if needed) and return existing file"""
506 514 for ext in pathexts:
507 515 executable = pathcommand + ext
508 516 if os.path.exists(executable):
509 517 return executable
510 518 return None
511 519
512 520 if pycompat.ossep in command:
513 521 return findexisting(command)
514 522
515 523 for path in encoding.environ.get(b'PATH', b'').split(pycompat.ospathsep):
516 524 executable = findexisting(os.path.join(path, command))
517 525 if executable is not None:
518 526 return executable
519 527 return findexisting(os.path.expanduser(os.path.expandvars(command)))
520 528
521 529
522 530 _wantedkinds = {stat.S_IFREG, stat.S_IFLNK}
523 531
524 532
525 533 def statfiles(files):
526 534 '''Stat each file in files. Yield each stat, or None if a file
527 535 does not exist or has a type we don't care about.
528 536
529 537 Cluster and cache stat per directory to minimize number of OS stat calls.'''
530 538 dircache = {} # dirname -> filename -> status | None if file does not exist
531 539 getkind = stat.S_IFMT
532 540 for nf in files:
533 541 nf = normcase(nf)
534 542 dir, base = os.path.split(nf)
535 543 if not dir:
536 544 dir = b'.'
537 545 cache = dircache.get(dir, None)
538 546 if cache is None:
539 547 try:
540 548 dmap = {
541 549 normcase(n): s
542 550 for n, k, s in listdir(dir, True)
543 551 if getkind(s.st_mode) in _wantedkinds
544 552 }
545 553 except OSError as err:
546 554 # Python >= 2.5 returns ENOENT and adds winerror field
547 555 # EINVAL is raised if dir is not a directory.
548 556 if err.errno not in (errno.ENOENT, errno.EINVAL, errno.ENOTDIR):
549 557 raise
550 558 dmap = {}
551 559 cache = dircache.setdefault(dir, dmap)
552 560 yield cache.get(base, None)
553 561
554 562
555 563 def username(uid=None):
556 564 """Return the name of the user with the given uid.
557 565
558 566 If uid is None, return the name of the current user."""
559 567 if not uid:
560 568 return pycompat.fsencode(getpass.getuser())
561 569 return None
562 570
563 571
564 572 def groupname(gid=None):
565 573 """Return the name of the group with the given gid.
566 574
567 575 If gid is None, return the name of the current group."""
568 576 return None
569 577
570 578
571 579 def readlink(pathname):
572 580 return pycompat.fsencode(os.readlink(pycompat.fsdecode(pathname)))
573 581
574 582
575 583 def removedirs(name):
576 584 """special version of os.removedirs that does not remove symlinked
577 585 directories or junction points if they actually contain files"""
578 586 if listdir(name):
579 587 return
580 588 os.rmdir(name)
581 589 head, tail = os.path.split(name)
582 590 if not tail:
583 591 head, tail = os.path.split(head)
584 592 while head and tail:
585 593 try:
586 594 if listdir(head):
587 595 return
588 596 os.rmdir(head)
589 597 except (ValueError, OSError):
590 598 break
591 599 head, tail = os.path.split(head)
592 600
593 601
594 602 def rename(src, dst):
595 603 '''atomically rename file src to dst, replacing dst if it exists'''
596 604 try:
597 605 os.rename(src, dst)
598 606 except OSError as e:
599 607 if e.errno != errno.EEXIST:
600 608 raise
601 609 unlink(dst)
602 610 os.rename(src, dst)
603 611
604 612
605 613 def gethgcmd():
606 614 return [encoding.strtolocal(arg) for arg in [sys.executable] + sys.argv[:1]]
607 615
608 616
609 617 def groupmembers(name):
610 618 # Don't support groups on Windows for now
611 619 raise KeyError
612 620
613 621
614 622 def isexec(f):
615 623 return False
616 624
617 625
618 626 class cachestat(object):
619 627 def __init__(self, path):
620 628 pass
621 629
622 630 def cacheable(self):
623 631 return False
624 632
625 633
626 634 def lookupreg(key, valname=None, scope=None):
627 635 ''' Look up a key/value name in the Windows registry.
628 636
629 637 valname: value name. If unspecified, the default value for the key
630 638 is used.
631 639 scope: optionally specify scope for registry lookup, this can be
632 640 a sequence of scopes to look up in order. Default (CURRENT_USER,
633 641 LOCAL_MACHINE).
634 642 '''
635 643 if scope is None:
636 644 scope = (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE)
637 645 elif not isinstance(scope, (list, tuple)):
638 646 scope = (scope,)
639 647 for s in scope:
640 648 try:
641 649 with winreg.OpenKey(s, encoding.strfromlocal(key)) as hkey:
642 650 name = valname and encoding.strfromlocal(valname) or valname
643 651 val = winreg.QueryValueEx(hkey, name)[0]
644 652 # never let a Unicode string escape into the wild
645 653 return encoding.unitolocal(val)
646 654 except EnvironmentError:
647 655 pass
648 656
649 657
650 658 expandglobs = True
651 659
652 660
653 661 def statislink(st):
654 662 '''check whether a stat result is a symlink'''
655 663 return False
656 664
657 665
658 666 def statisexec(st):
659 667 '''check whether a stat result is an executable file'''
660 668 return False
661 669
662 670
663 671 def poll(fds):
664 672 # see posix.py for description
665 673 raise NotImplementedError()
666 674
667 675
668 676 def readpipe(pipe):
669 677 """Read all available data from a pipe."""
670 678 chunks = []
671 679 while True:
672 680 size = win32.peekpipe(pipe)
673 681 if not size:
674 682 break
675 683
676 684 s = pipe.read(size)
677 685 if not s:
678 686 break
679 687 chunks.append(s)
680 688
681 689 return b''.join(chunks)
682 690
683 691
684 692 def bindunixsocket(sock, path):
685 693 raise NotImplementedError('unsupported platform')
General Comments 0
You need to be logged in to leave comments. Login now