text: # -*- coding: utf-8 -*-
  # vi:si:et:sw=4:sts=4:ts=4
  
  from __future__ import unicode_literals
  
  import hashlib
  import os
  
  from django.utils.encoding import force_bytes, force_text
  from django.utils.functional import cached_property
  
  from cronman.spawner import CronSpawner
  from cronman.worker.process_manager import ProcessManager
  
  
  class BaseCronWorkerFile(object):
      """Stats file kept by Cron Worker - base class"""
  
      EXTENSION = None  # must be set in subclass
  
      def __init__(self, data_dir, name):
          self.name = name
          self.data_dir = data_dir
          self.path = os.path.join(
              self.data_dir, "{}{}".format(self.name, self.EXTENSION)
          )
  
      @classmethod
      def get_file_name(cls, name, args=None, kwargs=None, random=False):
          """Generates file name from CronJob's name, args and kwargs"""
          parts = [name]
          if args or kwargs:
              params_bytes_repr = force_bytes(repr((args, kwargs)))
              parts.append(
                  hashlib.md5(params_bytes_repr).hexdigest()[:10]  # nosec
              )
          if random:
              parts.append(hashlib.md5(os.urandom(16)).hexdigest()[:10])  # nosec
          return "_".join(parts)
  
      @classmethod
      def all(cls, data_dir, name=None, args=None, kwargs=None):
          """Iterates over files in given directory"""
          for file_name in os.listdir(data_dir):
              base_name, ext = os.path.splitext(file_name)
              if ext == cls.EXTENSION:
                  if name:  # Filter files by specific job:
                      base_name_begin = cls.get_file_name(name, args, kwargs)
                      if base_name.startswith(base_name_begin):
                          yield cls(data_dir, base_name)
                  else:  # No filters:
                      yield cls(data_dir, base_name)
  
      def write_content(self, content):
          """Populates this file with new content"""
          with open(self.path, "wb") as file_:
              file_.write(force_bytes(content))
              file_.flush()
              # NOTE:
              # "flush() does not necessarily write the file’s data to disk.
              # Use flush() followed by os.fsync() to ensure this behavior."
              # (from Python 2.7 docs)
              os.fsync(file_.fileno())
  
      def read_content(self):
          """Reads contents of this file"""
          try:
              with open(self.path, "rb") as file_:
                  content = force_text(file_.read())
          except IOError:  # File deleted already
              content = None
          return content
  
      def delete(self):
          """Deletes this file"""
          try:
              os.unlink(self.path)
          except OSError:
              pass  # File deleted already
  
      def exists(self):
          """Checks if this file exists"""
          return os.path.exists(self.path)
  
  
  class CronWorkerPIDFile(BaseCronWorkerFile):
      """PID file (and lock) for Cron Worker"""
  
      EXTENSION = ".pid"
  
      @classmethod
      def by_pid(cls, data_dir, pid):
          """Retrieves PID file from given directory by PID value"""
          for candidate_pid_file in cls.all(data_dir):
              if candidate_pid_file.pid == pid:
                  pid_file = candidate_pid_file
                  break
          else:
              pid_file = None
          return pid_file
  
      def create(self):
          """Creates the PID file"""
          pid = os.getpid()
          self.write_content(pid)
          self.__dict__["pid"] = pid  # Bypass `pid` property
  
      @cached_property
      def pid(self):
          """PID extracted from this file"""
          try:
              pid = int(self.read_content())
          except (ValueError, TypeError):  # PID file deleted / truncated
              pid = None
          return pid
  
      @cached_property
      def process(self):
          """Process Manager instance for this PID file"""
          return ProcessManager(self.pid)  # ProcessManager can handle `None`
  
      def exists_with_alive_process(self):
          """Checks if PID file exists and corresponding process is alive.
          Removes file if process is dead.
          """
          if self.exists():
              if not self.pid or self.process.alive() is False:
                  # PID file deleted/truncated or process is dead:
                  self.delete()
                  result = False
              else:  # Process is alive or we don't have access:
                  result = True
          else:
              result = False
          return result
  
      @cached_property
      def job_spec_file(self):
          """JobSpec file associated with this file"""
          return CronWorkerJobSpecFile(self.data_dir, self.name)
  
  
  class CronWorkerJobSpecFile(BaseCronWorkerFile):
      """JobSpec file for Cron Worker's running jobs with resume feature"""
  
      EXTENSION = ".jobspec"
  
      @classmethod
      def by_pid(cls, data_dir, pid):
          """Retrieves JobSpec file from given directory by PID value"""
          job_spec_file = None
          pid_file = CronWorkerPIDFile.by_pid(data_dir, pid)
          if pid_file:
              job_spec_file = cls(data_dir, pid_file.name)
              if not job_spec_file.exists():
                  job_spec_file = None
          return job_spec_file
  
      def create(self, job_spec):
          """Creates the JobSpec file"""
          self.write_content(job_spec)
          self.__dict__["job_spec"] = job_spec  # Bypass `job_spec` property
  
      def resume(self):
          """Starts a worker process for job spec stored in this file"""
          job_spec = self.job_spec
          self.delete()  # file is deleted before spawning new worker
          if job_spec:
              pid = self.cron_spawner.start_worker(job_spec)
          else:
              pid = None
          return pid
  
      @cached_property
      def job_spec(self):
          """JobSpec extracted from this file"""
          return self.read_content() or None
  
      @cached_property
      def pid_file(self):
          """PID file associated with this file"""
          return CronWorkerPIDFile(self.data_dir, self.name)
  
      @cached_property
      def cron_spawner(self):
          """Cron Spawner instance"""
          return CronSpawner(
              data_dir=self.data_dir, extra_env={"CRON_PROCESS_RESUMED": "1"}
          )
  