title: python: Remove reentrance file lock upon keyboard interrupt / Ctrl-C
body: <h2>Goal</h2>
  <p>I wrote a simple file-locking to avoid reentrance error while a script is running.</p>
  <p>Then I'd love the lock file to be automatically removed if user press Ctrl-C on both Windows and macOS.</p>
  <h2>Code</h2>
  <p>To lock a process, I use a decorator on the main function of the script.</p>
  <pre class="lang-py prettyprint-override"><code>@util.rerun_lock(name='my_program', folder=lock_file_folder)
  def main(args):
      # .... 
      # long-running tasks
      # ....
      pass
  
  </code></pre>
  <p>My reentrance class</p>
  <pre class="lang-py prettyprint-override"><code>class RerunLock:
      &quot;&quot;&quot;Lock process from reentering when seeing lock file on disk.&quot;&quot;&quot;
      def __init__(self, name, folder=None, logger=glogger):
          os.makedirs(folder, exist_ok=True)
          filename = f'lock_{name}.json' if name else 'lock_{}.json'.format(next(tempfile._get_candidate_names()))
          self.lockFile = osp.join(folder, filename) if folder else join(get_platform_tmp_dir(), filename)
          self.logger = logger
          # CAUTION:
          # - windows grpc server crashes with signals:
          #   - ValueError: signal only works in main thread of the main interpreter
          # - signals are disabled for windows
          if threading.current_thread() is threading.main_thread():
              common_sigs = [
                  signal.SIGABRT,
                  signal.SIGFPE,
                  signal.SIGILL,
                  signal.SIGINT,
                  signal.SIGSEGV,
                  signal.SIGTERM,
              ]
              plat_sigs = [
                  signal.SIGBREAK,
                  # CAUTION
                  # - CTRL_C_EVENT, CTRL_BREAK_EVENT not working on Windows
                  # signal.CTRL_C_EVENT,
                  # signal.CTRL_BREAK_EVENT,
              ] if platform.system() == 'Windows' else [
                  # CAUTION:
                  # - SIGCHLD as an alias is safe to ignore
                  # - SIGKILL must be handled by os.kill()
                  signal.SIGALRM,
                  signal.SIGBUS,
                  # signal.SIGCHLD,
                  signal.SIGCONT,
                  signal.SIGHUP,
                  # signal.SIGKILL,
                  signal.SIGPIPE,
              ]
              for sig in common_sigs + plat_sigs:
                  signal.signal(sig, self.handle_signal)
  
      def lock(self):
          if not self.is_locked():
              save_json(self.lockFile, {'pid': os.getpid()})
              return True
          else:
              self.logger.warning('Locked by pid: {}. Will stay locked until it ends.'.format(os.getpid()))
              return False
  
      def unlock(self):
          try:
              os.remove(self.lockFile)
          except FileNotFoundError:
              self.logger.warning('Already unlocked. Aborted.')
          except Exception:
              failure = traceback.format_exc()
              self.logger.error('{}\nFailed to unlock. Must delete the lock by hand: {}.'.format(failure, self.lockFile))
  
      def is_locked(self):
          return osp.isfile(self.lockFile)
  
      def handle_signal(self, sig, frame):
          msg = f'Terminated due to signal: {signal.Signals(sig).name}; Will unlock'
          self.logger.warning(msg)
          self.unlock()
          raise RuntimeError(msg)
  </code></pre>
  <p>A decorator to use for main function of a generic script</p>
  <pre class="lang-py prettyprint-override"><code>def rerun_lock(name, folder=None, logger=glogger):
      &quot;&quot;&quot;Decorator for reentrance locking on functions&quot;&quot;&quot;
      def decorator(f):
          @functools.wraps(f)
          def wrapper(*args, **kwargs):
              my_lock = None
              try:
                  my_lock = RerunLock(name, folder, logger)
                  if not my_lock.lock():
                      return 1
                  try:
                      ret = f(*args, **kwargs)
                  except KeyboardInterrupt as e:
                      my_lock.unlock()
                      raise e
                  my_lock.unlock()
              except Exception as e:
                  my_lock.unlock()
                  # leave exception to its upper handler or let the program crash
                  raise e
              return ret
          return wrapper
      return decorator
  </code></pre>
  <h2>Expected</h2>
  <p>With my setup, I expect the file-locker class can create the lock file on initial run, and then block all subsequent runs until the initial process exists. Then the lock file will be auto-removed on safe exist or upon exceptions and signals.</p>
  <p>I've removed unsupported signals in my platform-dependent signal list.</p>
  <h2>Got</h2>
  <p>On Windows, the code will leave the lock file without removing it if an exception happens or using Ctrl-C if I also use a command-line prompt based on stdin, e.g., <a href="https://github.com/prompt-toolkit/python-prompt-toolkit" rel="nofollow noreferrer">the python-prompt-toolkit library</a>.</p>
  <p>On macOS, I haven't had a lot of issues.</p>
  <h2>Question</h2>
  <p>Is this a platform-specific problem or is there a design flaw in my code?</p>