# This program is copyright 2011-2026 Percona LLC and/or its affiliates.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
# 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, version 2; OR the Perl Artistic License.  On UNIX and similar
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
# licenses.
#
# You should have received a copy of the GNU General Public License, version 2
# along with this program; if not, see <https://www.gnu.org/licenses/>.
# ###########################################################################
# collect_mysql_info package
# ###########################################################################

# Package: collect_mysql_info
# collect collects mysql information.

# XXX
# THIS LIB REQUIRES log_warn_die.sh, summary_common.sh, and alt_cmds.sh!
# XXX

CMD_MYSQL="${CMD_MYSQL:-""}"
CMD_MYSQLDUMP="${CMD_MYSQLDUMP:-""}"

# Simply looks for instances of mysqld in the outof of ps.
collect_mysqld_instances () {
   local variables_file="$1"

   local pids="$(_pidof mysqld)"

   if [ -n "$pids" ]; then

      for pid in $pids; do
         local nice="$( get_nice_of_pid $pid )"
         local oom="$( get_oom_of_pid $pid )"
         echo "internal::nice_of_$pid    $nice" >> "$variables_file"
         echo "internal::oom_of_$pid    $oom" >> "$variables_file"
      done

      pids="$(echo $pids | sed -e 's/ /,/g')"
      ps ww -p "$pids" 2>/dev/null
   else
      echo "mysqld doesn't appear to be running"
   fi

}

# Tries to find the my.cnf file by examining 'ps' output.
# You have to specify the port for the instance you are
# interested in, in case there are multiple instances.
find_my_cnf_file() {
   local file="$1"
   local port="${2:-""}"
   local pid_file="${3:-""}"

   local cnf_file=""

   if [ "$pid_file" ]; then
      local pid=$(cat "$pid_file")
      cnf_file="$(grep --max-count 1 -E "^\s*$pid\s+" "$file" \
         | awk 'BEGIN{RS=" "; FS="=";} $1 ~ /--defaults-file/ { print $2; }')"

      if [ -n "$cnf_file" ]; then
         echo "$cnf_file"
         return
      fi
   fi

   if [ "$port" ]; then
      # Find the cnf file for the specific port.
      cnf_file="$(grep --max-count 1 "/mysqld.*--port=$port" "$file" \
         | awk 'BEGIN{RS=" "; FS="=";} $1 ~ /--defaults-file/ { print $2; }')"

      if [ -n "$cnf_file" ]; then
         echo "$cnf_file"
         return
      fi
   else
      cnf_file="$(grep --max-count 1 '/mysqld' "$file" \
         | awk 'BEGIN{RS=" "; FS="=";} $1 ~ /--defaults-file/ { print $2; }')"
   fi

   if [ -z "$cnf_file" ]; then
      # Cannot autodetect config file, try common locations.
      if [ -e "/etc/my.cnf" ]; then
         cnf_file="/etc/my.cnf"
      elif [ -e "/etc/mysql/my.cnf" ]; then
         cnf_file="/etc/mysql/my.cnf"
      elif [ -e "/var/db/mysql/my.cnf" ]; then
         cnf_file="/var/db/mysql/my.cnf";
      fi
   fi

   echo "$cnf_file"
}

collect_mysql_variables () {
   $CMD_MYSQL $EXT_ARGV -ss  -e 'SHOW /*!40100 GLOBAL*/ VARIABLES'
}

collect_mysql_status () {
   $CMD_MYSQL $EXT_ARGV -ss -e 'SHOW /*!50000 GLOBAL*/ STATUS'
}

collect_mysql_databases () {
   $CMD_MYSQL $EXT_ARGV -ss -e 'SHOW DATABASES' 2>/dev/null
}

collect_mysql_plugins () {
   $CMD_MYSQL $EXT_ARGV -ss -e 'SHOW PLUGINS' 2>/dev/null
}

collect_mysql_replica_status () {
   local replica_name="$1"

   $CMD_MYSQL $EXT_ARGV -ssE -e "SHOW ${replica_name} STATUS" 2>/dev/null
}

collect_mysql_innodb_status () {
   $CMD_MYSQL $EXT_ARGV -ssE -e 'SHOW /*!50000 ENGINE*/ INNODB STATUS' 2>/dev/null
}

collect_mysql_ndb_status () {
   $CMD_MYSQL $EXT_ARGV -ssE -e 'show /*!50000 ENGINE*/ NDB STATUS' 2>/dev/null
}

collect_mysql_processlist () {
   $CMD_MYSQL $EXT_ARGV -ssE -e 'SHOW FULL PROCESSLIST' 2>/dev/null
}

collect_mysql_users () {
# The where clause has been added to skip listing MySQL 8+ roles as users.
# ROLES are locked accounts, without passwords and expired.
   $CMD_MYSQL $EXT_ARGV -ss -e "SELECT COUNT(*), SUM(user=''), SUM(password=''), SUM(password NOT LIKE '*%') FROM mysql.user" 2>/dev/null
   if [ "$?" -ne 0 ]; then
       $CMD_MYSQL $EXT_ARGV -ss -e "SELECT COUNT(*), SUM(user=''), SUM(authentication_string=''), SUM(authentication_string NOT LIKE '*%') FROM mysql.user WHERE account_locked <> 'Y' AND password_expired <> 'Y' AND authentication_string <> ''" 2>/dev/null
   fi
}

collect_mysql_roles () {
   QUERY="SELECT DISTINCT User 'Role Name', if(from_user is NULL,0, 1) Active FROM mysql.user LEFT JOIN mysql.role_edges ON from_user=user WHERE account_locked='Y' AND password_expired='Y' AND authentication_string=''\G"
   $CMD_MYSQL $EXT_ARGV -ss -e "$QUERY" 2>/dev/null
}

collect_mysql_show_replica_hosts () {
   local version="$1"

   local replicas='replicas'
   if [ "$version" '<' "8.1" ]; then
      replicas='slave hosts';
   fi

   $CMD_MYSQL $EXT_ARGV -ssE -e "SHOW ${replicas}" 2>/dev/null
}

collect_source_logs_status () {
   local source_logs_file="$1"
   local source_status_file="$2"
   local version="$3"

   local source_log='binary'
   local source_status='binary log'
   if [ "$version" '<' "8.1" ]; then
      source_log='master';
      source_status='master';
   fi
   $CMD_MYSQL $EXT_ARGV -ss -e "SHOW ${source_log} LOGS" > "$source_logs_file" 2>/dev/null
   $CMD_MYSQL $EXT_ARGV -ss -e "SHOW ${source_status} STATUS" > "$source_status_file" 2>/dev/null
}

# Somewhat different from the others, this one joins the status we got earlier
collect_mysql_deferred_status () {
   local status_file="$1"
   collect_mysql_status > "$PT_TMPDIR/defer_gatherer"
   join "$status_file" "$PT_TMPDIR/defer_gatherer"
}

collect_internal_vars () {
   local mysqld_executables="${1:-""}"

   local FNV_64=""
   if $CMD_MYSQL $EXT_ARGV -e "SELECT FNV_64('a')" >/dev/null 2>&1; then
      FNV_64="Enabled";
   else
      FNV_64="Unknown";
   fi

   local now="$($CMD_MYSQL $EXT_ARGV -ss -e 'SELECT NOW()')"
   local user="$($CMD_MYSQL $EXT_ARGV -ss -e 'SELECT CURRENT_USER()')"
   local trigger_count=$($CMD_MYSQL $EXT_ARGV -ss -e "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TRIGGERS" 2>/dev/null)

   echo "pt-summary-internal-mysql_executable    $CMD_MYSQL"
   echo "pt-summary-internal-now    $now"
   echo "pt-summary-internal-user   $user"
   echo "pt-summary-internal-FNV_64   $FNV_64"
   echo "pt-summary-internal-trigger_count   $trigger_count"

   if [ -e "$mysqld_executables" ]; then
      local i=1
      while read executable; do
         echo "pt-summary-internal-mysqld_executable_${i}   $(has_symbols "$executable")"
         i=$(($i + 1))
      done < "$mysqld_executables"
   fi

   # jemalloc info
   local JEMALLOC_STATUS=''
   local GENERAL_JEMALLOC_STATUS=0
   local JEMALLOC_LOCATION=''

   for pid in $(pidof mysqld); do
      grep -qc jemalloc /proc/${pid}/environ || ldd $(which mysqld) 2>/dev/null | grep -qc jemalloc
      jemalloc_status=$?
      if [ $jemalloc_status = 1 ]; then
         echo "pt-summary-internal-jemalloc_enabled_for_pid_${pid}    0"
      else
         echo "pt-summary-internal-jemalloc_enabled_for_pid_${pid}    1"
         GENERAL_JEMALLOC_STATUS=1
      fi
   done

   if [ $GENERAL_JEMALLOC_STATUS -eq 1 ]; then
      JEMALLOC_LOCATION=$(find /usr/lib64/ /usr/lib/x86_64-linux-gnu /usr/lib -name "libjemalloc.*" 2>/dev/null | head -n 1)
      echo "pt-summary-internal-jemalloc_location    ${JEMALLOC_LOCATION}"
   fi
}

collect_keyring_plugins() {
    $CMD_MYSQL $EXT_ARGV --table -ss -e "SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE 'keyring%';"
}

collect_encrypted_tables() {
    $CMD_MYSQL $EXT_ARGV --table -ss -e "SELECT TABLE_SCHEMA, TABLE_NAME, CREATE_OPTIONS FROM INFORMATION_SCHEMA.TABLES WHERE CREATE_OPTIONS LIKE '%ENCRYPTION=_Y_%';"
}

collect_encrypted_tablespaces() {
    local version="$1"
# I_S.INNODB_[SYS_]TABLESPACES has a "flag" field. Encrypted tablespace has bit 14 set. You can check it with "flag & 8192".
# And seems like MySQL is capable of bitwise operations. https://dev.mysql.com/doc/refman/5.7/en/bit-functions.html
    if [ "$version" '<' "8.0" ]; then
        $CMD_MYSQL $EXT_ARGV --table -ss -e "SELECT SPACE, NAME, SPACE_TYPE from INFORMATION_SCHEMA.INNODB_SYS_TABLESPACES where FLAG&8192 = 8192;"
    else
        $CMD_MYSQL $EXT_ARGV --table -ss -e "SELECT SPACE, NAME, SPACE_TYPE from INFORMATION_SCHEMA.INNODB_TABLESPACES where FLAG&8192 = 8192;"
    fi
}

# Uses mysqldump and dumps the results to FILE.
# args and dbtodump are passed to mysqldump.
get_mysqldump_for () {
   local args="$1"
   local dbtodump="${2:-"--all-databases"}"

   $CMD_MYSQLDUMP $EXT_ARGV --no-data --skip-comments \
      --skip-add-locks --skip-add-drop-table --compact \
      --skip-lock-all-tables --skip-lock-tables --skip-set-charset \
      ${args} --databases $(local IFS=,; echo ${dbtodump})
}

# Returns a string with arguments to pass to mysqldump.
# Takes one argument, which should be a
get_mysqldump_args () {
   local file="$1"
   local trg_arg=""

   # If mysqldump supports triggers, then add options for routines.
   if $CMD_MYSQLDUMP --help --verbose 2>&1 | grep triggers >/dev/null; then
      # "mysqldump supports triggers"
      trg_arg="--routines"
   fi

   if [ "${trg_arg}" ]; then
      # Find out if there are any triggers.  If there are none, we will skip
      # that option to mysqldump, because when mysqldump checks for them, it
      # can take a long time, one table at a time.
      local triggers="--skip-triggers"
      local trg=$(get_var "pt-summary-internal-trigger_count" "$file" )
      if [ -n "${trg}" ] && [ "${trg}" -gt 0 ]; then
         triggers="--triggers"
      fi
      trg_arg="${trg_arg} ${triggers}";
   fi
   echo "${trg_arg}"
}

collect_mysqld_executables () {
   local mysqld_instances="$1"

   local ps_opt="cmd="
   if [ "$(uname -s)" = "Darwin" ]; then
      ps_opt="command="
   fi

   for pid in $( grep '/mysqld' "$mysqld_instances" | awk '/^.*[0-9]/{print $1}' ); do
      ps -o $ps_opt -p $pid | sed -e 's/^\(.*mysqld\) .*/\1/'
   done | sort -u
}

collect_mysql_info () {
   local dir="$1"

   collect_mysql_variables     > "$dir/mysql-variables"
   collect_mysql_status        > "$dir/mysql-status"
   collect_mysql_databases     > "$dir/mysql-databases"
   collect_mysql_plugins       > "$dir/mysql-plugins"
   collect_mysql_innodb_status > "$dir/innodb-status"
   collect_mysql_ndb_status    > "$dir/ndb-status"
   collect_mysql_processlist   > "$dir/mysql-processlist"
   collect_mysql_users         > "$dir/mysql-users"
   collect_mysql_roles         > "$dir/mysql-roles"
   collect_keyring_plugins     > "$dir/keyring-plugins"

   local mysql_version="$(get_var version "$dir/mysql-variables")"

   local replicas='replicas'
   local replica_name='replica'
   if [ "$mysql_version" '<' "8.1" ]; then
      replicas='slave-hosts'
      replica_name='slave'
   fi

   collect_mysql_replica_status $replica_name > "$dir/mysql-${replica_name}"
   collect_mysqld_instances   "$dir/mysql-variables"  > "$dir/mysqld-instances"
   collect_mysqld_executables "$dir/mysqld-instances" > "$dir/mysqld-executables"
   collect_mysql_show_replica_hosts "$mysql_version" > "$dir/mysql-${replicas}"

   local binlog="$(get_var log_bin "$dir/mysql-variables")"
   if [ "${binlog}" ]; then
      # "Got a binlog, going to get BINARY LOGS and BINARY LOG STATUS"
      local source_logs_file="mysql-binary-logs"
      local source_status_file="mysql-binary-log-status"

      if [ "$mysql_version" '<' "8.1" ]; then
         source_logs_file='mysql-master-logs'
         source_status_file='mysql-master-status'
      fi
      collect_source_logs_status "$dir/$source_logs_file" "$dir/$source_status_file" ${mysql_version}
   fi

   local uptime="$(get_var Uptime "$dir/mysql-status")"
   local current_time="$($CMD_MYSQL $EXT_ARGV -ss -e \
                         "SELECT LEFT(NOW() - INTERVAL ${uptime} SECOND, 16)")"

   local pid_file="$(get_var "pid_file" "$dir/mysql-variables")"
   local pid_file_exists=""
   [ -e "${pid_file}" ] && pid_file_exists=1
   echo "pt-summary-internal-pid_file_exists    $pid_file_exists" >> "$dir/mysql-variables"

   local port="$(get_var port "$dir/mysql-variables")"
   local cnf_file="$(find_my_cnf_file "$dir/mysqld-instances" ${port} ${pid_file})"

   [ -e "$cnf_file" ] && cat "$cnf_file" > "$dir/mysql-config-file"

   # TODO: Do these require a file of their own?
   echo "pt-summary-internal-current_time    $current_time" >> "$dir/mysql-variables"
   echo "pt-summary-internal-Config_File_path    $cnf_file" >> "$dir/mysql-variables"
   collect_internal_vars "$dir/mysqld-executables" >> "$dir/mysql-variables"

   # mysqldump schemas
   if [ "$OPT_DATABASES" -o "$OPT_ALL_DATABASES" ]; then
      local trg_arg="$(get_mysqldump_args "$dir/mysql-variables")"
      local dbs="${OPT_DATABASES:-""}"
      get_mysqldump_for "${trg_arg}" "$dbs" > "$dir/mysqldump"
   fi

   # encrypted tables and tablespaces
   if [ "${OPT_LIST_ENCRYPTED_TABLES}" = 'yes' ]; then
      collect_encrypted_tables                       > "$dir/encrypted-tables"
      collect_encrypted_tablespaces ${mysql_version} > "$dir/encrypted-tablespaces"
   fi

   # TODO: gather this data in the same format as normal: TS line, stats
   (
      sleep $OPT_SLEEP
      collect_mysql_deferred_status "$dir/mysql-status" > "$dir/mysql-status-defer"
   ) &
   _d "Forked child is $!"
}

# ###########################################################################
# End collect_mysql_info package
# ###########################################################################
