devserver: enhance static subdirectory locking

Shift to using a dedicated lock file in each static subdirectory as an
indicator for exclusive access. This frees us from the implicit
semantics by which a lock on each subdirectory can be obtained at most
once, which is necessary in cases where we revisit a preexisting
directory and stage additional artifacts in it.

- The previous locking behavior is retained by default; a client must
  use an optional argument to AcquireLock() to indicate that the locked
  subdirectory may preexist.

- Uses lockfile.FileLock for the actual locking. This is deemed more
  straightforward and flexible than interprocess locking: we need not
  map resources (directories) to locks, and our implementation is not
  limited to a single multithreaded process.

- ReleaseLock() does not remove the locked subdirectory, unless
  explicitly instructed to. Added code for graceful (non-destructive)
  release in successful branches.

- Test coverage of the new locking semantics.

BUG=None
TEST=devserver prevents concurrent access to the same staging location;
unit tests pass.

Change-Id: Icb8a20d475251b114ba632a040a7815eca395912
Reviewed-on: https://gerrit.chromium.org/gerrit/33139
Reviewed-by: Chris Sosa <sosa@chromium.org>
Commit-Ready: Gilad Arnold <garnold@chromium.org>
Tested-by: Gilad Arnold <garnold@chromium.org>
diff --git a/devserver_util_unittest.py b/devserver_util_unittest.py
index aac0f81..200972e 100755
--- a/devserver_util_unittest.py
+++ b/devserver_util_unittest.py
@@ -181,16 +181,36 @@
             self._static_dir, os.path.join(self._static_dir, os.pardir)))
 
   def testAcquireReleaseLocks(self):
-    # Successful lock and unlock.
+    # Successful lock and unlock, removing the newly created directory.
     lock_file = devserver_util.AcquireLock(self._static_dir, 'test-lock')
     self.assertTrue(os.path.exists(lock_file))
-    devserver_util.ReleaseLock(self._static_dir, 'test-lock')
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock',
+                               destroy=True)
     self.assertFalse(os.path.exists(lock_file))
 
-    # Attempt to lock an existing directory.
+    # Attempt to freshly create and lock an existing directory.
+    devserver_util.AcquireLock(self._static_dir, 'test-lock')
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock')
+    self.assertRaises(devserver_util.DevServerUtilError,
+                      devserver_util.AcquireLock, self._static_dir, 'test-lock')
+    devserver_util.AcquireLock(self._static_dir, 'test-lock', create_once=False)
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock',
+                               destroy=True)
+
+    # Sucessfully re-lock a pre-existing directory.
+    devserver_util.AcquireLock(self._static_dir, 'test-lock')
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock')
+    devserver_util.AcquireLock(self._static_dir, 'test-lock',
+                               create_once=False)
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock',
+                               destroy=True)
+
+    # Attempt to lock an already locked directory.
     devserver_util.AcquireLock(self._static_dir, 'test-lock')
     self.assertRaises(devserver_util.DevServerUtilError,
                       devserver_util.AcquireLock, self._static_dir, 'test-lock')
+    devserver_util.ReleaseLock(self._static_dir, 'test-lock',
+                               destroy=True)
 
   def testFindMatchingBoards(self):
     for key in TEST_LAYOUT: