Files
Das-OS/srcpkgs/das-helpers/files/dsv
T
dasandClaude Opus 5.5 acb7532a4f Stage 7: command-line helpers dsv, duser, dfw, dsys (0.5.0)
- 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>
2026-10-20 06:58:25 +03:00

430 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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