#!/bin/bash
# dsv - manage runit services on Das OS, the way systemctl does elsewhere.
#
# A service is a directory /etc/sv/<name>; it is enabled (started at boot) by
# linking it into the default runlevel, which runsvdir watches through
# /var/service. Logs come from the service's own svlogd directory or, for
# services that log to syslog, from socklog.
set -euo pipefail

sv_dir=/etc/sv
runlevel=/etc/runit/runsvdir/default
# Outside a running system (a chroot, an image) /var/service may not exist
service_dir=/var/service
[[ -d $service_dir ]] || service_dir=$runlevel
socklog=/var/log/socklog

if [[ ${LANG:-} == uk* || ${LC_ALL:-} == uk* || ${LC_MESSAGES:-} == uk* ]]; then
  uk=1
else
  uk=""
fi

# say UKRAINIAN ENGLISH: prints the message in the user's language
say() {
  if [[ -n $uk ]]; then echo "$1"; else echo "$2"; fi
}

die() {
  say "dsv: $1" "dsv: $2" >&2
  exit 1
}

usage() {
  if [[ -n $uk ]]; then
    cat <<'EOF'
Використання: dsv КОМАНДА [СЛУЖБА...]

  list                    усі служби: чи ввімкнена, стан, PID, час роботи
  status СЛУЖБА...        стан служби (sv status)
  enable СЛУЖБА...        запускати під час завантаження й запустити зараз
  disable СЛУЖБА...       не запускати під час завантаження й зупинити
  start|stop|restart СЛУЖБА...
                          запустити, зупинити, перезапустити
  logs СЛУЖБА [-f]        журнал служби (-f: стежити за новими рядками)

Служби — каталоги в /etc/sv. Без команди виконується list.
EOF
  else
    cat <<'EOF'
Usage: dsv COMMAND [SERVICE...]

  list                    all services: enabled or not, state, PID, uptime
  status SERVICE...       service state (sv status)
  enable SERVICE...       start at boot and start now
  disable SERVICE...      do not start at boot and stop now
  start|stop|restart SERVICE...
                          start, stop, restart
  logs SERVICE [-f]       the service log (-f: follow new lines)

Services are the directories in /etc/sv. Without a command, runs list.
EOF
  fi
}

# Service states live in root-only supervise directories
need_root() {
  if [[ $EUID -ne 0 ]]; then
    exec sudo "$0" "$@"
  fi
}

check_service() {
  [[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. ls $sv_dir)" "no service '$1' (see ls $sv_dir)"
}

is_enabled() {
  [[ -e $runlevel/$1 || -L $runlevel/$1 ]]
}

# Waits up to 10 s for runsvdir to pick up a new link (it rescans every 5 s)
wait_supervised() {
  local i
  for ((i = 0; i < 20; i++)); do
    [[ -e $service_dir/$1/supervise/ok ]] && return 0
    sleep 0.5
  done
  return 1
}

# row NAME ENABLED STATE PID UPTIME: one line of the list. printf pads by
# bytes, ${#s} counts characters, so Cyrillic columns stay aligned.
row() {
  local s widths=(24 9 6 8) i=0
  for s in "$1" "$2" "$3" "$4"; do
    printf '%s%*s ' "$s" $((widths[i] - ${#s})) ''
    i=$((i + 1))
  done
  printf '%s\n' "$5"
}

cmd_list() {
  local dir name enabled state pid time line
  row "$(say СЛУЖБА SERVICE)" "$(say УВІМКН. ENABLED)" "$(say СТАН STATE)" PID "$(say ПРАЦЮЄ UPTIME)"
  for dir in "$sv_dir"/*/; do
    name=$(basename "$dir")
    enabled=$(say ні no)
    state=-
    pid=-
    time=-
    if is_enabled "$name"; then
      enabled=$(say так yes)
      # "run: /var/service/sshd: (pid 812) 3600s; run: log: ..."
      line=$(sv status "$service_dir/$name" 2>/dev/null || true)
      state=${line%%:*}
      [[ $line =~ \(pid\ ([0-9]+)\) ]] && pid=${BASH_REMATCH[1]}
      [[ $line =~ \ ([0-9]+)s ]] && time=$(human_time "${BASH_REMATCH[1]}")
      [[ -n $state ]] || state='?'
    fi
    row "$name" "$enabled" "$state" "$pid" "$time"
  done
}

# human_time SECONDS: 3d4h, 2h5m, 7m, 42s
human_time() {
  local s=$1
  if ((s >= 86400)); then
    echo "$((s / 86400))d$((s % 86400 / 3600))h"
  elif ((s >= 3600)); then
    echo "$((s / 3600))h$((s % 3600 / 60))m"
  elif ((s >= 60)); then
    echo "$((s / 60))m"
  else
    echo "${s}s"
  fi
}

cmd_status() {
  local name rc=0
  for name in "$@"; do
    check_service "$name"
    if is_enabled "$name"; then
      sv status "$service_dir/$name" || rc=1
    else
      say "$name: вимкнена (dsv enable $name)" "$name: disabled (dsv enable $name)"
    fi
  done
  return $rc
}

cmd_enable() {
  local name
  for name in "$@"; do
    check_service "$name"
    if is_enabled "$name"; then
      say "$name: уже ввімкнена" "$name: already enabled"
      continue
    fi
    ln -s "$sv_dir/$name" "$runlevel/$name"
    say "$name: увімкнена" "$name: enabled"
    # runsvdir starts it within 5 s. Right after "dsv disable" its runsv is
    # still there with the service stopped, so ask for "up" as well.
    if [[ $service_dir == /var/service ]] && wait_supervised "$name"; then
      sv -v up "$service_dir/$name" || true
    fi
  done
}

cmd_disable() {
  local name
  for name in "$@"; do
    check_service "$name"
    if ! is_enabled "$name"; then
      say "$name: уже вимкнена" "$name: already disabled"
      continue
    fi
    # Stop it first: runsv only notices the removed link at runsvdir's next scan
    [[ $service_dir == /var/service ]] && sv down "$service_dir/$name" 2>/dev/null || true
    rm "$runlevel/$name"
    say "$name: вимкнена" "$name: disabled"
  done
}

cmd_control() {
  local action=$1 name rc=0
  shift
  for name in "$@"; do
    check_service "$name"
    if ! is_enabled "$name"; then
      say "$name: вимкнена, спершу: dsv enable $name" "$name: disabled, first: dsv enable $name" >&2
      rc=1
      continue
    fi
    # -v waits for the new state (up to 7 s) and prints it
    sv -v "$action" "$service_dir/$name" || rc=1
  done
  return $rc
}

# The log directory of a service whose log/run starts svlogd, if any
svlogd_dir() {
  local run=$sv_dir/$1/log/run words w
  [[ -r $run ]] || return 1
  grep -q svlogd "$run" || return 1
  read -ra words < <(grep -m1 svlogd "$run")
  for w in "${words[@]}"; do
    [[ $w == /* && -d $w ]] && echo "$w" && return 0
  done
  return 1
}

cmd_logs() {
  local name=${1:-} follow="" dir
  [[ -n $name ]] || die "вкажіть службу: dsv logs СЛУЖБА [-f]" "name a service: dsv logs SERVICE [-f]"
  [[ ${2:-} == -f ]] && follow=1
  check_service "$name"

  if dir=$(svlogd_dir "$name"); then
    if [[ -n $follow ]]; then
      exec tail -n 50 -F "$dir/current"
    fi
    cat "$dir"/@*.[su] "$dir/current" 2>/dev/null | tail -n 200
    return
  fi

  # Services without their own log write to syslog (vlogger -t <name>, or the
  # daemon itself as "<name>[pid]:" or "<name>-<helper>[pid]:", like
  # sshd-session), which socklog sorts into directories by facility.
  if ! compgen -G "$socklog/*/current" >/dev/null; then
    die "у $name немає власного журналу, а syslog не записується; увімкніть socklog: dsv enable socklog-unix nanoklogd" \
      "$name has no log of its own and syslog is not recorded; enable socklog: dsv enable socklog-unix nanoklogd"
  fi
  local pattern="[[:space:]]${name}(-[a-z]+)?(\[[0-9]+\])?:"
  if [[ -n $follow ]]; then
    tail -q -n 0 -F "$socklog"/*/current | grep --line-buffered -E "$pattern"
  else
    # A message can be in several directories (e.g. "everything" and "secure")
    cat "$socklog"/*/@*.[su] "$socklog"/*/current 2>/dev/null | grep -E "$pattern" | sort -u | tail -n 200 ||
      say "$name: у syslog немає записів (служба може писати лише в консоль runit)" \
        "$name: no syslog entries (the service may only write to the runit console)"
  fi
}

cmd=${1:-list}
[[ $# -gt 0 ]] && shift

case $cmd in
  list | ls)
    need_root list "$@"
    cmd_list
    ;;
  status)
    [[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
    need_root status "$@"
    cmd_status "$@"
    ;;
  enable | disable | start | stop | restart)
    [[ $# -gt 0 ]] || die "вкажіть службу" "name a service"
    need_root "$cmd" "$@"
    case $cmd in
      enable) cmd_enable "$@" ;;
      disable) cmd_disable "$@" ;;
      start) cmd_control up "$@" ;;
      stop) cmd_control down "$@" ;;
      restart) cmd_control restart "$@" ;;
    esac
    ;;
  logs | log)
    # socklog's logs are readable by root and the socklog group only
    [[ -r $socklog/messages/current ]] || need_root logs "$@"
    cmd_logs "$@"
    ;;
  -h | --help | help)
    usage
    ;;
  *)
    usage >&2
    exit 1
    ;;
esac
