#!/bin/bash # dsv - manage runit services on Das OS, the way systemctl does elsewhere. # # A service is a directory /etc/sv/; 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 # shellcheck source=helpers.sh . "${DAS_HELPERS:-/usr/lib/das/helpers.sh}" 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 usage() { if [[ -n $uk ]]; then cat <<'EOF' Використання: dsv КОМАНДА [СЛУЖБА...] list усі служби: чи ввімкнена, стан, PID, час роботи status СЛУЖБА... стан служби (sv status) enable СЛУЖБА... запускати під час завантаження й запустити зараз disable СЛУЖБА... не запускати під час завантаження й зупинити start|stop|restart СЛУЖБА... запустити, зупинити, перезапустити logs СЛУЖБА [-f] журнал служби (-f: стежити за новими рядками) new [СЛУЖБА] [ПАРАМЕТРИ] створити службу для своєї програми; чого не вказано, те запитає: --cmd КОМАНДА що запускати (програма не має йти у фон) --user КОРИСТУВАЧ від чийого імені --dir КАТАЛОГ робочий каталог --env ІМ'Я=ЗНАЧЕННЯ змінна середовища, можна кілька разів --no-log без власного журналу в /var/log/СЛУЖБА --enable одразу ввімкнути й запустити rm СЛУЖБА вимкнути й видалити службу, створену через new Служби — каталоги в /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) new [SERVICE] [OPTIONS] create a service for your own program; asks for what is not given: --cmd COMMAND what to run (the program must stay in the foreground) --user USER who runs it --dir DIR working directory --env NAME=VALUE an environment variable, may be repeated --no-log no log of its own in /var/log/SERVICE --enable enable and start it right away rm SERVICE disable and delete a service made with new Services are the directories in /etc/sv. Without a command, runs list. EOF fi } check_service() { [[ -d $sv_dir/$1 ]] || die "немає служби '$1' (див. dsv list)" "no service '$1' (see dsv list)" } 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 row() { pad "$1" 24 pad "$2" 9 pad "$3" 6 pad "$4" 8 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 , or the # daemon itself as "[pid]:" or "-[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 } # check_running NAME: after a start, tells whether the service stays up or # keeps restarting (runsv restarts a program that exits, once a second) check_running() { local line up='^run:[^;]*\) ([0-9]+)s' sleep 3 line=$(sv status "$service_dir/$1" 2>/dev/null || true) if [[ $line =~ $up ]] && ((BASH_REMATCH[1] >= 2)); then say "$1: працює" "$1: running" else warn "$1 не працює стабільно (${line%%;*}); див. dsv logs $1" \ "$1 does not stay up (${line%%;*}); see dsv logs $1" fi } cmd_new() { local name="" run_cmd="" user="" dir="" log=1 enable="" envs=() e home bin while (($#)); do case $1 in --cmd) run_cmd=${2:?}; shift ;; --user) user=${2:?}; shift ;; --dir) dir=${2:?}; shift ;; --env) envs+=("${2:?}"); shift ;; --no-log) log="" ;; --enable) enable=1 ;; -*) die "невідомий параметр $1 (dsv help)" "unknown option $1 (dsv help)" ;; *) [[ -z $name ]] || die "зайвий аргумент $1" "extra argument $1"; name=$1 ;; esac shift done if [[ -z $name ]]; then interactive || die "вкажіть назву служби" "name the service" ask name "Назва служби" "Service name" fi [[ $name =~ ^[A-Za-z0-9][A-Za-z0-9_.@-]*$ ]] || die "назва '$name': лише латинські літери, цифри, '.', '_', '-', '@'" \ "name '$name': only Latin letters, digits, '.', '_', '-', '@'" [[ ! -e $sv_dir/$name ]] || die "служба $name уже є" "service $name already exists" if [[ -z $run_cmd ]]; then interactive || die "вкажіть команду: --cmd" "give the command: --cmd" say "Команда запуску. Програма має працювати на передньому плані (без -d/--daemon)." \ "The command to run. The program must stay in the foreground (no -d/--daemon)." ask run_cmd "Команда" "Command" [[ -n $run_cmd ]] || die "команду не вказано" "no command given" fi read -r bin _ <<<"$run_cmd" if [[ $bin == */* ]]; then [[ -x $bin ]] || die "$bin не існує або не виконуваний" "$bin does not exist or is not executable" elif ! command -v "$bin" >/dev/null; then die "команду $bin не знайдено; вкажіть повний шлях" "command $bin not found; give the full path" fi if [[ -z $user ]] && interactive; then ask user "Від імені користувача" "Run as user" "${SUDO_USER:-root}" fi user=${user:-root} id -u "$user" >/dev/null 2>&1 || die "немає користувача $user" "no user $user" home=$(getent passwd "$user" | cut -d: -f6) if [[ -z $dir ]] && interactive; then ask dir "Робочий каталог" "Working directory" "$home" fi dir=${dir:-$home} [[ -d $dir ]] || die "немає каталогу $dir" "no directory $dir" if ((${#envs[@]} == 0)) && interactive; then ask e "Змінні середовища ІМ'Я=ЗНАЧЕННЯ через пробіл (Enter — без них)" \ "Environment variables NAME=VALUE separated by spaces (Enter for none)" read -ra envs <<<"$e" fi for e in "${envs[@]}"; do [[ $e =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || die "'$e' не має вигляду ІМ'Я=ЗНАЧЕННЯ" "'$e' is not NAME=VALUE" done # Build it aside, then move it in place in one step local tmp=$sv_dir/.$name.new rm -rf "$tmp" mkdir -p "$tmp/env" # chpst does not set these for the new user printf '%s\n' "$home" >"$tmp/env/HOME" printf '%s\n' "$user" >"$tmp/env/USER" for e in "${envs[@]}"; do printf '%s\n' "${e#*=}" >"$tmp/env/${e%%=*}" done cat >"$tmp/run" <&1 cd '$dir' || exit 1 exec chpst -u $user -e $sv_dir/$name/env $run_cmd EOF chmod 755 "$tmp/run" if [[ -n $log ]]; then mkdir -p "$tmp/log" "/var/log/$name" cat >"$tmp/log/run" </dev/null) ]]; then die "$name встановлена пакетом, її можна лише вимкнути: dsv disable $name" \ "$name comes from a package, it can only be disabled: dsv disable $name" fi if interactive && ! confirm "Видалити службу $name?" "Delete service $name?"; then return 1 fi if is_enabled "$name"; then cmd_disable "$name" # runsvdir stops its runsv at the next scan; wait for it before deleting for ((i = 0; i < 20; i++)); do pgrep -x -f "runsv $name" >/dev/null || break sleep 0.5 done fi rm -rf "${sv_dir:?}/$name" say "$name: видалена" "$name: deleted" [[ -d /var/log/$name ]] && say "Журнал залишився в /var/log/$name" "The log is kept in /var/log/$name" return 0 } 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 "$@" ;; new | create) need_root new "$@" cmd_new "$@" ;; rm | remove | delete) need_root rm "$@" cmd_rm "$@" ;; -h | --help | help) usage ;; *) usage >&2 exit 1 ;; esac