You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
# Copyright 2011 Google Inc.
 | 
						|
#
 | 
						|
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
# you may not use this file except in compliance with the License.
 | 
						|
# You may obtain a copy of the License at
 | 
						|
#
 | 
						|
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
#
 | 
						|
# Unless required by applicable law or agreed to in writing, software
 | 
						|
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
# See the License for the specific language governing permissions and
 | 
						|
# limitations under the License.
 | 
						|
 | 
						|
"""Locked file interface that should work on Unix and Windows pythons.
 | 
						|
 | 
						|
This module first tries to use fcntl locking to ensure serialized access
 | 
						|
to a file, then falls back on a lock file if that is unavialable.
 | 
						|
 | 
						|
Usage:
 | 
						|
    f = LockedFile('filename', 'r+b', 'rb')
 | 
						|
    f.open_and_lock()
 | 
						|
    if f.is_locked():
 | 
						|
      print 'Acquired filename with r+b mode'
 | 
						|
      f.file_handle().write('locked data')
 | 
						|
    else:
 | 
						|
      print 'Aquired filename with rb mode'
 | 
						|
    f.unlock_and_close()
 | 
						|
"""
 | 
						|
 | 
						|
__author__ = 'cache@google.com (David T McWherter)'
 | 
						|
 | 
						|
import errno
 | 
						|
import logging
 | 
						|
import os
 | 
						|
import time
 | 
						|
 | 
						|
from . import util
 | 
						|
 | 
						|
logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
class CredentialsFileSymbolicLinkError(Exception):
 | 
						|
  """Credentials files must not be symbolic links."""
 | 
						|
 | 
						|
 | 
						|
class AlreadyLockedException(Exception):
 | 
						|
  """Trying to lock a file that has already been locked by the LockedFile."""
 | 
						|
  pass
 | 
						|
 | 
						|
 | 
						|
def validate_file(filename):
 | 
						|
  if os.path.islink(filename):
 | 
						|
    raise CredentialsFileSymbolicLinkError(
 | 
						|
        'File: %s is a symbolic link.' % filename)
 | 
						|
 | 
						|
class _Opener(object):
 | 
						|
  """Base class for different locking primitives."""
 | 
						|
 | 
						|
  def __init__(self, filename, mode, fallback_mode):
 | 
						|
    """Create an Opener.
 | 
						|
 | 
						|
    Args:
 | 
						|
      filename: string, The pathname of the file.
 | 
						|
      mode: string, The preferred mode to access the file with.
 | 
						|
      fallback_mode: string, The mode to use if locking fails.
 | 
						|
    """
 | 
						|
    self._locked = False
 | 
						|
    self._filename = filename
 | 
						|
    self._mode = mode
 | 
						|
    self._fallback_mode = fallback_mode
 | 
						|
    self._fh = None
 | 
						|
 | 
						|
  def is_locked(self):
 | 
						|
    """Was the file locked."""
 | 
						|
    return self._locked
 | 
						|
 | 
						|
  def file_handle(self):
 | 
						|
    """The file handle to the file. Valid only after opened."""
 | 
						|
    return self._fh
 | 
						|
 | 
						|
  def filename(self):
 | 
						|
    """The filename that is being locked."""
 | 
						|
    return self._filename
 | 
						|
 | 
						|
  def open_and_lock(self, timeout, delay):
 | 
						|
    """Open the file and lock it.
 | 
						|
 | 
						|
    Args:
 | 
						|
      timeout: float, How long to try to lock for.
 | 
						|
      delay: float, How long to wait between retries.
 | 
						|
    """
 | 
						|
    pass
 | 
						|
 | 
						|
  def unlock_and_close(self):
 | 
						|
    """Unlock and close the file."""
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class _PosixOpener(_Opener):
 | 
						|
  """Lock files using Posix advisory lock files."""
 | 
						|
 | 
						|
  def open_and_lock(self, timeout, delay):
 | 
						|
    """Open the file and lock it.
 | 
						|
 | 
						|
    Tries to create a .lock file next to the file we're trying to open.
 | 
						|
 | 
						|
    Args:
 | 
						|
      timeout: float, How long to try to lock for.
 | 
						|
      delay: float, How long to wait between retries.
 | 
						|
 | 
						|
    Raises:
 | 
						|
      AlreadyLockedException: if the lock is already acquired.
 | 
						|
      IOError: if the open fails.
 | 
						|
      CredentialsFileSymbolicLinkError if the file is a symbolic link.
 | 
						|
    """
 | 
						|
    if self._locked:
 | 
						|
      raise AlreadyLockedException('File %s is already locked' %
 | 
						|
                                   self._filename)
 | 
						|
    self._locked = False
 | 
						|
 | 
						|
    validate_file(self._filename)
 | 
						|
    try:
 | 
						|
      self._fh = open(self._filename, self._mode)
 | 
						|
    except IOError, e:
 | 
						|
      # If we can't access with _mode, try _fallback_mode and don't lock.
 | 
						|
      if e.errno == errno.EACCES:
 | 
						|
        self._fh = open(self._filename, self._fallback_mode)
 | 
						|
        return
 | 
						|
 | 
						|
    lock_filename = self._posix_lockfile(self._filename)
 | 
						|
    start_time = time.time()
 | 
						|
    while True:
 | 
						|
      try:
 | 
						|
        self._lock_fd = os.open(lock_filename,
 | 
						|
                                os.O_CREAT|os.O_EXCL|os.O_RDWR)
 | 
						|
        self._locked = True
 | 
						|
        break
 | 
						|
 | 
						|
      except OSError, e:
 | 
						|
        if e.errno != errno.EEXIST:
 | 
						|
          raise
 | 
						|
        if (time.time() - start_time) >= timeout:
 | 
						|
          logger.warn('Could not acquire lock %s in %s seconds' % (
 | 
						|
              lock_filename, timeout))
 | 
						|
          # Close the file and open in fallback_mode.
 | 
						|
          if self._fh:
 | 
						|
            self._fh.close()
 | 
						|
          self._fh = open(self._filename, self._fallback_mode)
 | 
						|
          return
 | 
						|
        time.sleep(delay)
 | 
						|
 | 
						|
  def unlock_and_close(self):
 | 
						|
    """Unlock a file by removing the .lock file, and close the handle."""
 | 
						|
    if self._locked:
 | 
						|
      lock_filename = self._posix_lockfile(self._filename)
 | 
						|
      os.close(self._lock_fd)
 | 
						|
      os.unlink(lock_filename)
 | 
						|
      self._locked = False
 | 
						|
      self._lock_fd = None
 | 
						|
    if self._fh:
 | 
						|
      self._fh.close()
 | 
						|
 | 
						|
  def _posix_lockfile(self, filename):
 | 
						|
    """The name of the lock file to use for posix locking."""
 | 
						|
    return '%s.lock' % filename
 | 
						|
 | 
						|
 | 
						|
try:
 | 
						|
  import fcntl
 | 
						|
 | 
						|
  class _FcntlOpener(_Opener):
 | 
						|
    """Open, lock, and unlock a file using fcntl.lockf."""
 | 
						|
 | 
						|
    def open_and_lock(self, timeout, delay):
 | 
						|
      """Open the file and lock it.
 | 
						|
 | 
						|
      Args:
 | 
						|
        timeout: float, How long to try to lock for.
 | 
						|
        delay: float, How long to wait between retries
 | 
						|
 | 
						|
      Raises:
 | 
						|
        AlreadyLockedException: if the lock is already acquired.
 | 
						|
        IOError: if the open fails.
 | 
						|
        CredentialsFileSymbolicLinkError if the file is a symbolic link.
 | 
						|
      """
 | 
						|
      if self._locked:
 | 
						|
        raise AlreadyLockedException('File %s is already locked' %
 | 
						|
                                     self._filename)
 | 
						|
      start_time = time.time()
 | 
						|
 | 
						|
      validate_file(self._filename)
 | 
						|
      try:
 | 
						|
        self._fh = open(self._filename, self._mode)
 | 
						|
      except IOError, e:
 | 
						|
        # If we can't access with _mode, try _fallback_mode and don't lock.
 | 
						|
        if e.errno == errno.EACCES:
 | 
						|
          self._fh = open(self._filename, self._fallback_mode)
 | 
						|
          return
 | 
						|
 | 
						|
      # We opened in _mode, try to lock the file.
 | 
						|
      while True:
 | 
						|
        try:
 | 
						|
          fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
 | 
						|
          self._locked = True
 | 
						|
          return
 | 
						|
        except IOError, e:
 | 
						|
          # If not retrying, then just pass on the error.
 | 
						|
          if timeout == 0:
 | 
						|
            raise e
 | 
						|
          if e.errno != errno.EACCES:
 | 
						|
            raise e
 | 
						|
          # We could not acquire the lock. Try again.
 | 
						|
          if (time.time() - start_time) >= timeout:
 | 
						|
            logger.warn('Could not lock %s in %s seconds' % (
 | 
						|
                self._filename, timeout))
 | 
						|
            if self._fh:
 | 
						|
              self._fh.close()
 | 
						|
            self._fh = open(self._filename, self._fallback_mode)
 | 
						|
            return
 | 
						|
          time.sleep(delay)
 | 
						|
 | 
						|
    def unlock_and_close(self):
 | 
						|
      """Close and unlock the file using the fcntl.lockf primitive."""
 | 
						|
      if self._locked:
 | 
						|
        fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
 | 
						|
      self._locked = False
 | 
						|
      if self._fh:
 | 
						|
        self._fh.close()
 | 
						|
except ImportError:
 | 
						|
  _FcntlOpener = None
 | 
						|
 | 
						|
 | 
						|
try:
 | 
						|
  import pywintypes
 | 
						|
  import win32con
 | 
						|
  import win32file
 | 
						|
 | 
						|
  class _Win32Opener(_Opener):
 | 
						|
    """Open, lock, and unlock a file using windows primitives."""
 | 
						|
 | 
						|
    # Error #33:
 | 
						|
    #  'The process cannot access the file because another process'
 | 
						|
    FILE_IN_USE_ERROR = 33
 | 
						|
 | 
						|
    # Error #158:
 | 
						|
    #  'The segment is already unlocked.'
 | 
						|
    FILE_ALREADY_UNLOCKED_ERROR = 158
 | 
						|
 | 
						|
    def open_and_lock(self, timeout, delay):
 | 
						|
      """Open the file and lock it.
 | 
						|
 | 
						|
      Args:
 | 
						|
        timeout: float, How long to try to lock for.
 | 
						|
        delay: float, How long to wait between retries
 | 
						|
 | 
						|
      Raises:
 | 
						|
        AlreadyLockedException: if the lock is already acquired.
 | 
						|
        IOError: if the open fails.
 | 
						|
        CredentialsFileSymbolicLinkError if the file is a symbolic link.
 | 
						|
      """
 | 
						|
      if self._locked:
 | 
						|
        raise AlreadyLockedException('File %s is already locked' %
 | 
						|
                                     self._filename)
 | 
						|
      start_time = time.time()
 | 
						|
 | 
						|
      validate_file(self._filename)
 | 
						|
      try:
 | 
						|
        self._fh = open(self._filename, self._mode)
 | 
						|
      except IOError, e:
 | 
						|
        # If we can't access with _mode, try _fallback_mode and don't lock.
 | 
						|
        if e.errno == errno.EACCES:
 | 
						|
          self._fh = open(self._filename, self._fallback_mode)
 | 
						|
          return
 | 
						|
 | 
						|
      # We opened in _mode, try to lock the file.
 | 
						|
      while True:
 | 
						|
        try:
 | 
						|
          hfile = win32file._get_osfhandle(self._fh.fileno())
 | 
						|
          win32file.LockFileEx(
 | 
						|
              hfile,
 | 
						|
              (win32con.LOCKFILE_FAIL_IMMEDIATELY|
 | 
						|
               win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
 | 
						|
              pywintypes.OVERLAPPED())
 | 
						|
          self._locked = True
 | 
						|
          return
 | 
						|
        except pywintypes.error, e:
 | 
						|
          if timeout == 0:
 | 
						|
            raise e
 | 
						|
 | 
						|
          # If the error is not that the file is already in use, raise.
 | 
						|
          if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
 | 
						|
            raise
 | 
						|
 | 
						|
          # We could not acquire the lock. Try again.
 | 
						|
          if (time.time() - start_time) >= timeout:
 | 
						|
            logger.warn('Could not lock %s in %s seconds' % (
 | 
						|
                self._filename, timeout))
 | 
						|
            if self._fh:
 | 
						|
              self._fh.close()
 | 
						|
            self._fh = open(self._filename, self._fallback_mode)
 | 
						|
            return
 | 
						|
          time.sleep(delay)
 | 
						|
 | 
						|
    def unlock_and_close(self):
 | 
						|
      """Close and unlock the file using the win32 primitive."""
 | 
						|
      if self._locked:
 | 
						|
        try:
 | 
						|
          hfile = win32file._get_osfhandle(self._fh.fileno())
 | 
						|
          win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
 | 
						|
        except pywintypes.error, e:
 | 
						|
          if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
 | 
						|
            raise
 | 
						|
      self._locked = False
 | 
						|
      if self._fh:
 | 
						|
        self._fh.close()
 | 
						|
except ImportError:
 | 
						|
  _Win32Opener = None
 | 
						|
 | 
						|
 | 
						|
class LockedFile(object):
 | 
						|
  """Represent a file that has exclusive access."""
 | 
						|
 | 
						|
  @util.positional(4)
 | 
						|
  def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
 | 
						|
    """Construct a LockedFile.
 | 
						|
 | 
						|
    Args:
 | 
						|
      filename: string, The path of the file to open.
 | 
						|
      mode: string, The mode to try to open the file with.
 | 
						|
      fallback_mode: string, The mode to use if locking fails.
 | 
						|
      use_native_locking: bool, Whether or not fcntl/win32 locking is used.
 | 
						|
    """
 | 
						|
    opener = None
 | 
						|
    if not opener and use_native_locking:
 | 
						|
      if _Win32Opener:
 | 
						|
        opener = _Win32Opener(filename, mode, fallback_mode)
 | 
						|
      if _FcntlOpener:
 | 
						|
        opener = _FcntlOpener(filename, mode, fallback_mode)
 | 
						|
 | 
						|
    if not opener:
 | 
						|
      opener = _PosixOpener(filename, mode, fallback_mode)
 | 
						|
 | 
						|
    self._opener = opener
 | 
						|
 | 
						|
  def filename(self):
 | 
						|
    """Return the filename we were constructed with."""
 | 
						|
    return self._opener._filename
 | 
						|
 | 
						|
  def file_handle(self):
 | 
						|
    """Return the file_handle to the opened file."""
 | 
						|
    return self._opener.file_handle()
 | 
						|
 | 
						|
  def is_locked(self):
 | 
						|
    """Return whether we successfully locked the file."""
 | 
						|
    return self._opener.is_locked()
 | 
						|
 | 
						|
  def open_and_lock(self, timeout=0, delay=0.05):
 | 
						|
    """Open the file, trying to lock it.
 | 
						|
 | 
						|
    Args:
 | 
						|
      timeout: float, The number of seconds to try to acquire the lock.
 | 
						|
      delay: float, The number of seconds to wait between retry attempts.
 | 
						|
 | 
						|
    Raises:
 | 
						|
      AlreadyLockedException: if the lock is already acquired.
 | 
						|
      IOError: if the open fails.
 | 
						|
    """
 | 
						|
    self._opener.open_and_lock(timeout, delay)
 | 
						|
 | 
						|
  def unlock_and_close(self):
 | 
						|
    """Unlock and close a file."""
 | 
						|
    self._opener.unlock_and_close()
 |