Last night I finished refactoring the lock code and checked it in. I'm kinda surprised that its still working. Here's the story.
The original code pretty much didn't track the locks it handed out. It put them in the list of locks held in the volume's
DosList entry, and removed them when the locks were freed, but it never looked at them. It shouldn't have been doing that anyway - that lock list is only for when the volume is taken offline (eg the disk was ejected) while locks are still open. In that case, any outstanding locks are added to the
DosList. Later, if the volume comes online again, the handler detaches the locks and takes control of them. This is the mechanism by which the Amiga in days of old could request that you "please insert volume Work: in any drive".
This list was being used incorrectly, so it had to change - I have a feeling its responsible for a bug on native where you insert a new disk and both the old and the new volume appears. A real list of all locks is needed by the handler, for a number of things:
- If an exclusive lock on a file is requested, the handler needs to know if the file is already locked.
- Certain file attributes (like its length or location on disk) are the same no matter how many locks are open on the file. If one of those attributes change, they all need to be updated. This doesn't seem like it should be a problem, as there's should only be one (exclusive) lock on a file for these attributes to change, however traditionally "read-only" locks (as created by
FINDINPUT) can actually be written to, and renaming a file (which due to long filenames may require its directory entries to be moved) should be able to happen even when a file is open.
- Obviously, locks need to be available so they can be attached to the
I did consider just having a straight list of locks, but this meant a search through all the locks every time some shared attribute needed to change. So instead locks now have two parts: a shared part which contains the file's name and protection bits, the location of its first data cluster, and the location of its directory entry, and a per-instance part which has the current seek position and the IO handle. Put another way, the shared part has stuff about the file itself, while the per-instance part has stuff about access to the file.
The shared part (that I call "global" locks) are held in a linked list attached to the superblock structure. Each global lock has a list of per-instance (just called filelocks) locks attached to it. Each one of those has a pointer to its global lock. The system just passes filelocks around as normal (and out to DOS and back), but goes to the global lock when it needs some file data.
Now that this is in place, all the above things can be implemented. Exclusive locks are already done - when an attempt is made to obtain a lock, the global lock list is checked. If the caller requested exclusive and a global lock was found, the operation fails. Renaming now should be trivial - if the new name won't fit into the existing directory entries, then the existing ones are blanked, new ones created, and the entry attributes in the global lock are updated and seen by all filelocks).
DosList stuff is on hold. I'll get there, its just a few steps down on the my list. The next thing I want to do after renaming is done is to implement notifications. File change notifications are done by filename, not lock, and there can be more than one per file, so I need a place to store them even if the file isn't open. This is now trivial - the notifications get stored in the global lock (which will be created if a notification is requested and the file isn't open).
So now I've written this, and it still makes sense. Weird, it felt so hard at the time.