#! /bin/bash
#########################################################################
#									#
#	dnsmasq-postrotate is automatically generated,			#
#		please do not modify!					#
#									#
#########################################################################

#########################################################################
#									#
# Author: Copyright (C) 2018-2019, 2021, 2023-2025  Mark Grant		#
#									#
# Released under the GPLv3 only.					#
# SPDX-License-Identifier: GPL-3.0-only					#
#									#
# Purpose:								#
# To ask the daemon to re-open it's log file which is necessary after	#
# rotating the logs. Also request the daemon to write summary stats to	#
# the syslog.								#
#									#
# Syntax:	See usage()						#
#									#
# Exit codes used:-							#
# Bash standard Exit Codes:	0 - success				#
#				1 - general failure			#
# User-defined exit code range is 64 - 113				#
#	C/C++ Semi-standard exit codes from sysexits.h range is 64 - 78	#
#		EX_USAGE	64	command line usage error	#
#		EX_DATAERR	65	data format error		#
#		EX_NOINPUT	66	cannot open input		#
#		EX_NOUSER	67	addressee unknown		#
#		EX_NOHOST	68	host name unknown		#
#		EX_UNAVAILABLE	69	service unavailable		#
#		EX_SOFTWARE	70	internal software error		#
#		EX_OSERR	71	system error (e.g., can't fork)	#
#		EX_OSFILE	72	critical OS file missing	#
#		EX_CANTCREAT	73	can't create (user) output file	#
#		EX_IOERR	74	input/output error		#
#		EX_TEMPFAIL	75	temp failure; user is invited	#
#					to retry			#
#		EX_PROTOCOL	76	remote error in protocol	#
#		EX_NOPERM	77	permission denied		#
#		EX_CONFIG	78	configuration error		#
#	User-defined (here) exit codes range 79 - 113:			#
#		None							#
#									#
# Further Info:								#
#									#
#########################################################################


#########################
# Script-wide variables #
#########################
outputprefix="$(basename "$0"):"
readonly outputprefix
readonly version=1.1.1				# script version
readonly packageversion=1.3.1		# package version

readonly logrotatefile="/etc/logrotate.d/dnsmasq"
loglocation=""
logonly=false
oldloglocation=""
pidnum=""
postrotate=false
trace=false
verbose=false


#############
# Functions #
#############

# -h --help output.
# No parameters
# No return value
usage()
{
cat << EOF
Usage is:-
$(basename "$0") {-h|-V}
$(basename "$0") [-l] [-p] [-t] [-v]
Usage is:-
$(basename "$0") [options]
	-h or --help displays usage information
	-l or --log-only do not emit messages to stdout or stderr, just log
	-p or --post-rotate perform post-rotate function
	-t or --trace output the analysis of configuration parameters
	-v or --verbose verbose output
	-V or --version displays version information
EOF
}

# Standard function to emit messages depending on various parameters.
# Parameters -	$1 What:-	The message to emit.
#		$2 Where:-	stdout == 0
#				stderr == 1
#		$3 When:-	$3 == 0, Always.
#				$3 == 1, Output only if $verbose is true.
#				$3 == 2, Output only if $trace is true.
# No return value.
output()
{
	if (( ! $3 )) || [[ (( $3 == 1 )) && $verbose == true ]] \
		|| [[ (( $3 == 2 )) && $trace == true ]]; then
		logger "$outputprefix $1"
		std_cmd_err_handler $?
		if $logonly; then
			return
		fi
		if (( ! $2 )); then
			printf "%s %s\n" "$outputprefix" "$1"
		else
			printf "%s %s\n" "$outputprefix" "$1" 1>&2
		fi
	fi
}

# Standard function to tidy up and return exit code.
# Parameters - 	$1 is the exit code.
# No return value.
script_exit()
{
	exit "$1"
}

# Standard function to test command error and exit if non-zero.
# Parameters - 	$1 is the exit code, (normally $? from the preceeding command).
# No return value.
std_cmd_err_handler()
{
	if (( $1 )); then
		script_exit "$1"
	fi
}

# Standard trap exit function.
# No parameters.
# No return value.
# shellcheck disable=SC2317  # Do not warn about unreachable commands in trap
# functions, they are legitimate.
trap_exit()
{
	local -i exit_code=$?
	local msg

	msg="Script terminating with exit code $exit_code due to trap received."
	output "$msg" 1 0
	script_exit "$exit_code"
}

# Setup trap.
trap trap_exit SIGHUP SIGINT SIGQUIT SIGTERM

# Process command line arguments with GNU getopt.
# Parameters -	$1 is the command line.
# No return value.
proc_CL()
{
	local GETOPTTEMP
	local tmpGETOPTTEMP

	tmpGETOPTTEMP="getopt -o hlptvV --long help,log-only,post-rotate,"
	tmpGETOPTTEMP+="trace,verbose,version"
	GETOPTTEMP=$($tmpGETOPTTEMP -n "$0" -- "$@")
	std_cmd_err_handler $?

	eval set -- "$GETOPTTEMP"
	std_cmd_err_handler $?

	while true; do
		case "$1" in
		-h|--help)
			usage
			shift
			script_exit 0
			;;
		-l|--log-only)
			logonly=true
			shift
			;;
		-p|--post-rotate)
			postrotate=true
			shift
			;;
		-t|--trace)
			trace=true
			verbose=true
			shift
			;;
		-v|--verbose)
			verbose=true
			shift
			;;
		-V|--version)
			printf "%s Script version %s\n" "$0" $version
			printf "%s Package version %s\n" "$0" $packageversion
			shift
			script_exit 0
			;;
		--)	shift
			break
			;;
		*)	output "Internal error." 1 0
			script_exit 64
			;;
		esac
	done

	# Script does not accept other arguments.
	if (( $# > 0 )); then
		output "Invalid argument." 1 0
		script_exit 64
	fi
}

# Process a conf-dir parameter - a directory and masks.
# Parameters -	$1 is a conf-dir parameter.
# No return value.
proc_conf_dir ()
{
	local -i index
	local array=()
	local find_cmd
	local tmp_conf_list
	local msg
	local IFS_old=$IFS

	IFS+=","
	read -r -a array <<< "$1"
	output "Processing conf-dir ${array[0]}" 0 2
	find_cmd="find ${array[0]} -type f -maxdepth 1 -mindepth 1 "
	# ${! finds the array index}
	for index in "${!array[@]}"; do
		if [ $index == 0 ]; then
			continue
		fi
		if [[ ${array[index]} = \** ]]; then
			find_cmd+="-name ${array[index]} "
		else
			find_cmd+="! -name *${array[index]} "

		fi
	done
	tmp_conf_list=$(eval "$find_cmd" 2>/dev/null | xargs)
	next_conf_file_list+=$tmp_conf_list" "
	output "Find command used was: $find_cmd" 0 2
	msg="conf-files $tmp_conf_list from ${array[0]} added to the "
	msg+="list to process"
	output "$msg" 0 2

	IFS=$IFS_old
}

# Parse the command line only for the parameters of interest.
# Parameters -	$@ is the dnsmasq command line.
# No return value.
cmd_line_parse()
{
	local cmd
	local GETOPTTEMP
	local msg

	cmd="getopt -q -o 7:8:C:x: --long conf-dir:,log-facility:,conf-file:,"
	cmd+="pid-file:"
	# No error checking of the following as errors expected (partial options
	# specified).
	# shellcheck disable=SC2068	# We want seperate elements to iterate
	# through, not just one string.
	GETOPTTEMP=$($cmd -n "$0" -- $@)

	eval set -- "$GETOPTTEMP"
	std_cmd_err_handler $?

	while true; do
		case "$1" in
		-7|--conf-dir)
			msg="conf-dir $2 found on command line and "
			msg+="being processed"
			output "$msg" 0 2
			proc_conf_dir "$2"
			shift 2
			;;
		-8|--log-facility)
			cmd_line_loglocation=$2
			output "log-facility $2 found on command line" 0 2
			shift 2
			;;
		-C|--conf-file)
			msg="conf-file $2 found on command line and "
			msg+="overrides default $init_conffile. $2 "
			msg+="added to the list to process, if present"
			output "$msg" 0 2
			init_conffile="$(ls "$2" 2>/dev/null) "
			if [[ "$init_conffile" != " " ]]; then
				output "$2 found" 0 2
			else
				output "$2 not found" 0 2
			fi
			shift 2
			;;
		-x|--pid-file)
			cmd_line_pidfile=$2
			output "pid-file $2 found on command line" 0 2
			shift 2
			;;
		--)	shift
			break
			;;
		*)	echo "error"
			exit 1
			;;
		esac
	done
}

# Process a conf file.
# Parameters -	$1 is a conf-file parameter (A config filename).
# No return value.
proc_conf_file()
{
	local input=()
	local tmp_conffile
	local msg
	local oldIFS=$IFS
	IFS="="

	output "Processing conf-file $1" 0 2
	exec 3<"$1"
	while read -u3 -ra input; do
		# Ignore comment lines and blank lines.
		if [[ ${input[0]} = "#"* || ! ${input[0]} ]]; then
			continue
		fi

		case ${input[0]} in
		conf-dir)
			msg="conf-dir ${input[1]} found and being "
			msg+="processed"
			output "$msg" 0 2
			proc_conf_dir "${input[1]}"
			;;
		log-facility)
			# If already found, skip
			if [[ ! $loglocation ]]; then
				loglocation=${input[1]}
				msg="log-facility ${input[1]} found in "
				msg+="$1"
				output "$msg" 0 2
			fi
			;;
		conf-file)
			msg="conf-file ${input[1]} found and added to "
			msg+="the list to process, if present"
			output "$msg" 0 2
			tmp_conffile="$(ls "${input[1]}" 2>/dev/null) "
			next_conf_file_list+=$tmp_conffile
			if [[ $tmp_conffile != " " ]]; then
				output "${input[1]} found" 0 2
			else
				output "${input[1]} not found" 0 2
			fi
			;;
		pid-file)
			# If already found, skip
			if [[ ! $pidfile ]]; then
				pidfile=${input[1]}
				msg="pid-file ${input[1]} found in $1"
				output "$msg" 0 2
			fi
			;;
		*)
			# Not interested in any other parameters so just fall
			# through to next parameter
			;;
		esac
	done
	exec 3<&-

	IFS=$oldIFS
}

# Process the aggregated list of conf files from next_conf_file_list.
# Parameters -	$@ is the list of conf-files to process.
# No return value.
proc_conf_file_list()
{
	local conffile

	next_conf_file_list=""
	# shellcheck disable=SC2068	# We want seperate elements to iterate
	# through, not just one string.
	for conffile in $@; do
		proc_conf_file "$conffile"
	done
}

# Last resort to solve pid-file.
# No parameters.
# No return value.
pid_last_resort()
{
	local cmd

	# A general find to locate a pid file. The xdev means do not descend
	# into directories on other (mounted) filesystems. (Which avoids
	# problems on, for example, gvfs).
	# The prune and not readable means do not descend into non-readable
	# directories.
	cmd="find /run/ -xdev ! -readable -prune -o -name dnsmasq.pid "
	cmd+="-print"
	pidfile=$($cmd 2>/dev/null)

	[[ ! $pidfile ]] && pidfile=$ps_pid_file
}

# Retrieve the current log file location from the log rotate file.
# No parameters.
# No return value.
get_oldloglocation()
{
	local inputline

	if [[ ! -f $logrotatefile ]]; then
		output "Log rotate file $logrotatefile does not exist." 1 0
		return
	fi

	exec 4<$logrotatefile
	while read -u4 -r inputline; do
		if [[ $inputline != "#"* && $inputline ]]; then
			break
		fi
	done
	oldloglocation=$inputline
	exec 4<&-
}

# Retrieve the setup of desired config options via config files and CL.
# No parameters.
# No return value.
proc_get_setup()
{
	cmd_line_loglocation=""
	pidfile=""
	cmd_line_pidfile=""
	ps_pid_file=/tmp/$$.$(basename "$0")
	init_conffile=""
	next_conf_file_list=""

	local cmd_line

	# Set initial conf-file to man-page stated default, if present. May
	# later be overridden by the CL.
	init_conffile="$(ls /etc/dnsmasq.conf 2>/dev/null) "
	output "default conf is $init_conffile" 0 2

	# dnsmasq will always look in the dnsmasq.d directory, so add as a
	# default.
	proc_conf_dir "/etc/dnsmasq.d"

	cmd_line=$(ps --no-headers -C dnsmasq -o args)

	if [[ ! $cmd_line ]]; then
		output "cmd_line is empty string" 0 2
	fi

	if [[ $cmd_line ]]; then
		ps --no-headers -C dnsmasq -o pid  > "$ps_pid_file"
		cmd_line_parse "$cmd_line"
	else
		ps_pid_file=""
	fi

	# The man page states that a conf-file on the CL is an alternative to
	# the default, so if present, override default.
	next_conf_file_list="$init_conffile $next_conf_file_list "

	while [[ $next_conf_file_list ]]; do
		# If we have both, stop looking
		if [[ $loglocation && $pidfile ]]; then
			 break
		fi
		proc_conf_file_list "$next_conf_file_list"
	done

	# If not found use command line values, if present.
	[[ ! $loglocation ]] && loglocation=$cmd_line_loglocation
	[[ ! $pidfile ]] && pidfile=$cmd_line_pidfile

	# To be an alternative log file name, the entry must contain a '/'
	# character. This is mandated by dnsmasq.
	if [[ $loglocation && $loglocation != *"/"* ]]; then
		loglocation=""
	fi

	get_oldloglocation

	# If still no pid-file found, time for last resort.
	if [[ ! $pidfile ]]; then
		pid_last_resort
	fi

	if [[ $pidfile ]]; then
		pidnum=$(cat "$pidfile")
	fi
	# Remove temp pid file.
	rm -f "$ps_pid_file"

	# If no log location is found then use default location, the logrotate
	# file will be updated but if the log does not exist logrotate will just
	# skip it.
	[[ ! $loglocation ]] && loglocation=$oldloglocation

	output "Current loglocation is $oldloglocation" 0 2
	output "Final loglocation is $loglocation" 0 2
	output "Final pidfile is $pidfile" 0 2
	output "Final pidnum is $pidnum" 0 2

	unset cmd_line_loglocation
	unset pidfile
	unset cmd_line_pidfile
	unset ps_pid_file
	unset init_conffile
	unset next_conf_file_list
}

# Signal dnsmasq to write stats and open and close its logfile.
# No parameters.
# No return value.
signal_dnsmasq()
{
	# Request the daemon to write summary stats to the syslog.
	kill -s SIGUSR1 "$pidnum"
	std_cmd_err_handler $?
	output "Daemon requested to log summary stats." 0 1

	# Request the daemon to close and re-open it's log file.
	kill -s SIGUSR2 "$pidnum"
	std_cmd_err_handler $?
	output "Daemon requested to close and re-open it's log file." 0 1
}

# Replace the first live line of the logrotate file with the value discovered
# for log-facility.
# No parameters.
# No return value.
setup_logrotate()
{
	if [[ ! -f $logrotatefile ]]; then
		output "Log rotate file $logrotatefile does not exist." 1 0
		script_exit 66
	fi

	# Copy up to first live line.
	exec 4<$logrotatefile
	while read -u4 -r inputline; do
		if [[ $inputline != "#"* && $inputline ]]; then
			break
		fi
		 printf "%s\n" "$inputline" >> "$logrotatefile.tmp"
	done
	# Replace first live line.
	printf "%s\n" "$loglocation" >> "$logrotatefile.tmp"
	# Copy rest of file.
	while read -u4 -r inputline; do
		printf "%s\n" "$inputline" >> "$logrotatefile.tmp"
	done
	exec 4<&-

	# Replace file.
	rm -f $logrotatefile
	mv -f $logrotatefile.tmp $logrotatefile
}



########
# Main #
########

proc_CL "$@"

proc_get_setup

if [[ $oldloglocation != "$loglocation" ]]; then
	setup_logrotate
else
	output "No change required in logrotate file." 0 1
fi

if [[ ! $pidnum ]]; then
	output "dnsmasq daemon not running." 0 1
	script_exit 0
fi

if $postrotate ; then
	signal_dnsmasq
fi

script_exit 0

