- das-helpers replaces the dsv package and adds shared functions in /usr/lib/das/helpers.sh (bilingual messages, sudo re-exec, prompts). - dsv new creates a runit service (command, user, directory, environment, svlogd log) and checks it stays up; dsv rm deletes one not from a package. - duser: list, add (installer groups, --admin, key, password), del, passwd, lock/unlock (key logins too), key add|list|rm, groups +/-. - dfw: allow/deny/status/reload for the tcp_ports and udp_ports sets of the inet das table, kept in /etc/nftables.d/dfw.nft and checked with nft -c. - dsys: info, hostname, timezone, locale, keymap. - xtools-minimal on both images. - nftables.conf no longer suggests restarting the nftables service: its finish script flushes the whole ruleset, fail2ban's and Docker's included. - das-server-install 0.2.0 mentions the helpers when it finishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
430 lines
15 KiB
Bash
Executable File
430 lines
15 KiB
Bash
Executable File
#!/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
|
||
|
||
# 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 <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
|
||
}
|
||
|
||
# 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" <<EOF
|
||
#!/bin/sh
|
||
# Created by dsv new. After editing: dsv restart $name
|
||
# Environment variables: one file per variable in $sv_dir/$name/env
|
||
exec 2>&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" <<EOF
|
||
#!/bin/sh
|
||
# Log of $name, read with: dsv logs $name
|
||
mkdir -p /var/log/$name
|
||
exec svlogd -tt /var/log/$name
|
||
EOF
|
||
chmod 755 "$tmp/log/run"
|
||
fi
|
||
mv "$tmp" "$sv_dir/$name"
|
||
say "$name: створена в $sv_dir/$name" "$name: created in $sv_dir/$name"
|
||
|
||
if [[ -z $enable ]] && interactive; then
|
||
confirm "Увімкнути й запустити зараз?" "Enable and start it now?" && enable=1
|
||
fi
|
||
if [[ -n $enable ]]; then
|
||
cmd_enable "$name"
|
||
[[ $service_dir == /var/service ]] && check_running "$name"
|
||
else
|
||
say "Запустити: dsv enable $name" "To start it: dsv enable $name"
|
||
fi
|
||
}
|
||
|
||
cmd_rm() {
|
||
local name=${1:-} i
|
||
[[ -n $name ]] || die "вкажіть службу" "name a service"
|
||
check_service "$name"
|
||
# Services from packages come back with the next update; disable them instead
|
||
# (xbps-query -o succeeds even when no package owns the file)
|
||
if [[ -n $(xbps-query -o "$sv_dir/$name/run" 2>/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
|