1##############################################################################
2#
3# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
4# All Rights Reserved.
5#
6# This software is subject to the provisions of the Zope Public License,
7# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11# FOR A PARTICULAR PURPOSE
12#
13##############################################################################
14
15import os
16import errno
17import logging
18logger = logging.getLogger("zc.lockfile")
19
20__metaclass__ = type
21
22class LockError(Exception):
23    """Couldn't get a lock
24    """
25
26try:
27    import fcntl
28except ImportError:
29    try:
30        import msvcrt
31    except ImportError:
32        def _lock_file(file):
33            raise TypeError('No file-locking support on this platform')
34        def _unlock_file(file):
35            raise TypeError('No file-locking support on this platform')
36
37    else:
38        # Windows
39        def _lock_file(file):
40            # Lock just the first byte
41            try:
42                msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
43            except IOError:
44                raise LockError("Couldn't lock %r" % file.name)
45
46        def _unlock_file(file):
47            try:
48                file.seek(0)
49                msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
50            except IOError:
51                raise LockError("Couldn't unlock %r" % file.name)
52
53else:
54    # Unix
55    _flags = fcntl.LOCK_EX | fcntl.LOCK_NB
56
57    def _lock_file(file):
58        try:
59            fcntl.flock(file.fileno(), _flags)
60        except IOError:
61            raise LockError("Couldn't lock %r" % file.name)
62
63    def _unlock_file(file):
64        fcntl.flock(file.fileno(), fcntl.LOCK_UN)
65
66class LazyHostName:
67    """Avoid importing socket and calling gethostname() unnecessarily"""
68    def __str__(self):
69        import socket
70        return socket.gethostname()
71
72
73class SimpleLockFile:
74
75    _fp = None
76
77    def __init__(self, path):
78        self._path = path
79        try:
80            # Try to open for writing without truncation:
81            fp = open(path, 'r+')
82        except IOError:
83            # If the file doesn't exist, we'll get an IO error, try a+
84            # Note that there may be a race here. Multiple processes
85            # could fail on the r+ open and open the file a+, but only
86            # one will get the the lock and write a pid.
87            fp = open(path, 'a+')
88
89        try:
90            _lock_file(fp)
91            self._fp = fp
92        except:
93            fp.close()
94            raise
95
96        # Lock acquired
97        self._on_lock()
98        fp.flush()
99
100    def close(self):
101        if self._fp is not None:
102            _unlock_file(self._fp)
103            self._fp.close()
104            self._fp = None
105
106    def _on_lock(self):
107        """
108        Allow subclasses to supply behavior to occur following
109        lock acquisition.
110        """
111
112
113class LockFile(SimpleLockFile):
114
115    def __init__(self, path, content_template='{pid}'):
116        self._content_template = content_template
117        super(LockFile, self).__init__(path)
118
119    def _on_lock(self):
120        content = self._content_template.format(
121            pid=os.getpid(),
122            hostname=LazyHostName(),
123        )
124        self._fp.write(" %s\n" % content)
125        self._fp.truncate()
126