From 271487380ce1a8091f95ccee27e6b0403b5f4866 Mon Sep 17 00:00:00 2001 From: "SilverAg.L" Date: Tue, 16 Dec 2025 02:58:04 +0800 Subject: [PATCH] runbg regenerate. - make use of journald - avoid command once invoke being triggered twice (or even more). --- bin/runbg | 360 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 221 insertions(+), 139 deletions(-) diff --git a/bin/runbg b/bin/runbg index 614c63e..8703dd9 100755 --- a/bin/runbg +++ b/bin/runbg @@ -6,49 +6,47 @@ PATH=$(zsh -c -i 'echo $PATH') export PATH -# start-bg.sh - robustly start a command in the background and redirect stdout+stderr to a log # Generated by GitHub Copilot. - +# start-bg-journal.sh - start a command as a detached daemon and send stdout/stderr to systemd-journald +# # Features: -# - Uses setsid (preferred) to detach from the controlling terminal. -# - Falls back to nohup if setsid is unavailable. -# - Runs the command with stdin closed, stdout+stderr redirected to a logfile. -# - Sets a safe umask and changes to / to avoid blocking filesystems. -# - Writes a pidfile next to the log for easy management. -# - Uses positional args ($@), script PID ($$) and reports child PID ($!). +# - Sends stdout+stderr to journald using systemd-cat when available. +# - Falls back to writing into a logfile if systemd-cat isn't present or if --log is passed. +# - Uses setsid to detach, closes inherited file descriptors (best-effort), +# redirects stdin to /dev/null, and execs the target command. +# - Supports PID file, umask, and a syslog identifier (tag) and priority for journald. +# - Uses positional args ($@) for the command; uses $$ in auto-generated names and prints the child PID ($!). # # Usage: -# start-bg.sh [ -l LOGFILE | -d LOGDIR ] [ -m METHOD ] -- 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 +# start-bg-journal.sh [ -t TAG ] [ -r PRIORITY ] [ -l LOGFILE | -d LOGDIR ] [ -p PIDFILE ] [ -u UMASK ] -- command [args...] # # Examples: -# ./start-bg.sh -- sleep 60 -# ./start-bg.sh -d /var/log/myapp -- /usr/bin/myapp --config /etc/myapp.conf -# ./start-bg.sh -l ./my.log -- /usr/bin/myapp arg1 arg2 - +# ./start-bg-journal.sh -t myapp -- /usr/bin/myapp --config /etc/myapp.conf +# ./start-bg-journal.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 progname=$(basename "$0") usage() { cat </..pid<$$>.log - -A pidfile is written as .pid containing the child PID. EOF exit 1 } @@ -56,20 +54,32 @@ EOF # Defaults logdir="/tmp/$USER/runner" logfile="" -method="setsid" +pidfile="" +umask_val="0022" +tag="" +priority="" -# Parse options until the `--` separator +# Parse options up to -- while [[ $# -gt 0 ]]; do 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) if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi logfile="$2"; shift 2 ;; -d|--dir) if [[ $# -lt 2 ]]; then echo "Missing argument for $1"; usage; fi logdir="$2"; shift 2 ;; - -m|--method) + -p|--pidfile) 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) usage ;; --) @@ -81,130 +91,202 @@ while [[ $# -gt 0 ]]; do esac done -# Remaining args are the command to run if [[ $# -eq 0 ]]; then echo "Error: no command specified." usage fi -# Preserve the command and args +# Build command array from remaining args. cmd=( "$@" ) cmd_basename=$(basename "${cmd[0]}") timestamp=$(date +%Y%m%d-%H%M%S) -mkdir -p -- "${logdir}" - -if [[ -z "${logfile}" ]]; then - logfile="${logdir}/${cmd_basename}.${timestamp}.pid$$.log" +# If tag not set, default to command basename +if [[ -z "$tag" ]]; then + tag="${cmd_basename}" fi -pidfile="${logfile}.pid" +# Detect systemd-cat +systemd_cat_path="" +if command -v systemd-cat >/dev/null 2>&1; then + systemd_cat_path=$(command -v systemd-cat) +fi -# Validate method -case "${method}" in - setsid|nohup) ;; - *) - echo "Invalid method: ${method}. Allowed: setsid|nohup" - exit 2 - ;; -esac - -# Prepare environment for child -# We construct a small wrapper for safe daemonization: -start_with_setsid() { - # Prefer existing setsid binary (coreutils/util-linux) - if ! command -v setsid >/dev/null 2>&1; then - return 1 - fi - - # Use setsid to start the process in a new session. We: - # - set a conservative 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 - # 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 - disown "$child_pid" 2>/dev/null || true - - # setsid the already-forked child to start a new session. Some systems allow: - # setsid -w - # but that's nonportable; instead we try to re-exec the command under setsid. - # If `setsid` can't be applied to the backgrounded PID, we respawn via setsid: - if setsid true >/dev/null 2>&1; then - # Respawn under setsid to ensure proper session leader if available. - # 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 +# 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 - echo "setsid not available, falling back to nohup" - child_pid=$(start_with_nohup) + use_journal=false + fi +fi + +# Prepare logfile if needed +mkdir -p -- "${logdir}" +if ! $use_journal; then + if [[ -z "${logfile}" ]]; then + logfile="${logdir}/${cmd_basename}.${timestamp}.pid$$.log" + fi + # Ensure file exists and is writable + : > "${logfile}" || { echo "Cannot write to log file: ${logfile}"; exit 1; } +fi + +# 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# }" +} + +# Build the inner script to run under setsid. We inject expanded variables from outer shell. +if $use_journal; then + # 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 "Warning: unknown priority '$priority' — journald may reject it." >&2 ;; + esac + + # Prepare a single string of quoted journal args for insertion into the -c script + # We need to protect values with single quotes if they contain special chars. + journal_args_escaped="" + for a in "${journal_args[@]}"; do + # replace every single quote with '\'"\'' + a_escaped=${a//\'/\'"\'"\'} + journal_args_escaped="$journal_args_escaped '$a_escaped'" + done + + 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 + +# Redirect stdio: stdin -> /dev/null, stdout+stderr -> systemd-cat (journal) +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 + # 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 + +exec "__LOGFILE__" 2>&1 + +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 + +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 "Daemon PID: ${child_pid}" +if $use_journal; then + echo "Output destination: systemd-journald (via $(printf '%s' "$systemd_cat_path"))" + echo "Journal tag: ${tag}" + if [[ -n "${priority}" ]]; then + echo "Journal priority: ${priority}" fi else - child_pid=$(start_with_nohup) + echo "Log file: ${logfile}" fi - -# Write pidfile (best-effort) -if [[ -n "${child_pid}" ]]; then - printf '%s\n' "${child_pid}" > "${pidfile}" 2>/dev/null || true +if [[ -n "${pidfile}" ]]; then + echo "PID file: ${pidfile}" fi - -# Reporting -echo "Script PID: $$" -if [[ -n "${child_pid}" ]]; then - echo "Child PID: ${child_pid}" -else - echo "Child PID: (unknown)" -fi -echo "Pidfile: ${pidfile}" echo "Started at: $(date --iso-8601=seconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)" -echo -echo "To follow the log: tail -F ${logfile}" -exit 0 +if ! $use_journal; then + echo + echo "To follow the log: tail -F ${logfile}" +else + echo + echo "To view logs for this tag: journalctl -t ${tag} -f" +fi