devserver: Make symlink creation atomic.

Without this patch, the symlink creation is performed in two steps:
if the symlink already exists, it is first removed and then it
re-created. When two or more threads are creating the same symlink
in this wat, a race condition could make a thread attempt to create
a symlink that already exists because another thread created it
between the two steps. This situation is faced on the referenced bug.

On the other hand, a two-step symlink creation when the symlink
already exists leaves a small window of time where the symlink to
be replaced doesn't exists. This could make another thread trying
to read the symlink (either the previous or the new one) fail.

This patch fixes both problems by creating the symlink atomically.
This is done in a standard way, creating it somewhere else on the
same file system and moving it to its final destination.

BUG=chromium:270434
TEST=common_util unittest the symlink creation.

Change-Id: I38f24adcc5bf2f6d289105697de3981ecd51ad38
Reviewed-on: https://chromium-review.googlesource.com/167971
Reviewed-by: Alex Deymo <deymo@chromium.org>
Tested-by: Alex Deymo <deymo@chromium.org>
Commit-Queue: Alex Deymo <deymo@chromium.org>
diff --git a/common_util.py b/common_util.py
index 1cf72b1..db88772 100644
--- a/common_util.py
+++ b/common_util.py
@@ -12,6 +12,7 @@
 import hashlib
 import os
 import shutil
+import tempfile
 import threading
 import subprocess
 
@@ -263,6 +264,33 @@
   shutil.copy(source, dest)
 
 
+def SymlinkFile(target, link):
+  """Atomically creates or replaces the symlink |link| pointing to |target|.
+
+  If the specified |link| file already exists it is replaced with the new link
+  atomically.
+  """
+  if not os.path.exists(target):
+    return
+  _Log('Creating symlink: %s --> %s', link, target)
+
+  # Use the created link_base file to prevent other calls to SymlinkFile() to
+  # pick the same link_base temp file, thanks to mkstemp().
+  with tempfile.NamedTemporaryFile(prefix=os.path.basename(link)) as link_fd:
+    link_base = link_fd.name
+
+    # Use the unique link_base filename to create a symlink, but on the same
+    # directory as the required |link| to ensure the created symlink is in the
+    # same file system as |link|.
+    link_name = os.path.join(os.path.dirname(link),
+                             os.path.basename(link_base) + "-link")
+
+    # Create the symlink and then rename it to the final position. This ensures
+    # the symlink creation is atomic.
+    os.symlink(target, link_name)
+    os.rename(link_name, link)
+
+
 class LockDict(object):
   """A dictionary of locks.