Monday, November 01, 2010

Python msvcrt.locking trials and tribulations

It should be easy. All I wanted to do is to replace my C demos which use flock with a nice homely Python version on Windows. I wanted to stick with the standard library so I could just slot it into the existsing course (so I couldn't use the Locking module on PyPi).

A simple demo to start with:
1. Process 1: lock a region, write a record, pause
2. Process 2: attempt to lock the same region
3. Allow Process 1 to release the lock
4. Process 2 continues

Converting the code was easy (I thought), but no matter what I did I always found the region was locked for the second process - the lock was not being released.

I blame the documentation, and possibly the implementation.

What is not obvious is that the current file position must be reset to the original locking position before the lock gets released. The write (of course) advances the current file position, so by the time we do the unlock we are unlocking a region for which we don't have the lock in the first place! Wouldn't it be better if msvcrt.locking returned a lock object, saving the file position? Anyway, here is my completed demo:


"""
lock_w.py

Run two copies, each in its own terminal session.
Allow one to write a number of records, then switch
to the other showing that it blocks on the same record.
Switch back and release the lock, and show that the
blocked process proceeds.

Also run with lock_r to demonstrate interaction with
read locks.

Default filename is rlock.dat

Clive Darke QA
"""

import msvcrt
import os
import sys
import time
from datetime import datetime

REC_LIM = 20

if len(sys.argv) < 2:
pFilename = "rlock.dat"
else:
pFilename = sys.argv[1]

fh = open(pFilename, "w")

# Do only once
Record = dict()
Record['Pid'] = os.getpid()

for i in range(REC_LIM):
Record['Text'] = "Record number %02d" % i
Record['Time'] = datetime.now().strftime("%H:%M:%S")

# Get the size of the area to be locked
line = str(Record) + "\n"

# Get the current start position
start_pos = fh.tell()

print "Getting lock for",Record['Text']
msvcrt.locking(fh.fileno(), msvcrt.LK_RLCK, len(line)+1)
fh.write(line) # This advances the current position

raw_input("Written record %02d, clear the lock?" % i)

# Save the end position
end_pos = fh.tell()

# Reset the current position before releasing the lock
fh.seek(start_pos)
msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, len(line)+1)

# Go back to the end of the written record
fh.seek(end_pos)

print

fh.close()