text: """
  runit service module
  (http://smarden.org/runit)
  
  This module is compatible with the :mod:`service <salt.states.service>` states,
  so it can be used to maintain services using the ``provider`` argument:
  
  .. code-block:: yaml
  
      myservice:
        service:
          - running
          - provider: runit
  
  Provides virtual `service` module on systems using runit as init.
  
  
  Service management rules (`sv` command):
  
      service $n is ENABLED   if file SERVICE_DIR/$n/run exists
      service $n is AVAILABLE if ENABLED or if file AVAIL_SVR_DIR/$n/run exists
      service $n is DISABLED  if AVAILABLE but not ENABLED
  
      SERVICE_DIR/$n is normally a symlink to a AVAIL_SVR_DIR/$n folder
  
  
  Service auto-start/stop mechanism:
  
      `sv` (auto)starts/stops service as soon as SERVICE_DIR/<service> is
      created/deleted, both on service creation or a boot time.
  
      autostart feature is disabled if file SERVICE_DIR/<n>/down exists. This
      does not affect the current's service status (if already running) nor
      manual service management.
  
  
  Service's alias:
  
      Service `sva` is an alias of service `svc` when `AVAIL_SVR_DIR/sva` symlinks
      to folder `AVAIL_SVR_DIR/svc`. `svc` can't be enabled if it is already
      enabled through an alias already enabled, since `sv` files are stored in
      folder `SERVICE_DIR/svc/`.
  
      XBPS package management uses a service's alias to provides service
      alternative(s), such as chrony and openntpd both aliased to ntpd.
  """
  
  import glob
  import logging
  import os
  import time
  
  import salt.utils.files
  import salt.utils.path
  from salt.exceptions import CommandExecutionError
  
  log = logging.getLogger(__name__)
  
  
  # Function alias to not shadow built-ins.
  __func_alias__ = {"reload_": "reload"}
  
  # which dir sv works with
  VALID_SERVICE_DIRS = [
      "/service",
      "/var/service",
      "/etc/service",
  ]
  SERVICE_DIR = None
  for service_dir in VALID_SERVICE_DIRS:
      if os.path.exists(service_dir):
          SERVICE_DIR = service_dir
          break
  
  # available service directory(ies)
  AVAIL_SVR_DIRS = []
  
  # Define the module's virtual name
  __virtualname__ = "runit"
  __virtual_aliases__ = ("runit",)
  
  
  def __virtual__():
      """
      Virtual service only on systems using runit as init process (PID 1).
      Otherwise, use this module with the provider mechanism.
      """
      if __grains__.get("init") == "runit":
          if __grains__["os"] == "Void":
              add_svc_avail_path("/etc/sv")
          global __virtualname__
          __virtualname__ = "service"
          return __virtualname__
      if salt.utils.path.which("sv"):
          return __virtualname__
      return (False, "Runit not available.  Please install sv")
  
  
  def _service_path(name):
      """
      Return SERVICE_DIR+name if possible
  
      name
          the service's name to work on
      """
      if not SERVICE_DIR:
          raise CommandExecutionError("Could not find service directory.")
      return os.path.join(SERVICE_DIR, name)
  
  
  # -- states.service  compatible args
  def start(name):
      """
      Start service
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.start <service name>
      """
      cmd = "sv start {}".format(_service_path(name))
      return not __salt__["cmd.retcode"](cmd)
  
  
  # -- states.service compatible args
  def stop(name):
      """
      Stop service
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.stop <service name>
      """
      cmd = "sv stop {}".format(_service_path(name))
      return not __salt__["cmd.retcode"](cmd)
  
  
  # -- states.service compatible
  def reload_(name):
      """
      Reload service
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.reload <service name>
      """
      cmd = "sv reload {}".format(_service_path(name))
      return not __salt__["cmd.retcode"](cmd)
  
  
  # -- states.service compatible
  def restart(name):
      """
      Restart service
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.restart <service name>
      """
      cmd = "sv restart {}".format(_service_path(name))
      return not __salt__["cmd.retcode"](cmd)
  
  
  # -- states.service compatible
  def full_restart(name):
      """
      Calls runit.restart()
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.full_restart <service name>
      """
      restart(name)
  
  
  # -- states.service compatible
  def status(name, sig=None):
      """
      Return ``True`` if service is running
  
      name
          the service's name
  
      sig
          signature to identify with ps
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.status <service name>
      """
      if sig:
          # usual way to do by others (debian_service, netbsdservice).
          # XXX probably does not work here (check 'runsv sshd' instead of 'sshd' ?)
          return bool(__salt__["status.pid"](sig))
  
      svc_path = _service_path(name)
      if not os.path.exists(svc_path):
          # service does not exist
          return False
  
      # sv return code is not relevant to get a service status.
      # Check its output instead.
      cmd = "sv status {}".format(svc_path)
      try:
          out = __salt__["cmd.run_stdout"](cmd)
          return out.startswith("run: ")
      except Exception:  # pylint: disable=broad-except
          # sv (as a command) returned an error
          return False
  
  
  def _is_svc(svc_path):
      """
      Return ``True`` if directory <svc_path> is really a service:
      file <svc_path>/run exists and is executable
  
      svc_path
          the (absolute) directory to check for compatibility
      """
      run_file = os.path.join(svc_path, "run")
      if (
          os.path.exists(svc_path)
          and os.path.exists(run_file)
          and os.access(run_file, os.X_OK)
      ):
          return True
      return False
  
  
  def status_autostart(name):
      """
      Return ``True`` if service <name> is autostarted by sv
      (file $service_folder/down does not exist)
      NB: return ``False`` if the service is not enabled.
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.status_autostart <service name>
      """
      return not os.path.exists(os.path.join(_service_path(name), "down"))
  
  
  def get_svc_broken_path(name="*"):
      """
      Return list of broken path(s) in SERVICE_DIR that match ``name``
  
      A path is broken if it is a broken symlink or can not be a runit service
  
      name
          a glob for service name. default is '*'
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.get_svc_broken_path <service name>
      """
      if not SERVICE_DIR:
          raise CommandExecutionError("Could not find service directory.")
  
      ret = set()
  
      for el in glob.glob(os.path.join(SERVICE_DIR, name)):
          if not _is_svc(el):
              ret.add(el)
      return sorted(ret)
  
  
  def get_svc_avail_path():
      """
      Return list of paths that may contain available services
      """
      return AVAIL_SVR_DIRS
  
  
  def add_svc_avail_path(path):
      """
      Add a path that may contain available services.
      Return ``True`` if added (or already present), ``False`` on error.
  
      path
          directory to add to AVAIL_SVR_DIRS
      """
      if os.path.exists(path):
          if path not in AVAIL_SVR_DIRS:
              AVAIL_SVR_DIRS.append(path)
          return True
      return False
  
  
  def _get_svc_path(name="*", status=None):
      """
      Return a list of paths to services with ``name`` that have the specified ``status``
  
      name
          a glob for service name. default is '*'
  
      status
          None       : all services (no filter, default choice)
          'DISABLED' : available service(s) that is not enabled
          'ENABLED'  : enabled service (whether started on boot or not)
      """
  
      # This is the core routine to work with services, called by many
      # other functions of this module.
      #
      # The name of a service is the "apparent" folder's name that contains its
      # "run" script. If its "folder" is a symlink, the service is an "alias" of
      # the targeted service.
  
      if not SERVICE_DIR:
          raise CommandExecutionError("Could not find service directory.")
  
      # path list of enabled services as /AVAIL_SVR_DIRS/$service,
      # taking care of any service aliases (do not use os.path.realpath()).
      ena = set()
      for el in glob.glob(os.path.join(SERVICE_DIR, name)):
          if _is_svc(el):
              ena.add(os.readlink(el))
              log.trace("found enabled service path: %s", el)
  
      if status == "ENABLED":
          return sorted(ena)
  
      # path list of available services as /AVAIL_SVR_DIRS/$service
      ava = set()
      for d in AVAIL_SVR_DIRS:
          for el in glob.glob(os.path.join(d, name)):
              if _is_svc(el):
                  ava.add(el)
                  log.trace("found available service path: %s", el)
  
      if status == "DISABLED":
          # service available but not enabled
          ret = ava.difference(ena)
      else:
          # default: return available services
          ret = ava.union(ena)
  
      return sorted(ret)
  
  
  def _get_svc_list(name="*", status=None):
      """
      Return list of services that have the specified service ``status``
  
      name
          a glob for service name. default is '*'
  
      status
          None       : all services (no filter, default choice)
          'DISABLED' : available service that is not enabled
          'ENABLED'  : enabled service (whether started on boot or not)
      """
      return sorted([os.path.basename(el) for el in _get_svc_path(name, status)])
  
  
  def get_svc_alias():
      """
      Returns the list of service's name that are aliased and their alias path(s)
      """
  
      ret = {}
      for d in AVAIL_SVR_DIRS:
          for el in glob.glob(os.path.join(d, "*")):
              if not os.path.islink(el):
                  continue
              psvc = os.readlink(el)
              if not os.path.isabs(psvc):
                  psvc = os.path.join(d, psvc)
              nsvc = os.path.basename(psvc)
              if nsvc not in ret:
                  ret[nsvc] = []
              ret[nsvc].append(el)
      return ret
  
  
  def available(name):
      """
      Returns ``True`` if the specified service is available, otherwise returns
      ``False``.
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.available <service name>
      """
      return name in _get_svc_list(name)
  
  
  def missing(name):
      """
      The inverse of runit.available.
      Returns ``True`` if the specified service is not available, otherwise returns
      ``False``.
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.missing <service name>
      """
      return name not in _get_svc_list(name)
  
  
  def get_all():
      """
      Return a list of all available services
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' runit.get_all
      """
      return _get_svc_list()
  
  
  def get_enabled():
      """
      Return a list of all enabled services
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.get_enabled
      """
      return _get_svc_list(status="ENABLED")
  
  
  def get_disabled():
      """
      Return a list of all disabled services
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.get_disabled
      """
      return _get_svc_list(status="DISABLED")
  
  
  def enabled(name):
      """
      Return ``True`` if the named service is enabled, ``False`` otherwise
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.enabled <service name>
      """
      # exhaustive check instead of (only) os.path.exists(_service_path(name))
      return name in _get_svc_list(name, "ENABLED")
  
  
  def disabled(name):
      """
      Return ``True`` if the named service is disabled, ``False``  otherwise
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.disabled <service name>
      """
      # return True for a non-existent service
      return name not in _get_svc_list(name, "ENABLED")
  
  
  def show(name):
      """
      Show properties of one or more units/jobs or the manager
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.show <service name>
      """
      ret = {}
      ret["enabled"] = False
      ret["disabled"] = True
      ret["running"] = False
      ret["service_path"] = None
      ret["autostart"] = False
      ret["command_path"] = None
  
      ret["available"] = available(name)
      if not ret["available"]:
          return ret
  
      ret["enabled"] = enabled(name)
      ret["disabled"] = not ret["enabled"]
      ret["running"] = status(name)
      ret["autostart"] = status_autostart(name)
      ret["service_path"] = _get_svc_path(name)[0]
      if ret["service_path"]:
          ret["command_path"] = os.path.join(ret["service_path"], "run")
  
      # XXX provide info about alias ?
  
      return ret
  
  
  def enable(name, start=False, **kwargs):
      """
      Start service ``name`` at boot.
      Returns ``True`` if operation is successful
  
      name
          the service's name
  
      start : False
          If ``True``, start the service once enabled.
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.enable <name> [start=True]
      """
  
      # non-existent service
      if not available(name):
          return False
  
      # if service is aliased, refuse to enable it
      alias = get_svc_alias()
      if name in alias:
          log.error("This service is aliased, enable its alias instead")
          return False
  
      # down_file: file that disables sv autostart
      svc_realpath = _get_svc_path(name)[0]
      down_file = os.path.join(svc_realpath, "down")
  
      # if service already enabled, remove down_file to
      # let service starts on boot (as requested)
      if enabled(name):
          if os.path.exists(down_file):
              try:
                  os.unlink(down_file)
              except OSError:
                  log.error("Unable to remove file %s", down_file)
                  return False
          return True
  
      # let's enable the service
  
      if not start:
          # create a temp 'down' file BEFORE enabling service.
          # will prevent sv from starting this service automatically.
          log.trace("need a temporary file %s", down_file)
          if not os.path.exists(down_file):
              try:
                  # pylint: disable=resource-leakage
                  salt.utils.files.fopen(down_file, "w").close()
                  # pylint: enable=resource-leakage
              except OSError:
                  log.error("Unable to create file %s", down_file)
                  return False
  
      # enable the service
      try:
          os.symlink(svc_realpath, _service_path(name))
  
      except OSError:
          # (attempt to) remove temp down_file anyway
          log.error("Unable to create symlink %s", down_file)
          if not start:
              os.unlink(down_file)
          return False
  
      # ensure sv is aware of this new service before continuing.
      # if not, down_file might be removed too quickly,
      # before 'sv' have time to take care about it.
      # Documentation indicates that a change is handled within 5 seconds.
      cmd = "sv status {}".format(_service_path(name))
      retcode_sv = 1
      count_sv = 0
      while retcode_sv != 0 and count_sv < 10:
          time.sleep(0.5)
          count_sv += 1
          call = __salt__["cmd.run_all"](cmd)
          retcode_sv = call["retcode"]
  
      # remove the temp down_file in any case.
      if (not start) and os.path.exists(down_file):
          try:
              os.unlink(down_file)
          except OSError:
              log.error("Unable to remove temp file %s", down_file)
              retcode_sv = 1
  
      # if an error happened, revert our changes
      if retcode_sv != 0:
          os.unlink(os.path.join([_service_path(name), name]))
          return False
      return True
  
  
  def disable(name, stop=False, **kwargs):
      """
      Don't start service ``name`` at boot
      Returns ``True`` if operation is successful
  
      name
          the service's name
  
      stop
          if True, also stops the service
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.disable <name> [stop=True]
      """
  
      # non-existent as registrered service
      if not enabled(name):
          return False
  
      # down_file: file that prevent sv autostart
      svc_realpath = _get_svc_path(name)[0]
      down_file = os.path.join(svc_realpath, "down")
  
      if stop:
          stop(name)
  
      if not os.path.exists(down_file):
          try:
              # pylint: disable=resource-leakage
              salt.utils.files.fopen(down_file, "w").close()
              # pylint: enable=resource-leakage
          except OSError:
              log.error("Unable to create file %s", down_file)
              return False
  
      return True
  
  
  def remove(name):
      """
      Remove the service <name> from system.
      Returns ``True`` if operation is successful.
      The service will be also stopped.
  
      name
          the service's name
  
      CLI Example:
  
      .. code-block:: bash
  
          salt '*' service.remove <name>
      """
  
      if not enabled(name):
          return False
  
      svc_path = _service_path(name)
      if not os.path.islink(svc_path):
          log.error("%s is not a symlink: not removed", svc_path)
          return False
  
      if not stop(name):
          log.error("Failed to stop service %s", name)
          return False
      try:
          os.remove(svc_path)
      except OSError:
          log.error("Unable to remove symlink %s", svc_path)
          return False
      return True
  
  
  # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
  