__init__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. # coding: utf-8
  2. import os
  3. import stat
  4. import sys
  5. import shutil
  6. import logging
  7. from six import reraise
  8. import library.python.func
  9. import library.python.strings
  10. logger = logging.getLogger(__name__)
  11. ERRORS = {
  12. 'SUCCESS': 0,
  13. 'PATH_NOT_FOUND': 3,
  14. 'ACCESS_DENIED': 5,
  15. 'SHARING_VIOLATION': 32,
  16. 'INSUFFICIENT_BUFFER': 122,
  17. 'DIR_NOT_EMPTY': 145,
  18. }
  19. RETRIABLE_FILE_ERRORS = (ERRORS['ACCESS_DENIED'], ERRORS['SHARING_VIOLATION'])
  20. RETRIABLE_DIR_ERRORS = (ERRORS['ACCESS_DENIED'], ERRORS['DIR_NOT_EMPTY'], ERRORS['SHARING_VIOLATION'])
  21. @library.python.func.lazy
  22. def on_win():
  23. """Check if code run on Windows"""
  24. return os.name == 'nt'
  25. class NotOnWindowsError(RuntimeError):
  26. def __init__(self, message):
  27. super(NotOnWindowsError, self).__init__(message)
  28. class DisabledOnWindowsError(RuntimeError):
  29. def __init__(self, message):
  30. super(DisabledOnWindowsError, self).__init__(message)
  31. class NoCTypesError(RuntimeError):
  32. def __init__(self, message):
  33. super(NoCTypesError, self).__init__(message)
  34. def win_only(f):
  35. """Decorator for Windows-only functions"""
  36. def f_wrapped(*args, **kwargs):
  37. if not on_win():
  38. raise NotOnWindowsError('Windows-only function is called, but platform is not Windows')
  39. return f(*args, **kwargs)
  40. return f_wrapped
  41. def win_disabled(f):
  42. """Decorator for functions disabled on Windows"""
  43. def f_wrapped(*args, **kwargs):
  44. if on_win():
  45. run_disabled()
  46. return f(*args, **kwargs)
  47. return f_wrapped
  48. def errorfix(f):
  49. if not on_win():
  50. return f
  51. def f_wrapped(*args, **kwargs):
  52. try:
  53. return f(*args, **kwargs)
  54. except WindowsError:
  55. tp, value, tb = sys.exc_info()
  56. fix_error(value)
  57. reraise(tp, value, tb)
  58. return f_wrapped
  59. def diehard(winerrors, tries=100, delay=1):
  60. """
  61. Decorator for diehard wrapper
  62. On Windows platform retries to run function while specific WindowsError is thrown
  63. On non-Windows platforms fallbacks to function itself
  64. """
  65. def wrap(f):
  66. if not on_win():
  67. return f
  68. return lambda *args, **kwargs: run_diehard(f, winerrors, tries, delay, *args, **kwargs)
  69. return wrap
  70. def win_path_fix(path):
  71. """Fix slashes in paths on windows"""
  72. return path if sys.platform != 'win32' else path.replace('\\', '/')
  73. if on_win():
  74. import msvcrt
  75. import time
  76. import library.python.strings
  77. _has_ctypes = True
  78. try:
  79. import ctypes
  80. from ctypes import wintypes
  81. except ImportError:
  82. _has_ctypes = False
  83. _INVALID_HANDLE_VALUE = -1
  84. _MOVEFILE_REPLACE_EXISTING = 0x1
  85. _MOVEFILE_WRITE_THROUGH = 0x8
  86. _SEM_FAILCRITICALERRORS = 0x1
  87. _SEM_NOGPFAULTERRORBOX = 0x2
  88. _SEM_NOALIGNMENTFAULTEXCEPT = 0x4
  89. _SEM_NOOPENFILEERRORBOX = 0x8
  90. _SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
  91. _CREATE_NO_WINDOW = 0x8000000
  92. _ATOMIC_RENAME_FILE_TRANSACTION_DEFAULT_TIMEOUT = 1000
  93. _HANDLE_FLAG_INHERIT = 0x1
  94. @win_only
  95. def require_ctypes(f):
  96. def f_wrapped(*args, **kwargs):
  97. if not _has_ctypes:
  98. raise NoCTypesError('No ctypes found')
  99. return f(*args, **kwargs)
  100. return f_wrapped
  101. # Run function in diehard mode (see diehard decorator commentary)
  102. @win_only
  103. def run_diehard(f, winerrors, tries, delay, *args, **kwargs):
  104. if isinstance(winerrors, int):
  105. winerrors = (winerrors,)
  106. ei = None
  107. for t in range(tries):
  108. if t:
  109. logger.debug('Diehard [errs %s]: try #%d in %s', ','.join(str(x) for x in winerrors), t, f)
  110. try:
  111. return f(*args, **kwargs)
  112. except WindowsError as e:
  113. if e.winerror not in winerrors:
  114. raise
  115. ei = sys.exc_info()
  116. time.sleep(delay)
  117. reraise(ei[0], ei[1], ei[2])
  118. @win_only
  119. def run_disabled(*args, **kwargs):
  120. """Placeholder for disabled functions"""
  121. raise DisabledOnWindowsError('Function called is disabled on Windows')
  122. class CustomWinError(WindowsError):
  123. def __init__(self, winerror, message='', filename=None):
  124. super(CustomWinError, self).__init__(winerror, message)
  125. self.message = message
  126. self.strerror = self.message if self.message else format_error(self.windows_error)
  127. self.filename = filename
  128. self.utf8 = True
  129. @win_only
  130. def unicode_path(path):
  131. return library.python.strings.to_unicode(path, library.python.strings.fs_encoding())
  132. @win_only
  133. @require_ctypes
  134. def format_error(error):
  135. if isinstance(error, WindowsError):
  136. error = error.winerror
  137. if not isinstance(error, int):
  138. return 'Unknown'
  139. return ctypes.FormatError(error)
  140. @win_only
  141. def fix_error(windows_error):
  142. if not windows_error.strerror:
  143. windows_error.strerror = format_error(windows_error)
  144. transcode_error(windows_error)
  145. @win_only
  146. def transcode_error(windows_error, to_enc='utf-8'):
  147. from_enc = 'utf-8' if getattr(windows_error, 'utf8', False) else library.python.strings.guess_default_encoding()
  148. if from_enc != to_enc:
  149. windows_error.strerror = library.python.strings.to_str(
  150. windows_error.strerror, to_enc=to_enc, from_enc=from_enc
  151. )
  152. setattr(windows_error, 'utf8', to_enc == 'utf-8')
  153. class Transaction(object):
  154. def __init__(self, timeout=None, description=''):
  155. self.timeout = timeout
  156. self.description = description
  157. @require_ctypes
  158. def __enter__(self):
  159. self._handle = ctypes.windll.ktmw32.CreateTransaction(None, 0, 0, 0, 0, self.timeout, self.description)
  160. if self._handle == _INVALID_HANDLE_VALUE:
  161. raise ctypes.WinError()
  162. return self._handle
  163. @require_ctypes
  164. def __exit__(self, t, v, tb):
  165. try:
  166. if not ctypes.windll.ktmw32.CommitTransaction(self._handle):
  167. raise ctypes.WinError()
  168. finally:
  169. ctypes.windll.kernel32.CloseHandle(self._handle)
  170. @win_only
  171. def file_handle(f):
  172. return msvcrt.get_osfhandle(f.fileno())
  173. # https://www.python.org/dev/peps/pep-0446/
  174. # http://mihalop.blogspot.ru/2014/05/python-subprocess-and-file-descriptors.html
  175. @require_ctypes
  176. @win_only
  177. def open_file(*args, **kwargs):
  178. f = open(*args, **kwargs)
  179. ctypes.windll.kernel32.SetHandleInformation(file_handle(f), _HANDLE_FLAG_INHERIT, 0)
  180. return f
  181. @win_only
  182. @require_ctypes
  183. def replace_file(src, dst):
  184. if not ctypes.windll.kernel32.MoveFileExW(
  185. unicode_path(src), unicode_path(dst), _MOVEFILE_REPLACE_EXISTING | _MOVEFILE_WRITE_THROUGH
  186. ):
  187. raise ctypes.WinError()
  188. @win_only
  189. @require_ctypes
  190. def replace_file_across_devices(src, dst):
  191. with Transaction(
  192. timeout=_ATOMIC_RENAME_FILE_TRANSACTION_DEFAULT_TIMEOUT,
  193. description='ya library.python.windows replace_file_across_devices',
  194. ) as transaction:
  195. if not ctypes.windll.kernel32.MoveFileTransactedW(
  196. unicode_path(src),
  197. unicode_path(dst),
  198. None,
  199. None,
  200. _MOVEFILE_REPLACE_EXISTING | _MOVEFILE_WRITE_THROUGH,
  201. transaction,
  202. ):
  203. raise ctypes.WinError()
  204. @win_only
  205. @require_ctypes
  206. def hardlink(src, lnk):
  207. if not ctypes.windll.kernel32.CreateHardLinkW(unicode_path(lnk), unicode_path(src), None):
  208. raise ctypes.WinError()
  209. # Requires SE_CREATE_SYMBOLIC_LINK_NAME privilege
  210. @win_only
  211. @win_disabled
  212. @require_ctypes
  213. def symlink_file(src, lnk):
  214. if not ctypes.windll.kernel32.CreateSymbolicLinkW(unicode_path(lnk), unicode_path(src), 0):
  215. raise ctypes.WinError()
  216. # Requires SE_CREATE_SYMBOLIC_LINK_NAME privilege
  217. @win_only
  218. @win_disabled
  219. @require_ctypes
  220. def symlink_dir(src, lnk):
  221. if not ctypes.windll.kernel32.CreateSymbolicLinkW(
  222. unicode_path(lnk), unicode_path(src), _SYMBOLIC_LINK_FLAG_DIRECTORY
  223. ):
  224. raise ctypes.WinError()
  225. @win_only
  226. @require_ctypes
  227. def lock_file(f, offset, length, raises=True):
  228. locked = ctypes.windll.kernel32.LockFile(
  229. file_handle(f), _low_dword(offset), _high_dword(offset), _low_dword(length), _high_dword(length)
  230. )
  231. if not raises:
  232. return bool(locked)
  233. if not locked:
  234. raise ctypes.WinError()
  235. @win_only
  236. @require_ctypes
  237. def unlock_file(f, offset, length, raises=True):
  238. unlocked = ctypes.windll.kernel32.UnlockFile(
  239. file_handle(f), _low_dword(offset), _high_dword(offset), _low_dword(length), _high_dword(length)
  240. )
  241. if not raises:
  242. return bool(unlocked)
  243. if not unlocked:
  244. raise ctypes.WinError()
  245. @win_only
  246. @require_ctypes
  247. def set_error_mode(mode):
  248. return ctypes.windll.kernel32.SetErrorMode(mode)
  249. @win_only
  250. def rmtree(path):
  251. def error_handler(func, handling_path, execinfo):
  252. e = execinfo[1]
  253. if e.winerror == ERRORS['PATH_NOT_FOUND']:
  254. handling_path = "\\\\?\\" + handling_path # handle path over 256 symbols
  255. if os.path.exists(path):
  256. return func(handling_path)
  257. if e.winerror == ERRORS['ACCESS_DENIED']:
  258. try:
  259. # removing of r/w directory with read-only files in it yields ACCESS_DENIED
  260. # which is not an insuperable obstacle https://bugs.python.org/issue19643
  261. os.chmod(handling_path, stat.S_IWRITE)
  262. except OSError:
  263. pass
  264. else:
  265. # propagate true last error if this attempt fails
  266. return func(handling_path)
  267. raise e
  268. shutil.rmtree(path, onerror=error_handler)
  269. # Don't display the Windows GPF dialog if the invoked program dies.
  270. # http://msdn.microsoft.com/en-us/library/windows/desktop/ms680621.aspx
  271. @win_only
  272. def disable_error_dialogs():
  273. set_error_mode(_SEM_NOGPFAULTERRORBOX | _SEM_FAILCRITICALERRORS)
  274. @win_only
  275. def default_process_creation_flags():
  276. return 0
  277. @require_ctypes
  278. def _low_dword(x):
  279. return ctypes.c_ulong(x & ((1 << 32) - 1))
  280. @require_ctypes
  281. def _high_dword(x):
  282. return ctypes.c_ulong((x >> 32) & ((1 << 32) - 1))
  283. @win_only
  284. @require_ctypes
  285. def get_current_process():
  286. handle = ctypes.windll.kernel32.GetCurrentProcess()
  287. if not handle:
  288. raise ctypes.WinError()
  289. return wintypes.HANDLE(handle)
  290. @win_only
  291. @require_ctypes
  292. def get_process_handle_count(proc_handle):
  293. assert isinstance(proc_handle, wintypes.HANDLE)
  294. GetProcessHandleCount = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HANDLE, wintypes.POINTER(wintypes.DWORD))(
  295. ("GetProcessHandleCount", ctypes.windll.kernel32)
  296. )
  297. hndcnt = wintypes.DWORD()
  298. if not GetProcessHandleCount(proc_handle, ctypes.byref(hndcnt)):
  299. raise ctypes.WinError()
  300. return hndcnt.value
  301. @win_only
  302. @require_ctypes
  303. def set_handle_information(file, inherit=None, protect_from_close=None):
  304. for flag, value in [(inherit, 1), (protect_from_close, 2)]:
  305. if flag is not None:
  306. assert isinstance(flag, bool)
  307. if not ctypes.windll.kernel32.SetHandleInformation(
  308. file_handle(file), _low_dword(value), _low_dword(int(flag))
  309. ):
  310. raise ctypes.WinError()
  311. @win_only
  312. @require_ctypes
  313. def get_windows_directory():
  314. buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
  315. size = ctypes.windll.kernel32.GetWindowsDirectoryW(buf, ctypes.wintypes.MAX_PATH)
  316. if not size:
  317. raise ctypes.WinError()
  318. if size > ctypes.wintypes.MAX_PATH - 1:
  319. raise CustomWinError(ERRORS['INSUFFICIENT_BUFFER'])
  320. return ctypes.wstring_at(buf, size)