hgext/remotefilelog/extutil.py
changeset 40495 3a333a582d7b
child 40496 60eb35b0c11c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/remotefilelog/extutil.py	Thu Sep 27 13:03:19 2018 -0400
@@ -0,0 +1,151 @@
+# extutil.py - useful utility methods for extensions
+#
+# Copyright 2016 Facebook
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import contextlib
+import errno
+import os
+import subprocess
+import time
+
+from mercurial import (
+    error,
+    lock as lockmod,
+    pycompat,
+    util,
+    vfs as vfsmod,
+)
+
+if pycompat.iswindows:
+    # no fork on Windows, but we can create a detached process
+    # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
+    # No stdlib constant exists for this value
+    DETACHED_PROCESS = 0x00000008
+    _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
+
+    def runbgcommand(script, env, shell=False, stdout=None, stderr=None):
+        '''Spawn a command without waiting for it to finish.'''
+        # we can't use close_fds *and* redirect stdin. I'm not sure that we
+        # need to because the detached process has no console connection.
+        subprocess.Popen(
+            script, shell=shell, env=env, close_fds=True,
+            creationflags=_creationflags, stdout=stdout, stderr=stderr)
+else:
+    def runbgcommand(cmd, env, shell=False, stdout=None, stderr=None):
+        '''Spawn a command without waiting for it to finish.'''
+        # double-fork to completely detach from the parent process
+        # based on http://code.activestate.com/recipes/278731
+        pid = os.fork()
+        if pid:
+            # Parent process
+            (_pid, status) = os.waitpid(pid, 0)
+            if os.WIFEXITED(status):
+                returncode = os.WEXITSTATUS(status)
+            else:
+                returncode = -os.WTERMSIG(status)
+            if returncode != 0:
+                # The child process's return code is 0 on success, an errno
+                # value on failure, or 255 if we don't have a valid errno
+                # value.
+                #
+                # (It would be slightly nicer to return the full exception info
+                # over a pipe as the subprocess module does.  For now it
+                # doesn't seem worth adding that complexity here, though.)
+                if returncode == 255:
+                    returncode = errno.EINVAL
+                raise OSError(returncode, 'error running %r: %s' %
+                              (cmd, os.strerror(returncode)))
+            return
+
+        returncode = 255
+        try:
+            # Start a new session
+            os.setsid()
+
+            stdin = open(os.devnull, 'r')
+            if stdout is None:
+                stdout = open(os.devnull, 'w')
+            if stderr is None:
+                stderr = open(os.devnull, 'w')
+
+            # connect stdin to devnull to make sure the subprocess can't
+            # muck up that stream for mercurial.
+            subprocess.Popen(
+                cmd, shell=shell, env=env, close_fds=True,
+                stdin=stdin, stdout=stdout, stderr=stderr)
+            returncode = 0
+        except EnvironmentError as ex:
+            returncode = (ex.errno & 0xff)
+            if returncode == 0:
+                # This shouldn't happen, but just in case make sure the
+                # return code is never 0 here.
+                returncode = 255
+        except Exception:
+            returncode = 255
+        finally:
+            # mission accomplished, this child needs to exit and not
+            # continue the hg process here.
+            os._exit(returncode)
+
+def runshellcommand(script, env):
+    '''
+    Run a shell command in the background.
+    This spawns the command and returns before it completes.
+
+    Prefer using runbgcommand() instead of this function.  This function should
+    be discouraged in new code.  Running commands through a subshell requires
+    you to be very careful about correctly escaping arguments, and you need to
+    make sure your command works with both Windows and Unix shells.
+    '''
+    runbgcommand(script, env=env, shell=True)
+
+@contextlib.contextmanager
+def flock(lockpath, description, timeout=-1):
+    """A flock based lock object. Currently it is always non-blocking.
+
+    Note that since it is flock based, you can accidentally take it multiple
+    times within one process and the first one to be released will release all
+    of them. So the caller needs to be careful to not create more than one
+    instance per lock.
+    """
+
+    # best effort lightweight lock
+    try:
+        import fcntl
+        fcntl.flock
+    except ImportError:
+        # fallback to Mercurial lock
+        vfs = vfsmod.vfs(os.path.dirname(lockpath))
+        with lockmod.lock(vfs, os.path.basename(lockpath), timeout=timeout):
+            yield
+        return
+    # make sure lock file exists
+    util.makedirs(os.path.dirname(lockpath))
+    with open(lockpath, 'a'):
+        pass
+    lockfd = os.open(lockpath, os.O_RDONLY, 0o664)
+    start = time.time()
+    while True:
+        try:
+            fcntl.flock(lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+            break
+        except IOError as ex:
+            if ex.errno == errno.EAGAIN:
+                if timeout != -1 and time.time() - start > timeout:
+                    raise error.LockHeld(errno.EAGAIN, lockpath, description,
+                                         '')
+                else:
+                    time.sleep(0.05)
+                    continue
+            raise
+
+    try:
+        yield
+    finally:
+        fcntl.flock(lockfd, fcntl.LOCK_UN)
+        os.close(lockfd)