<commit_before>#! /bin/sh
#
# Control program for managing pmlogger and pmie instances.
#
# Copyright (c) 2020 <NAME>.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# TODO
# - regex expansion for <class>
# - create => create + start ... is there a case for create only?  
# - destroy => stop + destroy ... is there a case for destroy iff already
#   stopped?
# - more than 1 -c option ... what does it mean?  the current code simply
#   and silently uses the last -c option from the command line (this
#   warrants at least a warning) ... if supported the likely semantics
#   are the union of the named classes ... unless this is allowed, a glob
#   pattern for the -c arg (classname) makes no sense
# - pmfind integration
# - other sections in the "policy" files, especially with pmfind to
#   (a) at create, pick a class by probing the host
#   (b) at create, decide not to by probing the host or by hostname
#       pattern
#   (c) at destroy, decide not to or wait some time before destroying
#       (the latter is really hard)
# - IAM=pmie is completely untested
#

. "$PCP_DIR/etc/pcp.env"

prog=`basename "$0"`
case "$prog"
in
    pmlogctl*)	IAM=pmlogger
		CONTROLFILE=$PCP_PMLOGGERCONTROL_PATH
    		;;
    pmiectl*)	IAM=pmie
		CONTROLFILE=$PCP_PMIECONTROL_PATH
    		;;
    *)		echo >&2 "$0: who the hell are you, bozo?"
    		exit 1
		;;
esac
CONTROLDIR=${CONTROLFILE}.d

tmp=`mktemp -d "$PCP_TMPFILE_DIR/$prog.XXXXXXXXX"` || exit 1
status=0

_cleanup()
{
    [ -n "$action" -a "$action" != status ] && _unlock
    rm -rf $tmp
}
trap "_cleanup; exit \$status" 0 1 2 3 15

cat >$tmp/usage <<End-of-File
# Usage: [options] command [host ...]

Options:
  -a,--all                      apply action to all matching hosts
  -c=NAME,--class=NAME    	${IAM} instances belong to the NAME class \
[default: default]
  -f,--force                    force action if possible
  -n,--showme             	perform a dry run, showing what would be done
  -p=POLICY,--policy=POLICY	use POLICY as the class policy file \
[default: $PCP_ETC_DIR/pcp/${IAM}/class.d/<class>]
  -v,--verbose            	increase verbosity
  --help
End-of-File

_warning()
{
    echo >&2 "Warning: $1"
}

_error()
{
    echo >&2 "Error: $1"
    status=1
    exit
}

_lock()
{
    $SHOWME && return

    # can assume $_dir is writeable ... if we get this far we're running
    # as root ...
    #
    _dir="$PCP_ETC_DIR/pcp/${IAM}"
    # demand mutual exclusion
    #
    rm -f $tmp/stamp $tmp/out
    _delay=200		# 1/10 of a second, so max wait is 20 sec
    while [ $_delay -gt 0 ]
    do
	if pmlock -v "$_dir/lock" >>$tmp/out 2>&1
	then
	    echo "$$" >"$_dir/lock"
	    break
	else
	    [ -f $tmp/stamp ] || touch -t `pmdate -30M %Y%m%d%H%M` $tmp/stamp
	    find $tmp/stamp -newer "$_dir/lock" -print 2>/dev/null >$tmp/tmp
	    if [ -s $tmp/tmp ]
	    then
		if [ -f "$_dir/lock" ]
		then
		    _warning "removing lock file older than 30 minutes (PID `cat $_dir/lock`)"
		    LC_TIME=POSIX ls -l "$_dir/lock"
		    rm -f "$_dir/lock"
		else
		    # there is a small timing window here where pmlock
		    # might fail, but the lock file has been removed by
		    # the time we get here, so just keep trying
		    #
		    :
		fi
	    fi
	fi
	pmsleep 0.1
	_delay=`expr $_delay - 1`
    done

    if [ $_delay -eq 0 ]
    then
	# failed to gain mutex lock
	#
	if [ -f "$_dir/lock" ]
	then
	    _warning "is another $prog job running concurrently?"
	    LC_TIME=POSIX ls -l "$_dir/lock"
	else
	    _error "`cat $tmp/out`"
	fi
	_error "failed to acquire exclusive lock ($_dir/lock) ..."
	return 1
    else
	if $VERY_VERBOSE
	then
	    echo "Lock acquired `cat $_dir/lock` `ls -l $_dir/lock`"
	fi
    fi

    return 0
}

_unlock()
{
    $SHOWME && return
    _dir="$PCP_ETC_DIR/pcp/${IAM}"
    if [ -f "$_dir/lock" ]
    then
	rm -f "$_dir/lock"
	$VERY_VERBOSE && echo "Lock released"
    fi
}

# FreeBSD's egrep does not support -r nor -Z
#
# This variant accepts -r or -rl as the first argument ...
# -r always outputs the filename at the start of the line (no matter
# how may filenames are processed, followed by a | (using a : causes
# all manner of problems with the hostname local:) followed by line
# of text from the file that matches the pattern
#
_egrep()
{
    if [ "$1" = "-rl" ]
    then
	_text=false
    elif [ "$1" = "-r" ]
    then
	_text=true
    else
	echo >&2 "Botch: _egrep() requires -r or -rl, not $1"
	return
    fi
    shift
    _pat="$1"
    shift

    # skip errors from find(1), only interested in real, existing files
    #
    find $* -type f 2>/dev/null \
    | while read _f
    do
	if $_text
	then
	    egrep "$_pat" <$_f | sed -e "s;^;$_f|;"
	else
	    egrep "$_pat" <$_f >/dev/null && echo "$_f"
	fi
    done
}

_usage()
{
    pmgetopt --progname=$prog --config=$tmp/usage --usage 2>&1 \
    | sed >&2 -e 's/ \[default/\n                        [default/'
    cat >&2 <<End-of-File

Avaliable commands:
   create [-c classname] host ...
   {start|stop|restart|destroy|status} [-c classname] [host ...]

   and host may be a valid hostname or an egrep(1) pattern that matches
   the start of a hostname
End-of-File
    status=1
    exit
}

# find matching hosts from command line args ...
# 1. find control lines that contain each named host (or all hosts in the
#    case of no hosts on the command line)
# 2. if --class is specified, then restrict the hosts from 1. to those that
#    are in the named class<commit_msg>src/pmlogctl/pmlogctl.sh: divert stderr from egrep<commit_after>#! /bin/sh
#
# Control program for managing pmlogger and pmie instances.
#
# Copyright (c) 2020 <NAME>.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# TODO
# - regex expansion for <class>
# - create => create + start ... is there a case for create only?  
# - destroy => stop + destroy ... is there a case for destroy iff already
#   stopped?
# - more than 1 -c option ... what does it mean?  the current code simply
#   and silently uses the last -c option from the command line (this
#   warrants at least a warning) ... if supported the likely semantics
#   are the union of the named classes ... unless this is allowed, a glob
#   pattern for the -c arg (classname) makes no sense
# - pmfind integration
# - other sections in the "policy" files, especially with pmfind to
#   (a) at create, pick a class by probing the host
#   (b) at create, decide not to by probing the host or by hostname
#       pattern
#   (c) at destroy, decide not to or wait some time before destroying
#       (the latter is really hard)
# - IAM=pmie is completely untested
#

. "$PCP_DIR/etc/pcp.env"

prog=`basename "$0"`
case "$prog"
in
    pmlogctl*)	IAM=pmlogger
		CONTROLFILE=$PCP_PMLOGGERCONTROL_PATH
    		;;
    pmiectl*)	IAM=pmie
		CONTROLFILE=$PCP_PMIECONTROL_PATH
    		;;
    *)		echo >&2 "$0: who the hell are you, bozo?"
    		exit 1
		;;
esac
CONTROLDIR=${CONTROLFILE}.d

tmp=`mktemp -d "$PCP_TMPFILE_DIR/$prog.XXXXXXXXX"` || exit 1
status=0

_cleanup()
{
    [ -n "$action" -a "$action" != status ] && _unlock
    rm -rf $tmp
}
trap "_cleanup; exit \$status" 0 1 2 3 15

cat >$tmp/usage <<End-of-File
# Usage: [options] command [host ...]

Options:
  -a,--all                      apply action to all matching hosts
  -c=NAME,--class=NAME    	${IAM} instances belong to the NAME class \
[default: default]
  -f,--force                    force action if possible
  -n,--showme             	perform a dry run, showing what would be done
  -p=POLICY,--policy=POLICY	use POLICY as the class policy file \
[default: $PCP_ETC_DIR/pcp/${IAM}/class.d/<class>]
  -v,--verbose            	increase verbosity
  --help
End-of-File

_warning()
{
    echo >&2 "Warning: $1"
}

_error()
{
    echo >&2 "Error: $1"
    status=1
    exit
}

_lock()
{
    $SHOWME && return

    # can assume $_dir is writeable ... if we get this far we're running
    # as root ...
    #
    _dir="$PCP_ETC_DIR/pcp/${IAM}"
    # demand mutual exclusion
    #
    rm -f $tmp/stamp $tmp/out
    _delay=200		# 1/10 of a second, so max wait is 20 sec
    while [ $_delay -gt 0 ]
    do
	if pmlock -v "$_dir/lock" >>$tmp/out 2>&1
	then
	    echo "$$" >"$_dir/lock"
	    break
	else
	    [ -f $tmp/stamp ] || touch -t `pmdate -30M %Y%m%d%H%M` $tmp/stamp
	    find $tmp/stamp -newer "$_dir/lock" -print 2>/dev/null >$tmp/tmp
	    if [ -s $tmp/tmp ]
	    then
		if [ -f "$_dir/lock" ]
		then
		    _warning "removing lock file older than 30 minutes (PID `cat $_dir/lock`)"
		    LC_TIME=POSIX ls -l "$_dir/lock"
		    rm -f "$_dir/lock"
		else
		    # there is a small timing window here where pmlock
		    # might fail, but the lock file has been removed by
		    # the time we get here, so just keep trying
		    #
		    :
		fi
	    fi
	fi
	pmsleep 0.1
	_delay=`expr $_delay - 1`
    done

    if [ $_delay -eq 0 ]
    then
	# failed to gain mutex lock
	#
	if [ -f "$_dir/lock" ]
	then
	    _warning "is another $prog job running concurrently?"
	    LC_TIME=POSIX ls -l "$_dir/lock"
	else
	    _error "`cat $tmp/out`"
	fi
	_error "failed to acquire exclusive lock ($_dir/lock) ..."
	return 1
    else
	if $VERY_VERBOSE
	then
	    echo "Lock acquired `cat $_dir/lock` `ls -l $_dir/lock`"
	fi
    fi

    return 0
}

_unlock()
{
    $SHOWME && return
    _dir="$PCP_ETC_DIR/pcp/${IAM}"
    if [ -f "$_dir/lock" ]
    then
	rm -f "$_dir/lock"
	$VERY_VERBOSE && echo "Lock released"
    fi
}

# FreeBSD's egrep does not support -r nor -Z
#
# This variant accepts -r or -rl as the first argument ...
# -r always outputs the filename at the start of the line (no matter
# how may filenames are processed, followed by a | (using a : causes
# all manner of problems with the hostname local:) followed by line
# of text from the file that matches the pattern
#
_egrep()
{
    if [ "$1" = "-rl" ]
    then
	_text=false
    elif [ "$1" = "-r" ]
    then
	_text=true
    else
	echo >&2 "Botch: _egrep() requires -r or -rl, not $1"
	return
    fi
    shift
    _pat="$1"
    shift

    # skip errors from find(1) and egrep(1), only interested in matches for
    # real, existing files
    #
    find $* -type f 2>/dev/null \
    | while read _f
    do
	if $_text
	then
	    egrep "$_pat" <$_f 2>/dev/null | sed -e "s;^;$_f|;"
	else
	    egrep "$_pat" <$_f >/dev/null 2>&1 && echo "$_f"
	fi
    done
}

_usage()
{
    pmgetopt --progname=$prog --config=$tmp/usage --usage 2>&1 \
    | sed >&2 -e 's/ \[default/\n                        [default/'
    cat >&2 <<End-of-File

Avaliable commands:
   create [-c classname] host ...
   {start|stop|restart|destroy|status} [-c classname] [host ...]

   and host may be a valid hostname or an egrep(1) pattern that matches
   the start of a hostname
End-of-File
    status=1
    exit
}

# find matching hosts from command line args ...
# 1. find control lines that contain each named host (or all hosts in the
#    case of no hosts on the command line)
# 2. if --class is specified, then restrict the hosts from 1. to those that
#    are in the named class
{'fraction_non_alphanumeric': 0.11873684210526315, 'fraction_numerical': 0.010610526315789473, 'mean_word_length': 2.535576064304853, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5253081', 'n_tokens_mistral': 4268, 'n_tokens_neox': 4042, 'n_words': 1559}