# Lock the test suite. # Copyright (C) 2002-2005 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 report bugs at http://bugzilla.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 a 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 # status messages: # # "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 both $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, if 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 $TIMESTAMP\n": the lock has been successfully obtained. The # timestamp is on the "YYYY-MM-DD HH:MM:SS.ssssss" format and can # be used for diagnostic purposes. # # "queued: socket $USER $CWD\n": this is printed when the lock is # held, and another process wants it. The lock is still held. # # "bye $TIMESTAMP\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. The timestamp is # on the same format as in the "locked" message. If a lock was, # the timestamp refers to a time immediately before the lock was # released. import socket import select import sys import errno import string import os import select import getpass import time LOCKNAME = "locksuite.lock" __reported_queued = {} def now(): t = time.time() s = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)) return s + ("%06.6f" % (t - int(t)))[1:] 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 locker = None try: locker = os.readlink(LOCKNAME) except os.error, e: if e.errno != errno.ENOENT: raise 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 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", now() sys.stdout.flush() while 1: poll_input(s, 3600) finally: when = now() if s != None: s.close() if link != None: os.unlink(link) print "bye", now() sys.stdout.flush() if __name__ == '__main__': main()