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