runbg regenerate.
- make use of journald - avoid command once invoke being triggered twice (or even more).
This commit is contained in:
332
bin/runbg
332
bin/runbg
@@ -6,49 +6,47 @@
|
|||||||
PATH=$(zsh -c -i 'echo $PATH')
|
PATH=$(zsh -c -i 'echo $PATH')
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
# start-bg.sh - robustly start a command in the background and redirect stdout+stderr to a log
|
|
||||||
# Generated by GitHub Copilot.
|
# Generated by GitHub Copilot.
|
||||||
|
# start-bg-journal.sh - start a command as a detached daemon and send stdout/stderr to systemd-journald
|
||||||
|
#
|
||||||
# Features:
|
# Features:
|
||||||
# - Uses setsid (preferred) to detach from the controlling terminal.
|
# - Sends stdout+stderr to journald using systemd-cat when available.
|
||||||
# - Falls back to nohup if setsid is unavailable.
|
# - Falls back to writing into a logfile if systemd-cat isn't present or if --log is passed.
|
||||||
# - Runs the command with stdin closed, stdout+stderr redirected to a logfile.
|
# - Uses setsid to detach, closes inherited file descriptors (best-effort),
|
||||||
# - Sets a safe umask and changes to / to avoid blocking filesystems.
|
# redirects stdin to /dev/null, and execs the target command.
|
||||||
# - Writes a pidfile next to the log for easy management.
|
# - Supports PID file, umask, and a syslog identifier (tag) and priority for journald.
|
||||||
# - Uses positional args ($@), script PID ($$) and reports child PID ($!).
|
# - Uses positional args ($@) for the command; uses $$ in auto-generated names and prints the child PID ($!).
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# start-bg.sh [ -l LOGFILE | -d LOGDIR ] [ -m METHOD ] -- command [args...]
|
# start-bg-journal.sh [ -t TAG ] [ -r PRIORITY ] [ -l LOGFILE | -d LOGDIR ] [ -p PIDFILE ] [ -u UMASK ] -- command [args...]
|
||||||
#
|
|
||||||
# Options:
|
|
||||||
# -l, --log LOGFILE Path to log file (if omitted an auto name is used)
|
|
||||||
# -d, --dir LOGDIR Directory for autogenerated logs (default: ./logs)
|
|
||||||
# -m, --method METHOD Background method: setsid (default) | nohup
|
|
||||||
# -h, --help Show this help and exit
|
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./start-bg.sh -- sleep 60
|
# ./start-bg-journal.sh -t myapp -- /usr/bin/myapp --config /etc/myapp.conf
|
||||||
# ./start-bg.sh -d /var/log/myapp -- /usr/bin/myapp --config /etc/myapp.conf
|
# ./start-bg-journal.sh -l ./my.log -- /usr/bin/myapp arg1 arg2
|
||||||
# ./start-bg.sh -l ./my.log -- /usr/bin/myapp arg1 arg2
|
# ./start-bg-journal.sh -p /run/myapp.pid -t myapp -r info -- /usr/bin/myapp
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
progname=$(basename "$0")
|
progname=$(basename "$0")
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: $progname [ -l LOGFILE | -d LOGDIR ] [ -m METHOD ] -- command [args...]
|
Usage: $progname [ -t TAG ] [ -r PRIORITY ] [ -l LOGFILE | -d LOGDIR ] [ -p PIDFILE ] [ -u UMASK ] -- command [args...]
|
||||||
|
|
||||||
Start "command [args...]" detached from the terminal and redirect stdout+stderr to a log.
|
Start command [args...] as a background daemon, sending stdout+stderr to systemd-journald (via systemd-cat)
|
||||||
|
when available. If systemd-cat is not available or if -l/--log is provided, logs are written to a logfile.
|
||||||
|
|
||||||
METHOD:
|
Options:
|
||||||
setsid - use setsid to start a session and detach (preferred)
|
-t, --tag TAG Tag / SYSLOG_IDENTIFIER used by systemd-cat (default: basename(command))
|
||||||
nohup - use nohup (fallback/compat)
|
-r, --priority PRIO Journal priority (emerg, alert, crit, err, warning, notice, info, debug)
|
||||||
|
-l, --log LOGFILE Write stdout+stderr to LOGFILE instead of journald
|
||||||
|
-d, --dir LOGDIR Directory for autogenerated log files (default: /tmp/$USER/runner)
|
||||||
|
-p, --pidfile PATH Write daemon PID to PATH
|
||||||
|
-u, --umask UMASK Set umask for the daemon (default: 0022)
|
||||||
|
-h, --help Show this help and exit
|
||||||
|
|
||||||
When no -l is provided, a log file is auto-generated:
|
When no -l is provided and journald is available, output goes to journald.
|
||||||
|
When -l is not provided and journald is not available, a logfile is auto-generated:
|
||||||
<LOGDIR>/<command>.<YYYYMMDD-HHMMSS>.pid<$$>.log
|
<LOGDIR>/<command>.<YYYYMMDD-HHMMSS>.pid<$$>.log
|
||||||
|
|
||||||
A pidfile is written as <logfile>.pid containing the child PID.
|
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -56,20 +54,32 @@ EOF
|
|||||||
# Defaults
|
# Defaults
|
||||||
logdir="/tmp/$USER/runner"
|
logdir="/tmp/$USER/runner"
|
||||||
logfile=""
|
logfile=""
|
||||||
method="setsid"
|
pidfile=""
|
||||||
|
umask_val="0022"
|
||||||
|
tag=""
|
||||||
|
priority=""
|
||||||
|
|
||||||
# Parse options until the `--` separator
|
# Parse options up to --
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
-t|--tag)
|
||||||
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
|
tag="$2"; shift 2 ;;
|
||||||
|
-r|--priority)
|
||||||
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
|
priority="$2"; shift 2 ;;
|
||||||
-l|--log)
|
-l|--log)
|
||||||
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
logfile="$2"; shift 2 ;;
|
logfile="$2"; shift 2 ;;
|
||||||
-d|--dir)
|
-d|--dir)
|
||||||
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
logdir="$2"; shift 2 ;;
|
logdir="$2"; shift 2 ;;
|
||||||
-m|--method)
|
-p|--pidfile)
|
||||||
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
method="$2"; shift 2 ;;
|
pidfile="$2"; shift 2 ;;
|
||||||
|
-u|--umask)
|
||||||
|
if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi
|
||||||
|
umask_val="$2"; shift 2 ;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
usage ;;
|
usage ;;
|
||||||
--)
|
--)
|
||||||
@@ -81,130 +91,202 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Remaining args are the command to run
|
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
echo "Error: no command specified."
|
echo "Error: no command specified."
|
||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Preserve the command and args
|
# Build command array from remaining args.
|
||||||
cmd=( "$@" )
|
cmd=( "$@" )
|
||||||
cmd_basename=$(basename "${cmd[0]}")
|
cmd_basename=$(basename "${cmd[0]}")
|
||||||
timestamp=$(date +%Y%m%d-%H%M%S)
|
timestamp=$(date +%Y%m%d-%H%M%S)
|
||||||
|
|
||||||
mkdir -p -- "${logdir}"
|
# If tag not set, default to command basename
|
||||||
|
if [[ -z "$tag" ]]; then
|
||||||
|
tag="${cmd_basename}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect systemd-cat
|
||||||
|
systemd_cat_path=""
|
||||||
|
if command -v systemd-cat >/dev/null 2>&1; then
|
||||||
|
systemd_cat_path=$(command -v systemd-cat)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If user provided logfile explicitly, force file mode
|
||||||
|
use_journal=false
|
||||||
|
if [[ -n "${logfile}" ]]; then
|
||||||
|
use_journal=false
|
||||||
|
else
|
||||||
|
if [[ -n "${systemd_cat_path}" ]]; then
|
||||||
|
use_journal=true
|
||||||
|
else
|
||||||
|
use_journal=false
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare logfile if needed
|
||||||
|
mkdir -p -- "${logdir}"
|
||||||
|
if ! $use_journal; then
|
||||||
if [[ -z "${logfile}" ]]; then
|
if [[ -z "${logfile}" ]]; then
|
||||||
logfile="${logdir}/${cmd_basename}.${timestamp}.pid$$.log"
|
logfile="${logdir}/${cmd_basename}.${timestamp}.pid$$.log"
|
||||||
fi
|
fi
|
||||||
|
# Ensure file exists and is writable
|
||||||
|
: > "${logfile}" || { echo "Cannot write to log file: ${logfile}"; exit 1; }
|
||||||
|
fi
|
||||||
|
|
||||||
pidfile="${logfile}.pid"
|
# Helper: quote command for printing
|
||||||
|
quoted_cmd() {
|
||||||
|
local i out=""
|
||||||
|
for i in "$@"; do
|
||||||
|
printf -v i "%q" "$i"
|
||||||
|
out="${out} ${i}"
|
||||||
|
done
|
||||||
|
printf '%s' "${out# }"
|
||||||
|
}
|
||||||
|
|
||||||
# Validate method
|
# Build the inner script to run under setsid. We inject expanded variables from outer shell.
|
||||||
case "${method}" in
|
if $use_journal; then
|
||||||
setsid|nohup) ;;
|
# Compose systemd-cat args (escape tag and priority safely)
|
||||||
|
# Note: we rely on simple token expansion; tag/priority are validated minimally below.
|
||||||
|
journal_args=()
|
||||||
|
journal_args+=( "-t" )
|
||||||
|
journal_args+=( "$tag" )
|
||||||
|
if [[ -n "$priority" ]]; then
|
||||||
|
journal_args+=( "-p" )
|
||||||
|
journal_args+=( "$priority" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate priority loosely (allow common names or numeric)
|
||||||
|
case "$priority" in
|
||||||
|
""|emerg|alert|crit|err|warning|notice|info|debug|0|1|2|3|4|5|6|7) ;;
|
||||||
*)
|
*)
|
||||||
echo "Invalid method: ${method}. Allowed: setsid|nohup"
|
echo "Warning: unknown priority '$priority' — journald may reject it." >&2 ;;
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Prepare environment for child
|
# Prepare a single string of quoted journal args for insertion into the -c script
|
||||||
# We construct a small wrapper for safe daemonization:
|
# We need to protect values with single quotes if they contain special chars.
|
||||||
start_with_setsid() {
|
journal_args_escaped=""
|
||||||
# Prefer existing setsid binary (coreutils/util-linux)
|
for a in "${journal_args[@]}"; do
|
||||||
if ! command -v setsid >/dev/null 2>&1; then
|
# replace every single quote with '\'"\''
|
||||||
return 1
|
a_escaped=${a//\'/\'"\'"\'}
|
||||||
fi
|
journal_args_escaped="$journal_args_escaped '$a_escaped'"
|
||||||
|
done
|
||||||
|
|
||||||
# Use setsid to start the process in a new session. We:
|
inner_script=$(cat <<'EOF'
|
||||||
# - set a conservative umask
|
umask __UMASK__
|
||||||
# - change to / to avoid keeping directories busy
|
|
||||||
# - close stdin and redirect stdout/stderr to logfile
|
|
||||||
#
|
|
||||||
# We run setsid directly with the command array so arguments are preserved.
|
|
||||||
#
|
|
||||||
# Note: redirecting >"$logfile" 2>&1 < /dev/null outside ensures the setsid child
|
|
||||||
# inherits the redirections.
|
|
||||||
(
|
|
||||||
umask 022
|
|
||||||
cd / || true
|
cd / || true
|
||||||
# exec will replace the subshell with the command
|
|
||||||
exec "${cmd[@]}"
|
|
||||||
) >"${logfile}" 2>&1 < /dev/null &
|
|
||||||
child_pid=$!
|
|
||||||
|
|
||||||
# Detach the job from this shell's job table if possible
|
# Close fds (best-effort)
|
||||||
disown "$child_pid" 2>/dev/null || true
|
if [ -d /proc/self/fd ]; then
|
||||||
|
for fdpath in /proc/self/fd/*; do
|
||||||
# setsid the already-forked child to start a new session. Some systems allow:
|
fdnum=${fdpath##*/}
|
||||||
# setsid -w <pid>
|
case "$fdnum" in
|
||||||
# but that's nonportable; instead we try to re-exec the command under setsid.
|
0|1|2) continue ;;
|
||||||
# If `setsid` can't be applied to the backgrounded PID, we respawn via setsid:
|
esac
|
||||||
if setsid true >/dev/null 2>&1; then
|
eval "exec ${fdnum}>&-" 2>/dev/null || true
|
||||||
# Respawn under setsid to ensure proper session leader if available.
|
done
|
||||||
# Kill the previous child (it is still running) and restart under setsid.
|
|
||||||
# To avoid races: only do this if cmd is still running and the pid we started is a shell wrapper.
|
|
||||||
if kill -0 "$child_pid" 2>/dev/null; then
|
|
||||||
# Attempt to terminate wrapper and spawn a true setsid child.
|
|
||||||
kill "$child_pid" 2>/dev/null || true
|
|
||||||
# Short sleep to allow process cleanup (best-effort)
|
|
||||||
sleep 0.05
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Now start the real setsid-backed child with same redirections
|
|
||||||
setsid "${cmd[@]}" >"${logfile}" 2>&1 < /dev/null &
|
|
||||||
child_pid=$!
|
|
||||||
disown "$child_pid" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s' "$child_pid"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
start_with_nohup() {
|
|
||||||
# Use nohup as fallback; nohup will ignore HUP but doesn't start a new session.
|
|
||||||
nohup "${cmd[@]}" >"${logfile}" 2>&1 < /dev/null &
|
|
||||||
child_pid=$!
|
|
||||||
disown "$child_pid" 2>/dev/null || true
|
|
||||||
printf '%s' "$child_pid"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main launcher: pick method and start
|
|
||||||
echo "Launching command: ${cmd[*]}"
|
|
||||||
echo "Log file: ${logfile}"
|
|
||||||
echo "Method: ${method}"
|
|
||||||
|
|
||||||
child_pid=""
|
|
||||||
|
|
||||||
if [[ "${method}" == "setsid" ]]; then
|
|
||||||
# Try to start via setsid; if setsid not present, fallback to nohup
|
|
||||||
if child_pid=$(start_with_setsid); then
|
|
||||||
: # success
|
|
||||||
else
|
else
|
||||||
echo "setsid not available, falling back to nohup"
|
maxfd=$(ulimit -n 2>/dev/null || echo 256)
|
||||||
child_pid=$(start_with_nohup)
|
case "$maxfd" in (*[!0-9]*|"") maxfd=256 ;; esac
|
||||||
|
fd=3
|
||||||
|
while [ "$fd" -le "$maxfd" ]; do
|
||||||
|
eval "exec ${fd}>&-" 2>/dev/null || true
|
||||||
|
fd=$((fd+1))
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Redirect stdio: stdin -> /dev/null, stdout+stderr -> systemd-cat (journal)
|
||||||
|
exec </dev/null
|
||||||
|
# Note: the systemd-cat command and its args are inserted by the outer script.
|
||||||
|
exec > >( __SYSTEMD_CAT__ ) 2>&1
|
||||||
|
|
||||||
|
# Exec the target command; "$@" expands to the command and its args passed after --
|
||||||
|
exec "$@"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Substitute placeholders with safe values
|
||||||
|
inner_script=${inner_script//'__UMASK__'/"$umask_val"}
|
||||||
|
# Build the systemd-cat invocation string (path + args) — shell-escaped
|
||||||
|
# We need a single token string like: /bin/systemd-cat -t 'tag' -p 'priority'
|
||||||
|
systemd_cat_cmd="$(printf '%s' "$systemd_cat_path")$journal_args_escaped"
|
||||||
|
# Replace placeholder __SYSTEMD_CAT__ with the built command
|
||||||
|
inner_script=${inner_script//'__SYSTEMD_CAT__'/"$systemd_cat_cmd"}
|
||||||
|
|
||||||
|
# Start the detached process via setsid
|
||||||
|
setsid bash -c "$inner_script" -- "${cmd[@]}" &
|
||||||
else
|
else
|
||||||
child_pid=$(start_with_nohup)
|
# Fallback: redirect to logfile (file mode)
|
||||||
|
inner_script=$(cat <<'EOF'
|
||||||
|
umask __UMASK__
|
||||||
|
cd / || true
|
||||||
|
|
||||||
|
# Close fds (best-effort)
|
||||||
|
if [ -d /proc/self/fd ]; then
|
||||||
|
for fdpath in /proc/self/fd/*; do
|
||||||
|
fdnum=${fdpath##*/}
|
||||||
|
case "$fdnum" in
|
||||||
|
0|1|2) continue ;;
|
||||||
|
esac
|
||||||
|
eval "exec ${fdnum}>&-" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
else
|
||||||
|
maxfd=$(ulimit -n 2>/dev/null || echo 256)
|
||||||
|
case "$maxfd" in (*[!0-9]*|"") maxfd=256 ;; esac
|
||||||
|
fd=3
|
||||||
|
while [ "$fd" -le "$maxfd" ]; do
|
||||||
|
eval "exec ${fd}>&-" 2>/dev/null || true
|
||||||
|
fd=$((fd+1))
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Write pidfile (best-effort)
|
exec </dev/null
|
||||||
if [[ -n "${child_pid}" ]]; then
|
exec > "__LOGFILE__" 2>&1
|
||||||
printf '%s\n' "${child_pid}" > "${pidfile}" 2>/dev/null || true
|
|
||||||
|
exec "$@"
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
inner_script=${inner_script//'__UMASK__'/"$umask_val"}
|
||||||
|
# Escape logfile path for safe insertion (single-quote style)
|
||||||
|
log_escaped=${logfile//\'/\'"\'"\'}
|
||||||
|
inner_script=${inner_script//'__LOGFILE__'/"'$log_escaped'"}
|
||||||
|
setsid bash -c "$inner_script" -- "${cmd[@]}" &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reporting
|
child_pid=$!
|
||||||
|
|
||||||
|
# Try to disown if supported
|
||||||
|
if command -v disown >/dev/null 2>&1; then
|
||||||
|
disown "$child_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Optionally write pidfile
|
||||||
|
if [[ -n "${pidfile}" ]]; then
|
||||||
|
mkdir -p -- "$(dirname "${pidfile}")"
|
||||||
|
printf '%s\n' "${child_pid}" > "${pidfile}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Started command: $(quoted_cmd "${cmd[@]}")"
|
||||||
echo "Script PID: $$"
|
echo "Script PID: $$"
|
||||||
if [[ -n "${child_pid}" ]]; then
|
echo "Daemon PID: ${child_pid}"
|
||||||
echo "Child PID: ${child_pid}"
|
if $use_journal; then
|
||||||
else
|
echo "Output destination: systemd-journald (via $(printf '%s' "$systemd_cat_path"))"
|
||||||
echo "Child PID: (unknown)"
|
echo "Journal tag: ${tag}"
|
||||||
|
if [[ -n "${priority}" ]]; then
|
||||||
|
echo "Journal priority: ${priority}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Log file: ${logfile}"
|
||||||
|
fi
|
||||||
|
if [[ -n "${pidfile}" ]]; then
|
||||||
|
echo "PID file: ${pidfile}"
|
||||||
fi
|
fi
|
||||||
echo "Pidfile: ${pidfile}"
|
|
||||||
echo "Started at: $(date --iso-8601=seconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)"
|
echo "Started at: $(date --iso-8601=seconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)"
|
||||||
|
|
||||||
|
if ! $use_journal; then
|
||||||
echo
|
echo
|
||||||
echo "To follow the log: tail -F ${logfile}"
|
echo "To follow the log: tail -F ${logfile}"
|
||||||
|
else
|
||||||
exit 0
|
echo
|
||||||
|
echo "To view logs for this tag: journalctl -t ${tag} -f"
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user