locksuite.py 7.59 KB
Newer Older
Per Cederqvist's avatar
Per Cederqvist committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# Lock the test suite.
# Copyright (C) 2002  Lysator Academic Computer Association.
#
# This file is part of the LysKOM server.
# 
# LysKOM is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by 
# the Free Software Foundation; either version 1, or (at your option) 
# any later version.
# 
# LysKOM is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
# 
# You should have received a copy of the GNU General Public License
# along with LysKOM; see the file COPYING.  If not, write to
# Lysator, c/o ISY, Linkoping University, S-581 83 Linkoping, SWEDEN,
# or the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, 
# MA 02139, USA.
#
# Please mail bug reports to bug-lyskom@lysator.liu.se.

# This program is used by the test suite to ensure that only one test
# is running at a time.  It obtains two locks:
#
#   - first, a socket bound to a specific port.  This lock ensures
#     that only a single test is run on this computer.  If two tests
#     were run at the same time, they would interfere with each other,
#     since static port is used by the test suite.
#
#   - secondly, a symlink in the current working directory, that
#     points to the host and pid of this process.  This lock ensures
#     that the test suite isn't used by two processes over a shared
#     filesystem (such as NFS).
#
# This process communicates with the test suite on stdin/stdout.
# It understands a single command: "exit\n".  It can send these
# responses:
#
# "locking...\n": this is sent as soon as the process is started.
#
# "waiting: socket $USER $CWD\n": if the socket lock is already taken,
#     this message will be printed, with $USER replaced by the
#     username and $CWD with the current working directory of the
#     process that already holds the lock.  This message may be issued
#     several times, but never with the same values of both $USER and
#     $CWD.
#
# "waiting: file $LOCK $HOST:$PID\n": if the symlink lock is already
#     taken, this message will be printed, with $LOCK replaced by the
#     full path to the symlink lock file, $HOST replaced by the host
#     name and $PID by the pid of the process holding the lock.  This
#     message may be issued several times, but never with the same
#     values of bot $HOST and $PID.
#
# "failed: file $LOCK $HOST $PID\n": the symlink lock may be a stale
#     lock.  If $HOST is the local host, the program can check for
#     stale locks and break them.  However, it it is held by a remote
#     host, that isn't possible.  After waiting an hour for a lock
#     that is still held by the same remote host and pid, this program
#     will print this message and stop trying to obtain the lock.  If
#     this happens, the process should send the "exit\n" command.
#
# "locked\n": the lock has been successfully obtained.
#
# "queued: socket $USER $CWD\n": this is printed when the lock is
#     held, and another process wants it.  The lock is still held.
#
# "bye\n": this is sent as a response to the "exit\n" command.  If any
#     locks were held, they are now released.  After printing this
#     response, the process will exit.

import socket
import select
import sys
import errno
import string
import os
import select
import getpass
import time

LOCKNAME = "locksuite.lock"

__reported_queued = {}

def try_socket():
    s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        s.bind(('', 53263))
    except socket.error, e:
        if e[0] != errno.EADDRINUSE:
            raise
        return None
    s.listen(3)
    return s

def try_symlink(hostname, pid):
    """Try to lock the symlink.

    Return values:
      None: ok
      'host:pid': the lock was already held.
    """

    fn = "%s:%d" % (hostname, pid)
    while 1:
        try:
            os.symlink(fn, LOCKNAME)
            return None
        except os.error, e:
            if e.errno != errno.EEXIST:
                raise
116
        locker = None
Per Cederqvist's avatar
Per Cederqvist committed
117
        try:
118
            locker = os.readlink(LOCKNAME)
Per Cederqvist's avatar
Per Cederqvist committed
119
120
121
        except os.error, e:
            if e.errno != errno.ENOENT:
                raise
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
        if locker != None:
            if locker[:len(hostname)] != hostname:
                # Locked by a foreign host.
                return locker
            oldpid = locker[len(hostname):]

            if len(oldpid) < 2 or oldpid[0] != ":":
                # Broken lock file.  Return it anyhow.
                return locker

            try:
                os.kill(string.atoi(oldpid[1:]), 0)
                # Lock owner still living.
                return locker
            except os.error, e:
                if e.errno == errno.EPERM:
                    # Lock owner still living.
                    return locker
                elif e.errno == errno.ESRCH:
                    os.unlink(LOCKNAME)
                else:
                    raise
Per Cederqvist's avatar
Per Cederqvist committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
        time.sleep(1) # Just in case...

def myhostname():
    host = socket.gethostname()
    try:
        fqdn = socket.gethostbyaddr(host)
        return fqdn[0]
    except socket.error:
        return host

def my_id():
    return "%s %s" % (getpass.getuser(), os.getcwd())

def poll_input(s, timeout):
    global __reported_queued

    pend = [sys.stdin]
    if s != None:
        pend.append(s)
    (r, w, e) = select.select(pend, [], [], timeout)
    if sys.stdin in r:
        line = sys.stdin.readline() # Ignore the result.
        sys.exit(0)
    if s in r:
        (other, addr) = s.accept()
        other.send(my_id())
        their_id = other.recv(100)
        if not __reported_queued.has_key(their_id):
            print "queued: socket", their_id
            sys.stdout.flush()
            __reported_queued[their_id] = None
        other.close()
        

def get_socket_lock():
    reported_sockets = {}
    while 1:
        s = try_socket()
        if s != None:
            return s
        holder = get_holder()
        if holder != None and not reported_sockets.has_key(holder):
            print "waiting: socket", holder
            sys.stdout.flush()
            reported_sockets[holder] = None
        poll_input(None, 1.0)


def get_holder():
    s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        s.connect(('', 53263))
    except socket.error:
        return None
    s.send(my_id())
    their_id = s.recv(100)
    s.close()
    return their_id

def get_link_lock():
    reported_symlinks = {}
    start = time.time()
    host = myhostname()
    pid = os.getpid()
    while time.time() < start + 3600:
        lnk = try_symlink(host, pid)
        if lnk == None:
            return LOCKNAME
        if not reported_symlinks.has_key(lnk):
            print "waiting: file", os.path.join(os.getcwd(), LOCKNAME), lnk
            sys.stdout.flush()
            reported_symlinks[lnk] = None
            start = time.time()
        time.sleep(5)
    print "failed: file", os.path.join(os.getcwd(), LOCKNAME), lnk
    return None

def main():
    print "locking..."
    sys.stdout.flush()
    host = myhostname()

    s = None
    link = None
    try:
        s = get_socket_lock()
        link = get_link_lock()
        if link != None:
            print "locked"
        sys.stdout.flush()
        while 1:
            poll_input(s, 3600)
    finally:
        if s != None:
            s.close()
        if link != None:
            os.unlink(link)
        print "bye"
        sys.stdout.flush()

if __name__ == '__main__':
    main()