mirror of
https://github.com/percona/percona-toolkit.git
synced 2025-09-02 10:36:28 +00:00

pt-table-sync now uses up to 17 decimal digits when writing floating point numbers in the generated SQL statements. This is necessary to prevent unintended data changes.
5824 lines
168 KiB
Perl
Executable File
5824 lines
168 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# This program is part of Percona Toolkit: http://www.percona.com/software/
|
|
# See "COPYRIGHT, LICENSE, AND WARRANTY" at the end of this file for legal
|
|
# notices and disclaimers.
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
|
|
# This tool is "fat-packed": most of its dependent modules are embedded
|
|
# in this file. Setting %INC to this file for each module makes Perl aware
|
|
# of this so it will not try to load the module from @INC. See the tool's
|
|
# documentation for a full list of dependencies.
|
|
BEGIN {
|
|
$INC{$_} = __FILE__ for map { (my $pkg = "$_.pm") =~ s!::!/!g; $pkg } (qw(
|
|
Percona::Toolkit
|
|
OptionParser
|
|
Lmo::Utils
|
|
Lmo::Meta
|
|
Lmo::Object
|
|
Lmo::Types
|
|
Lmo
|
|
VersionParser
|
|
Quoter
|
|
DSNParser
|
|
Cxn
|
|
Daemon
|
|
HTTP::Micro
|
|
VersionCheck
|
|
Runtime
|
|
));
|
|
}
|
|
|
|
# ###########################################################################
|
|
# Percona::Toolkit package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Percona/Toolkit.pm
|
|
# t/lib/Percona/Toolkit.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Percona::Toolkit;
|
|
|
|
our $VERSION = '3.7.0';
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use Carp qw(carp cluck);
|
|
use Data::Dumper qw();
|
|
|
|
require Exporter;
|
|
our @ISA = qw(Exporter);
|
|
our @EXPORT_OK = qw(
|
|
have_required_args
|
|
Dumper
|
|
_d
|
|
);
|
|
|
|
sub have_required_args {
|
|
my ($args, @required_args) = @_;
|
|
my $have_required_args = 1;
|
|
foreach my $arg ( @required_args ) {
|
|
if ( !defined $args->{$arg} ) {
|
|
$have_required_args = 0;
|
|
carp "Argument $arg is not defined";
|
|
}
|
|
}
|
|
cluck unless $have_required_args; # print backtrace
|
|
return $have_required_args;
|
|
}
|
|
|
|
sub Dumper {
|
|
local $Data::Dumper::Indent = 1;
|
|
local $Data::Dumper::Sortkeys = 1;
|
|
local $Data::Dumper::Quotekeys = 0;
|
|
Data::Dumper::Dumper(@_);
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Percona::Toolkit package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# OptionParser package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/OptionParser.pm
|
|
# t/lib/OptionParser.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package OptionParser;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use List::Util qw(max);
|
|
use Getopt::Long;
|
|
use Data::Dumper;
|
|
|
|
my $POD_link_re = '[LC]<"?([^">]+)"?>';
|
|
|
|
sub new {
|
|
my ( $class, %args ) = @_;
|
|
my @required_args = qw();
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
|
|
my ($program_name) = $PROGRAM_NAME =~ m/([.A-Za-z-]+)$/;
|
|
$program_name ||= $PROGRAM_NAME;
|
|
my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
|
|
|
|
my %attributes = (
|
|
'type' => 1,
|
|
'short form' => 1,
|
|
'group' => 1,
|
|
'default' => 1,
|
|
'cumulative' => 1,
|
|
'negatable' => 1,
|
|
'repeatable' => 1, # means it can be specified more than once
|
|
);
|
|
|
|
my $self = {
|
|
head1 => 'OPTIONS', # These args are used internally
|
|
skip_rules => 0, # to instantiate another Option-
|
|
item => '--(.*)', # Parser obj that parses the
|
|
attributes => \%attributes, # DSN OPTIONS section. Tools
|
|
parse_attributes => \&_parse_attribs, # don't tinker with these args.
|
|
|
|
%args,
|
|
|
|
strict => 1, # disabled by a special rule
|
|
program_name => $program_name,
|
|
opts => {},
|
|
got_opts => 0,
|
|
short_opts => {},
|
|
defaults => {},
|
|
groups => {},
|
|
allowed_groups => {},
|
|
errors => [],
|
|
rules => [], # desc of rules for --help
|
|
mutex => [], # rule: opts are mutually exclusive
|
|
atleast1 => [], # rule: at least one opt is required
|
|
disables => {}, # rule: opt disables other opts
|
|
defaults_to => {}, # rule: opt defaults to value of other opt
|
|
DSNParser => undef,
|
|
default_files => [
|
|
"/etc/percona-toolkit/percona-toolkit.conf",
|
|
"/etc/percona-toolkit/$program_name.conf",
|
|
"$home/.percona-toolkit.conf",
|
|
"$home/.$program_name.conf",
|
|
],
|
|
types => {
|
|
string => 's', # standard Getopt type
|
|
int => 'i', # standard Getopt type
|
|
float => 'f', # standard Getopt type
|
|
Hash => 'H', # hash, formed from a comma-separated list
|
|
hash => 'h', # hash as above, but only if a value is given
|
|
Array => 'A', # array, similar to Hash
|
|
array => 'a', # array, similar to hash
|
|
DSN => 'd', # DSN
|
|
size => 'z', # size with kMG suffix (powers of 2^10)
|
|
time => 'm', # time, with an optional suffix of s/h/m/d
|
|
},
|
|
};
|
|
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub get_specs {
|
|
my ( $self, $file ) = @_;
|
|
$file ||= $self->{file} || __FILE__;
|
|
my @specs = $self->_pod_to_specs($file);
|
|
$self->_parse_specs(@specs);
|
|
|
|
open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
|
|
my $contents = do { local $/ = undef; <$fh> };
|
|
close $fh;
|
|
if ( $contents =~ m/^=head1 DSN OPTIONS/m ) {
|
|
PTDEBUG && _d('Parsing DSN OPTIONS');
|
|
my $dsn_attribs = {
|
|
dsn => 1,
|
|
copy => 1,
|
|
};
|
|
my $parse_dsn_attribs = sub {
|
|
my ( $self, $option, $attribs ) = @_;
|
|
map {
|
|
my $val = $attribs->{$_};
|
|
if ( $val ) {
|
|
$val = $val eq 'yes' ? 1
|
|
: $val eq 'no' ? 0
|
|
: $val;
|
|
$attribs->{$_} = $val;
|
|
}
|
|
} keys %$attribs;
|
|
return {
|
|
key => $option,
|
|
%$attribs,
|
|
};
|
|
};
|
|
my $dsn_o = new OptionParser(
|
|
description => 'DSN OPTIONS',
|
|
head1 => 'DSN OPTIONS',
|
|
dsn => 0, # XXX don't infinitely recurse!
|
|
item => '\* (.)', # key opts are a single character
|
|
skip_rules => 1, # no rules before opts
|
|
attributes => $dsn_attribs,
|
|
parse_attributes => $parse_dsn_attribs,
|
|
);
|
|
my @dsn_opts = map {
|
|
my $opts = {
|
|
key => $_->{spec}->{key},
|
|
dsn => $_->{spec}->{dsn},
|
|
copy => $_->{spec}->{copy},
|
|
desc => $_->{desc},
|
|
};
|
|
$opts;
|
|
} $dsn_o->_pod_to_specs($file);
|
|
$self->{DSNParser} = DSNParser->new(opts => \@dsn_opts);
|
|
}
|
|
|
|
if ( $contents =~ m/^=head1 VERSION\n\n^(.+)$/m ) {
|
|
$self->{version} = $1;
|
|
PTDEBUG && _d($self->{version});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub DSNParser {
|
|
my ( $self ) = @_;
|
|
return $self->{DSNParser};
|
|
};
|
|
|
|
sub get_defaults_files {
|
|
my ( $self ) = @_;
|
|
return @{$self->{default_files}};
|
|
}
|
|
|
|
sub _pod_to_specs {
|
|
my ( $self, $file ) = @_;
|
|
$file ||= $self->{file} || __FILE__;
|
|
open my $fh, '<', $file or die "Cannot open $file: $OS_ERROR";
|
|
|
|
my @specs = ();
|
|
my @rules = ();
|
|
my $para;
|
|
|
|
local $INPUT_RECORD_SEPARATOR = '';
|
|
while ( $para = <$fh> ) {
|
|
next unless $para =~ m/^=head1 $self->{head1}/;
|
|
last;
|
|
}
|
|
|
|
while ( $para = <$fh> ) {
|
|
last if $para =~ m/^=over/;
|
|
next if $self->{skip_rules};
|
|
chomp $para;
|
|
$para =~ s/\s+/ /g;
|
|
$para =~ s/$POD_link_re/$1/go;
|
|
PTDEBUG && _d('Option rule:', $para);
|
|
push @rules, $para;
|
|
}
|
|
|
|
die "POD has no $self->{head1} section" unless $para;
|
|
|
|
do {
|
|
if ( my ($option) = $para =~ m/^=item $self->{item}/ ) {
|
|
chomp $para;
|
|
PTDEBUG && _d($para);
|
|
my %attribs;
|
|
|
|
$para = <$fh>; # read next paragraph, possibly attributes
|
|
|
|
if ( $para =~ m/: / ) { # attributes
|
|
$para =~ s/\s+\Z//g;
|
|
%attribs = map {
|
|
my ( $attrib, $val) = split(/: /, $_);
|
|
die "Unrecognized attribute for --$option: $attrib"
|
|
unless $self->{attributes}->{$attrib};
|
|
($attrib, $val);
|
|
} split(/; /, $para);
|
|
if ( $attribs{'short form'} ) {
|
|
$attribs{'short form'} =~ s/-//;
|
|
}
|
|
$para = <$fh>; # read next paragraph, probably short help desc
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Option has no attributes');
|
|
}
|
|
|
|
$para =~ s/\s+\Z//g;
|
|
$para =~ s/\s+/ /g;
|
|
$para =~ s/$POD_link_re/$1/go;
|
|
|
|
$para =~ s/\.(?:\n.*| [A-Z].*|\Z)//s;
|
|
PTDEBUG && _d('Short help:', $para);
|
|
|
|
die "No description after option spec $option" if $para =~ m/^=item/;
|
|
|
|
if ( my ($base_option) = $option =~ m/^\[no\](.*)/ ) {
|
|
$option = $base_option;
|
|
$attribs{'negatable'} = 1;
|
|
}
|
|
|
|
push @specs, {
|
|
spec => $self->{parse_attributes}->($self, $option, \%attribs),
|
|
desc => $para
|
|
. (defined $attribs{default} ? " (default $attribs{default})" : ''),
|
|
group => ($attribs{'group'} ? $attribs{'group'} : 'default'),
|
|
attributes => \%attribs
|
|
};
|
|
}
|
|
while ( $para = <$fh> ) {
|
|
last unless $para;
|
|
if ( $para =~ m/^=head1/ ) {
|
|
$para = undef; # Can't 'last' out of a do {} block.
|
|
last;
|
|
}
|
|
last if $para =~ m/^=item /;
|
|
}
|
|
} while ( $para );
|
|
|
|
die "No valid specs in $self->{head1}" unless @specs;
|
|
|
|
close $fh;
|
|
return @specs, @rules;
|
|
}
|
|
|
|
sub _parse_specs {
|
|
my ( $self, @specs ) = @_;
|
|
my %disables; # special rule that requires deferred checking
|
|
|
|
foreach my $opt ( @specs ) {
|
|
if ( ref $opt ) { # It's an option spec, not a rule.
|
|
PTDEBUG && _d('Parsing opt spec:',
|
|
map { ($_, '=>', $opt->{$_}) } keys %$opt);
|
|
|
|
my ( $long, $short ) = $opt->{spec} =~ m/^([\w-]+)(?:\|([^!+=]*))?/;
|
|
if ( !$long ) {
|
|
die "Cannot parse long option from spec $opt->{spec}";
|
|
}
|
|
$opt->{long} = $long;
|
|
|
|
die "Duplicate long option --$long" if exists $self->{opts}->{$long};
|
|
$self->{opts}->{$long} = $opt;
|
|
|
|
if ( length $long == 1 ) {
|
|
PTDEBUG && _d('Long opt', $long, 'looks like short opt');
|
|
$self->{short_opts}->{$long} = $long;
|
|
}
|
|
|
|
if ( $short ) {
|
|
die "Duplicate short option -$short"
|
|
if exists $self->{short_opts}->{$short};
|
|
$self->{short_opts}->{$short} = $long;
|
|
$opt->{short} = $short;
|
|
}
|
|
else {
|
|
$opt->{short} = undef;
|
|
}
|
|
|
|
$opt->{is_negatable} = $opt->{spec} =~ m/!/ ? 1 : 0;
|
|
$opt->{is_cumulative} = $opt->{spec} =~ m/\+/ ? 1 : 0;
|
|
$opt->{is_repeatable} = $opt->{attributes}->{repeatable} ? 1 : 0;
|
|
$opt->{is_required} = $opt->{desc} =~ m/required/ ? 1 : 0;
|
|
|
|
$opt->{group} ||= 'default';
|
|
$self->{groups}->{ $opt->{group} }->{$long} = 1;
|
|
|
|
$opt->{value} = undef;
|
|
$opt->{got} = 0;
|
|
|
|
my ( $type ) = $opt->{spec} =~ m/=(.)/;
|
|
$opt->{type} = $type;
|
|
PTDEBUG && _d($long, 'type:', $type);
|
|
|
|
|
|
$opt->{spec} =~ s/=./=s/ if ( $type && $type =~ m/[HhAadzm]/ );
|
|
|
|
if ( (my ($def) = $opt->{desc} =~ m/default\b(?: ([^)]+))?/) ) {
|
|
$self->{defaults}->{$long} = defined $def ? $def : 1;
|
|
PTDEBUG && _d($long, 'default:', $def);
|
|
}
|
|
|
|
if ( $long eq 'config' ) {
|
|
$self->{defaults}->{$long} = join(',', $self->get_defaults_files());
|
|
}
|
|
|
|
if ( (my ($dis) = $opt->{desc} =~ m/(disables .*)/) ) {
|
|
$disables{$long} = $dis;
|
|
PTDEBUG && _d('Deferring check of disables rule for', $opt, $dis);
|
|
}
|
|
|
|
$self->{opts}->{$long} = $opt;
|
|
}
|
|
else { # It's an option rule, not a spec.
|
|
PTDEBUG && _d('Parsing rule:', $opt);
|
|
push @{$self->{rules}}, $opt;
|
|
my @participants = $self->_get_participants($opt);
|
|
my $rule_ok = 0;
|
|
|
|
if ( $opt =~ m/mutually exclusive|one and only one/ ) {
|
|
$rule_ok = 1;
|
|
push @{$self->{mutex}}, \@participants;
|
|
PTDEBUG && _d(@participants, 'are mutually exclusive');
|
|
}
|
|
if ( $opt =~ m/at least one|one and only one/ ) {
|
|
$rule_ok = 1;
|
|
push @{$self->{atleast1}}, \@participants;
|
|
PTDEBUG && _d(@participants, 'require at least one');
|
|
}
|
|
if ( $opt =~ m/default to/ ) {
|
|
$rule_ok = 1;
|
|
$self->{defaults_to}->{$participants[0]} = $participants[1];
|
|
PTDEBUG && _d($participants[0], 'defaults to', $participants[1]);
|
|
}
|
|
if ( $opt =~ m/restricted to option groups/ ) {
|
|
$rule_ok = 1;
|
|
my ($groups) = $opt =~ m/groups ([\w\s\,]+)/;
|
|
my @groups = split(',', $groups);
|
|
%{$self->{allowed_groups}->{$participants[0]}} = map {
|
|
s/\s+//;
|
|
$_ => 1;
|
|
} @groups;
|
|
}
|
|
if( $opt =~ m/accepts additional command-line arguments/ ) {
|
|
$rule_ok = 1;
|
|
$self->{strict} = 0;
|
|
PTDEBUG && _d("Strict mode disabled by rule");
|
|
}
|
|
|
|
die "Unrecognized option rule: $opt" unless $rule_ok;
|
|
}
|
|
}
|
|
|
|
foreach my $long ( keys %disables ) {
|
|
my @participants = $self->_get_participants($disables{$long});
|
|
$self->{disables}->{$long} = \@participants;
|
|
PTDEBUG && _d('Option', $long, 'disables', @participants);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _get_participants {
|
|
my ( $self, $str ) = @_;
|
|
my @participants;
|
|
foreach my $long ( $str =~ m/--(?:\[no\])?([\w-]+)/g ) {
|
|
die "Option --$long does not exist while processing rule $str"
|
|
unless exists $self->{opts}->{$long};
|
|
push @participants, $long;
|
|
}
|
|
PTDEBUG && _d('Participants for', $str, ':', @participants);
|
|
return @participants;
|
|
}
|
|
|
|
sub opts {
|
|
my ( $self ) = @_;
|
|
my %opts = %{$self->{opts}};
|
|
return %opts;
|
|
}
|
|
|
|
sub short_opts {
|
|
my ( $self ) = @_;
|
|
my %short_opts = %{$self->{short_opts}};
|
|
return %short_opts;
|
|
}
|
|
|
|
sub set_defaults {
|
|
my ( $self, %defaults ) = @_;
|
|
$self->{defaults} = {};
|
|
foreach my $long ( keys %defaults ) {
|
|
die "Cannot set default for nonexistent option $long"
|
|
unless exists $self->{opts}->{$long};
|
|
$self->{defaults}->{$long} = $defaults{$long};
|
|
PTDEBUG && _d('Default val for', $long, ':', $defaults{$long});
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub get_defaults {
|
|
my ( $self ) = @_;
|
|
return $self->{defaults};
|
|
}
|
|
|
|
sub get_groups {
|
|
my ( $self ) = @_;
|
|
return $self->{groups};
|
|
}
|
|
|
|
sub _set_option {
|
|
my ( $self, $opt, $val ) = @_;
|
|
my $long = exists $self->{opts}->{$opt} ? $opt
|
|
: exists $self->{short_opts}->{$opt} ? $self->{short_opts}->{$opt}
|
|
: die "Getopt::Long gave a nonexistent option: $opt";
|
|
$opt = $self->{opts}->{$long};
|
|
if ( $opt->{is_cumulative} ) {
|
|
$opt->{value}++;
|
|
}
|
|
elsif ( ($opt->{type} || '') eq 's' && $val =~ m/^--?(.+)/ ) {
|
|
my $next_opt = $1;
|
|
if ( exists $self->{opts}->{$next_opt}
|
|
|| exists $self->{short_opts}->{$next_opt} ) {
|
|
$self->save_error("--$long requires a string value");
|
|
return;
|
|
}
|
|
else {
|
|
if ($opt->{is_repeatable}) {
|
|
push @{$opt->{value}} , $val;
|
|
}
|
|
else {
|
|
$opt->{value} = $val;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if ($opt->{is_repeatable}) {
|
|
push @{$opt->{value}} , $val;
|
|
}
|
|
else {
|
|
$opt->{value} = $val;
|
|
}
|
|
}
|
|
$opt->{got} = 1;
|
|
PTDEBUG && _d('Got option', $long, '=', $val);
|
|
}
|
|
|
|
sub get_opts {
|
|
my ( $self ) = @_;
|
|
|
|
foreach my $long ( keys %{$self->{opts}} ) {
|
|
$self->{opts}->{$long}->{got} = 0;
|
|
$self->{opts}->{$long}->{value}
|
|
= exists $self->{defaults}->{$long} ? $self->{defaults}->{$long}
|
|
: $self->{opts}->{$long}->{is_cumulative} ? 0
|
|
: undef;
|
|
}
|
|
$self->{got_opts} = 0;
|
|
|
|
$self->{errors} = [];
|
|
|
|
if ( @ARGV && $ARGV[0] =~/^--config=/ ) {
|
|
$ARGV[0] = substr($ARGV[0],9);
|
|
$ARGV[0] =~ s/^'(.*)'$/$1/;
|
|
$ARGV[0] =~ s/^"(.*)"$/$1/;
|
|
$self->_set_option('config', shift @ARGV);
|
|
}
|
|
if ( @ARGV && $ARGV[0] eq "--config" ) {
|
|
shift @ARGV;
|
|
$self->_set_option('config', shift @ARGV);
|
|
}
|
|
if ( $self->has('config') ) {
|
|
my @extra_args;
|
|
foreach my $filename ( split(',', $self->get('config')) ) {
|
|
eval {
|
|
push @extra_args, $self->_read_config_file($filename);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
if ( $self->got('config') ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
elsif ( PTDEBUG ) {
|
|
_d($EVAL_ERROR);
|
|
}
|
|
}
|
|
}
|
|
unshift @ARGV, @extra_args;
|
|
}
|
|
|
|
Getopt::Long::Configure('no_ignore_case', 'bundling');
|
|
GetOptions(
|
|
map { $_->{spec} => sub { $self->_set_option(@_); } }
|
|
grep { $_->{long} ne 'config' } # --config is handled specially above.
|
|
values %{$self->{opts}}
|
|
) or $self->save_error('Error parsing options');
|
|
|
|
if ( exists $self->{opts}->{version} && $self->{opts}->{version}->{got} ) {
|
|
if ( $self->{version} ) {
|
|
print $self->{version}, "\n";
|
|
exit 0;
|
|
}
|
|
else {
|
|
print "Error parsing version. See the VERSION section of the tool's documentation.\n";
|
|
exit 1;
|
|
}
|
|
}
|
|
|
|
if ( @ARGV && $self->{strict} ) {
|
|
$self->save_error("Unrecognized command-line options @ARGV");
|
|
}
|
|
|
|
foreach my $mutex ( @{$self->{mutex}} ) {
|
|
my @set = grep { $self->{opts}->{$_}->{got} } @$mutex;
|
|
if ( @set > 1 ) {
|
|
my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
|
|
@{$mutex}[ 0 .. scalar(@$mutex) - 2] )
|
|
. ' and --'.$self->{opts}->{$mutex->[-1]}->{long}
|
|
. ' are mutually exclusive.';
|
|
$self->save_error($err);
|
|
}
|
|
}
|
|
|
|
foreach my $required ( @{$self->{atleast1}} ) {
|
|
my @set = grep { $self->{opts}->{$_}->{got} } @$required;
|
|
if ( @set == 0 ) {
|
|
my $err = join(', ', map { "--$self->{opts}->{$_}->{long}" }
|
|
@{$required}[ 0 .. scalar(@$required) - 2] )
|
|
.' or --'.$self->{opts}->{$required->[-1]}->{long};
|
|
$self->save_error("Specify at least one of $err");
|
|
}
|
|
}
|
|
|
|
$self->_check_opts( keys %{$self->{opts}} );
|
|
$self->{got_opts} = 1;
|
|
return;
|
|
}
|
|
|
|
sub _check_opts {
|
|
my ( $self, @long ) = @_;
|
|
my $long_last = scalar @long;
|
|
while ( @long ) {
|
|
foreach my $i ( 0..$#long ) {
|
|
my $long = $long[$i];
|
|
next unless $long;
|
|
my $opt = $self->{opts}->{$long};
|
|
if ( $opt->{got} ) {
|
|
if ( exists $self->{disables}->{$long} ) {
|
|
my @disable_opts = @{$self->{disables}->{$long}};
|
|
map { $self->{opts}->{$_}->{value} = undef; } @disable_opts;
|
|
PTDEBUG && _d('Unset options', @disable_opts,
|
|
'because', $long,'disables them');
|
|
}
|
|
|
|
if ( exists $self->{allowed_groups}->{$long} ) {
|
|
|
|
my @restricted_groups = grep {
|
|
!exists $self->{allowed_groups}->{$long}->{$_}
|
|
} keys %{$self->{groups}};
|
|
|
|
my @restricted_opts;
|
|
foreach my $restricted_group ( @restricted_groups ) {
|
|
RESTRICTED_OPT:
|
|
foreach my $restricted_opt (
|
|
keys %{$self->{groups}->{$restricted_group}} )
|
|
{
|
|
next RESTRICTED_OPT if $restricted_opt eq $long;
|
|
push @restricted_opts, $restricted_opt
|
|
if $self->{opts}->{$restricted_opt}->{got};
|
|
}
|
|
}
|
|
|
|
if ( @restricted_opts ) {
|
|
my $err;
|
|
if ( @restricted_opts == 1 ) {
|
|
$err = "--$restricted_opts[0]";
|
|
}
|
|
else {
|
|
$err = join(', ',
|
|
map { "--$self->{opts}->{$_}->{long}" }
|
|
grep { $_ }
|
|
@restricted_opts[0..scalar(@restricted_opts) - 2]
|
|
)
|
|
. ' or --'.$self->{opts}->{$restricted_opts[-1]}->{long};
|
|
}
|
|
$self->save_error("--$long is not allowed with $err");
|
|
}
|
|
}
|
|
|
|
}
|
|
elsif ( $opt->{is_required} ) {
|
|
$self->save_error("Required option --$long must be specified");
|
|
}
|
|
|
|
$self->_validate_type($opt);
|
|
if ( $opt->{parsed} ) {
|
|
delete $long[$i];
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Temporarily failed to parse', $long);
|
|
}
|
|
}
|
|
|
|
die "Failed to parse options, possibly due to circular dependencies"
|
|
if @long == $long_last;
|
|
$long_last = @long;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _validate_type {
|
|
my ( $self, $opt ) = @_;
|
|
return unless $opt;
|
|
|
|
if ( !$opt->{type} ) {
|
|
$opt->{parsed} = 1;
|
|
return;
|
|
}
|
|
|
|
my $val = $opt->{value};
|
|
|
|
if ( $val && $opt->{type} eq 'm' ) { # type time
|
|
PTDEBUG && _d('Parsing option', $opt->{long}, 'as a time value');
|
|
my ( $prefix, $num, $suffix ) = $val =~ m/([+-]?)(\d+)([a-z])?$/;
|
|
if ( !$suffix ) {
|
|
my ( $s ) = $opt->{desc} =~ m/\(suffix (.)\)/;
|
|
$suffix = $s || 's';
|
|
PTDEBUG && _d('No suffix given; using', $suffix, 'for',
|
|
$opt->{long}, '(value:', $val, ')');
|
|
}
|
|
if ( $suffix =~ m/[smhd]/ ) {
|
|
$val = $suffix eq 's' ? $num # Seconds
|
|
: $suffix eq 'm' ? $num * 60 # Minutes
|
|
: $suffix eq 'h' ? $num * 3600 # Hours
|
|
: $num * 86400; # Days
|
|
$opt->{value} = ($prefix || '') . $val;
|
|
PTDEBUG && _d('Setting option', $opt->{long}, 'to', $val);
|
|
}
|
|
else {
|
|
$self->save_error("Invalid time suffix for --$opt->{long}");
|
|
}
|
|
}
|
|
elsif ( $val && $opt->{type} eq 'd' ) { # type DSN
|
|
PTDEBUG && _d('Parsing option', $opt->{long}, 'as a DSN');
|
|
my $prev = {};
|
|
my $from_key = $self->{defaults_to}->{ $opt->{long} };
|
|
if ( $from_key ) {
|
|
PTDEBUG && _d($opt->{long}, 'DSN copies from', $from_key, 'DSN');
|
|
if ( $self->{opts}->{$from_key}->{parsed} ) {
|
|
$prev = $self->{opts}->{$from_key}->{value};
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Cannot parse', $opt->{long}, 'until',
|
|
$from_key, 'parsed');
|
|
return;
|
|
}
|
|
}
|
|
my $defaults = $self->{DSNParser}->parse_options($self);
|
|
if (!$opt->{attributes}->{repeatable}) {
|
|
$opt->{value} = $self->{DSNParser}->parse($val, $prev, $defaults);
|
|
} else {
|
|
my $values = [];
|
|
for my $dsn_string (@$val) {
|
|
push @$values, $self->{DSNParser}->parse($dsn_string, $prev, $defaults);
|
|
}
|
|
$opt->{value} = $values;
|
|
}
|
|
}
|
|
elsif ( $val && $opt->{type} eq 'z' ) { # type size
|
|
PTDEBUG && _d('Parsing option', $opt->{long}, 'as a size value');
|
|
$self->_parse_size($opt, $val);
|
|
}
|
|
elsif ( $opt->{type} eq 'H' || (defined $val && $opt->{type} eq 'h') ) {
|
|
$opt->{value} = { map { $_ => 1 } split(/(?<!\\),\s*/, ($val || '')) };
|
|
}
|
|
elsif ( $opt->{type} eq 'A' || (defined $val && $opt->{type} eq 'a') ) {
|
|
$opt->{value} = [ split(/(?<!\\),\s*/, ($val || '')) ];
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Nothing to validate for option',
|
|
$opt->{long}, 'type', $opt->{type}, 'value', $val);
|
|
}
|
|
|
|
$opt->{parsed} = 1;
|
|
return;
|
|
}
|
|
|
|
sub get {
|
|
my ( $self, $opt ) = @_;
|
|
my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
|
|
die "Option $opt does not exist"
|
|
unless $long && exists $self->{opts}->{$long};
|
|
return $self->{opts}->{$long}->{value};
|
|
}
|
|
|
|
sub got {
|
|
my ( $self, $opt ) = @_;
|
|
my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
|
|
die "Option $opt does not exist"
|
|
unless $long && exists $self->{opts}->{$long};
|
|
return $self->{opts}->{$long}->{got};
|
|
}
|
|
|
|
sub has {
|
|
my ( $self, $opt ) = @_;
|
|
my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
|
|
return defined $long ? exists $self->{opts}->{$long} : 0;
|
|
}
|
|
|
|
sub set {
|
|
my ( $self, $opt, $val ) = @_;
|
|
my $long = (length $opt == 1 ? $self->{short_opts}->{$opt} : $opt);
|
|
die "Option $opt does not exist"
|
|
unless $long && exists $self->{opts}->{$long};
|
|
$self->{opts}->{$long}->{value} = $val;
|
|
return;
|
|
}
|
|
|
|
sub save_error {
|
|
my ( $self, $error ) = @_;
|
|
push @{$self->{errors}}, $error;
|
|
return;
|
|
}
|
|
|
|
sub errors {
|
|
my ( $self ) = @_;
|
|
return $self->{errors};
|
|
}
|
|
|
|
sub usage {
|
|
my ( $self ) = @_;
|
|
warn "No usage string is set" unless $self->{usage}; # XXX
|
|
return "Usage: " . ($self->{usage} || '') . "\n";
|
|
}
|
|
|
|
sub descr {
|
|
my ( $self ) = @_;
|
|
warn "No description string is set" unless $self->{description}; # XXX
|
|
my $descr = ($self->{description} || $self->{program_name} || '')
|
|
. " For more details, please use the --help option, "
|
|
. "or try 'perldoc $PROGRAM_NAME' "
|
|
. "for complete documentation.";
|
|
$descr = join("\n", $descr =~ m/(.{0,80})(?:\s+|$)/g)
|
|
unless $ENV{DONT_BREAK_LINES};
|
|
$descr =~ s/ +$//mg;
|
|
return $descr;
|
|
}
|
|
|
|
sub usage_or_errors {
|
|
my ( $self, $file, $return ) = @_;
|
|
$file ||= $self->{file} || __FILE__;
|
|
|
|
if ( !$self->{description} || !$self->{usage} ) {
|
|
PTDEBUG && _d("Getting description and usage from SYNOPSIS in", $file);
|
|
my %synop = $self->_parse_synopsis($file);
|
|
$self->{description} ||= $synop{description};
|
|
$self->{usage} ||= $synop{usage};
|
|
PTDEBUG && _d("Description:", $self->{description},
|
|
"\nUsage:", $self->{usage});
|
|
}
|
|
|
|
if ( $self->{opts}->{help}->{got} ) {
|
|
print $self->print_usage() or die "Cannot print usage: $OS_ERROR";
|
|
exit 0 unless $return;
|
|
}
|
|
elsif ( scalar @{$self->{errors}} ) {
|
|
print $self->print_errors() or die "Cannot print errors: $OS_ERROR";
|
|
exit 1 unless $return;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub print_errors {
|
|
my ( $self ) = @_;
|
|
my $usage = $self->usage() . "\n";
|
|
if ( (my @errors = @{$self->{errors}}) ) {
|
|
$usage .= join("\n * ", 'Errors in command-line arguments:', @errors)
|
|
. "\n";
|
|
}
|
|
return $usage . "\n" . $self->descr();
|
|
}
|
|
|
|
sub print_usage {
|
|
my ( $self ) = @_;
|
|
die "Run get_opts() before print_usage()" unless $self->{got_opts};
|
|
my @opts = values %{$self->{opts}};
|
|
|
|
my $maxl = max(
|
|
map {
|
|
length($_->{long}) # option long name
|
|
+ ($_->{is_negatable} ? 4 : 0) # "[no]" if opt is negatable
|
|
+ ($_->{type} ? 2 : 0) # "=x" where x is the opt type
|
|
}
|
|
@opts);
|
|
|
|
my $maxs = max(0,
|
|
map {
|
|
length($_)
|
|
+ ($self->{opts}->{$_}->{is_negatable} ? 4 : 0)
|
|
+ ($self->{opts}->{$_}->{type} ? 2 : 0)
|
|
}
|
|
values %{$self->{short_opts}});
|
|
|
|
my $lcol = max($maxl, ($maxs + 3));
|
|
my $rcol = 80 - $lcol - 6;
|
|
my $rpad = ' ' x ( 80 - $rcol );
|
|
|
|
$maxs = max($lcol - 3, $maxs);
|
|
|
|
my $usage = $self->descr() . "\n" . $self->usage();
|
|
|
|
my @groups = reverse sort grep { $_ ne 'default'; } keys %{$self->{groups}};
|
|
push @groups, 'default';
|
|
|
|
foreach my $group ( reverse @groups ) {
|
|
$usage .= "\n".($group eq 'default' ? 'Options' : $group).":\n\n";
|
|
foreach my $opt (
|
|
sort { $a->{long} cmp $b->{long} }
|
|
grep { $_->{group} eq $group }
|
|
@opts )
|
|
{
|
|
my $long = $opt->{is_negatable} ? "[no]$opt->{long}" : $opt->{long};
|
|
my $short = $opt->{short};
|
|
my $desc = $opt->{desc};
|
|
|
|
$long .= $opt->{type} ? "=$opt->{type}" : "";
|
|
|
|
if ( $opt->{type} && $opt->{type} eq 'm' ) {
|
|
my ($s) = $desc =~ m/\(suffix (.)\)/;
|
|
$s ||= 's';
|
|
$desc =~ s/\s+\(suffix .\)//;
|
|
$desc .= ". Optional suffix s=seconds, m=minutes, h=hours, "
|
|
. "d=days; if no suffix, $s is used.";
|
|
}
|
|
$desc = join("\n$rpad", grep { $_ } $desc =~ m/(.{0,$rcol}(?!\W))(?:\s+|(?<=\W)|$)/g);
|
|
$desc =~ s/ +$//mg;
|
|
if ( $short ) {
|
|
$usage .= sprintf(" --%-${maxs}s -%s %s\n", $long, $short, $desc);
|
|
}
|
|
else {
|
|
$usage .= sprintf(" --%-${lcol}s %s\n", $long, $desc);
|
|
}
|
|
}
|
|
}
|
|
|
|
$usage .= "\nOption types: s=string, i=integer, f=float, h/H/a/A=comma-separated list, d=DSN, z=size, m=time\n";
|
|
|
|
if ( (my @rules = @{$self->{rules}}) ) {
|
|
$usage .= "\nRules:\n\n";
|
|
$usage .= join("\n", map { " $_" } @rules) . "\n";
|
|
}
|
|
if ( $self->{DSNParser} ) {
|
|
$usage .= "\n" . $self->{DSNParser}->usage();
|
|
}
|
|
$usage .= "\nOptions and values after processing arguments:\n\n";
|
|
foreach my $opt ( sort { $a->{long} cmp $b->{long} } @opts ) {
|
|
my $val = $opt->{value};
|
|
my $type = $opt->{type} || '';
|
|
my $bool = $opt->{spec} =~ m/^[\w-]+(?:\|[\w-])?!?$/;
|
|
$val = $bool ? ( $val ? 'TRUE' : 'FALSE' )
|
|
: !defined $val ? '(No value)'
|
|
: $type eq 'd' ? $self->{DSNParser}->as_string($val)
|
|
: $type =~ m/H|h/ ? join(',', sort keys %$val)
|
|
: $type =~ m/A|a/ ? join(',', @$val)
|
|
: $val;
|
|
$usage .= sprintf(" --%-${lcol}s %s\n", $opt->{long}, $val);
|
|
}
|
|
return $usage;
|
|
}
|
|
|
|
sub prompt_noecho {
|
|
shift @_ if ref $_[0] eq __PACKAGE__;
|
|
my ( $prompt ) = @_;
|
|
local $OUTPUT_AUTOFLUSH = 1;
|
|
print STDERR $prompt
|
|
or die "Cannot print: $OS_ERROR";
|
|
my $response;
|
|
eval {
|
|
require Term::ReadKey;
|
|
Term::ReadKey::ReadMode('noecho');
|
|
chomp($response = <STDIN>);
|
|
Term::ReadKey::ReadMode('normal');
|
|
print "\n"
|
|
or die "Cannot print: $OS_ERROR";
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
die "Cannot read response; is Term::ReadKey installed? $EVAL_ERROR";
|
|
}
|
|
return $response;
|
|
}
|
|
|
|
sub _read_config_file {
|
|
my ( $self, $filename ) = @_;
|
|
open my $fh, "<", $filename or die "Cannot open $filename: $OS_ERROR\n";
|
|
my @args;
|
|
my $prefix = '--';
|
|
my $parse = 1;
|
|
|
|
LINE:
|
|
while ( my $line = <$fh> ) {
|
|
chomp $line;
|
|
next LINE if $line =~ m/^\s*(?:\#|\;|$)/;
|
|
$line =~ s/\s+#.*$//g;
|
|
$line =~ s/^\s+|\s+$//g;
|
|
if ( $line eq '--' ) {
|
|
$prefix = '';
|
|
$parse = 0;
|
|
next LINE;
|
|
}
|
|
|
|
if ( $parse
|
|
&& !$self->has('version-check')
|
|
&& $line =~ /version-check/
|
|
) {
|
|
next LINE;
|
|
}
|
|
|
|
if ( $parse
|
|
&& (my($opt, $arg) = $line =~ m/^\s*([^=\s]+?)(?:\s*=\s*(.*?)\s*)?$/)
|
|
) {
|
|
push @args, grep { defined $_ } ("$prefix$opt", $arg);
|
|
}
|
|
elsif ( $line =~ m/./ ) {
|
|
push @args, $line;
|
|
}
|
|
else {
|
|
die "Syntax error in file $filename at line $INPUT_LINE_NUMBER";
|
|
}
|
|
}
|
|
close $fh;
|
|
return @args;
|
|
}
|
|
|
|
sub read_para_after {
|
|
my ( $self, $file, $regex ) = @_;
|
|
open my $fh, "<", $file or die "Can't open $file: $OS_ERROR";
|
|
local $INPUT_RECORD_SEPARATOR = '';
|
|
my $para;
|
|
while ( $para = <$fh> ) {
|
|
next unless $para =~ m/^=pod$/m;
|
|
last;
|
|
}
|
|
while ( $para = <$fh> ) {
|
|
next unless $para =~ m/$regex/;
|
|
last;
|
|
}
|
|
$para = <$fh>;
|
|
chomp($para);
|
|
close $fh or die "Can't close $file: $OS_ERROR";
|
|
return $para;
|
|
}
|
|
|
|
sub clone {
|
|
my ( $self ) = @_;
|
|
|
|
my %clone = map {
|
|
my $hashref = $self->{$_};
|
|
my $val_copy = {};
|
|
foreach my $key ( keys %$hashref ) {
|
|
my $ref = ref $hashref->{$key};
|
|
$val_copy->{$key} = !$ref ? $hashref->{$key}
|
|
: $ref eq 'HASH' ? { %{$hashref->{$key}} }
|
|
: $ref eq 'ARRAY' ? [ @{$hashref->{$key}} ]
|
|
: $hashref->{$key};
|
|
}
|
|
$_ => $val_copy;
|
|
} qw(opts short_opts defaults);
|
|
|
|
foreach my $scalar ( qw(got_opts) ) {
|
|
$clone{$scalar} = $self->{$scalar};
|
|
}
|
|
|
|
return bless \%clone;
|
|
}
|
|
|
|
sub _parse_size {
|
|
my ( $self, $opt, $val ) = @_;
|
|
|
|
if ( lc($val || '') eq 'null' ) {
|
|
PTDEBUG && _d('NULL size for', $opt->{long});
|
|
$opt->{value} = 'null';
|
|
return;
|
|
}
|
|
|
|
my %factor_for = (k => 1_024, M => 1_048_576, G => 1_073_741_824);
|
|
my ($pre, $num, $factor) = $val =~ m/^([+-])?(\d+)([kMG])?$/;
|
|
if ( defined $num ) {
|
|
if ( $factor ) {
|
|
$num *= $factor_for{$factor};
|
|
PTDEBUG && _d('Setting option', $opt->{y},
|
|
'to num', $num, '* factor', $factor);
|
|
}
|
|
$opt->{value} = ($pre || '') . $num;
|
|
}
|
|
else {
|
|
$self->save_error("Invalid size for --$opt->{long}: $val");
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub _parse_attribs {
|
|
my ( $self, $option, $attribs ) = @_;
|
|
my $types = $self->{types};
|
|
return $option
|
|
. ($attribs->{'short form'} ? '|' . $attribs->{'short form'} : '' )
|
|
. ($attribs->{'negatable'} ? '!' : '' )
|
|
. ($attribs->{'cumulative'} ? '+' : '' )
|
|
. ($attribs->{'type'} ? '=' . $types->{$attribs->{type}} : '' );
|
|
}
|
|
|
|
sub _parse_synopsis {
|
|
my ( $self, $file ) = @_;
|
|
$file ||= $self->{file} || __FILE__;
|
|
PTDEBUG && _d("Parsing SYNOPSIS in", $file);
|
|
|
|
local $INPUT_RECORD_SEPARATOR = ''; # read paragraphs
|
|
open my $fh, "<", $file or die "Cannot open $file: $OS_ERROR";
|
|
my $para;
|
|
1 while defined($para = <$fh>) && $para !~ m/^=head1 SYNOPSIS/;
|
|
die "$file does not contain a SYNOPSIS section" unless $para;
|
|
my @synop;
|
|
for ( 1..2 ) { # 1 for the usage, 2 for the description
|
|
my $para = <$fh>;
|
|
push @synop, $para;
|
|
}
|
|
close $fh;
|
|
PTDEBUG && _d("Raw SYNOPSIS text:", @synop);
|
|
my ($usage, $desc) = @synop;
|
|
die "The SYNOPSIS section in $file is not formatted properly"
|
|
unless $usage && $desc;
|
|
|
|
$usage =~ s/^\s*Usage:\s+(.+)/$1/;
|
|
chomp $usage;
|
|
|
|
$desc =~ s/\n/ /g;
|
|
$desc =~ s/\s{2,}/ /g;
|
|
$desc =~ s/\. ([A-Z][a-z])/. $1/g;
|
|
$desc =~ s/\s+$//;
|
|
|
|
return (
|
|
description => $desc,
|
|
usage => $usage,
|
|
);
|
|
};
|
|
|
|
sub set_vars {
|
|
my ($self, $file) = @_;
|
|
$file ||= $self->{file} || __FILE__;
|
|
|
|
my %user_vars;
|
|
my $user_vars = $self->has('set-vars') ? $self->get('set-vars') : undef;
|
|
if ( $user_vars ) {
|
|
foreach my $var_val ( @$user_vars ) {
|
|
my ($var, $val) = $var_val =~ m/([^\s=]+)=(\S+)/;
|
|
die "Invalid --set-vars value: $var_val\n" unless $var && defined $val;
|
|
$user_vars{$var} = {
|
|
val => $val,
|
|
default => 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
my %default_vars;
|
|
my $default_vars = $self->read_para_after($file, qr/MAGIC_set_vars/);
|
|
if ( $default_vars ) {
|
|
%default_vars = map {
|
|
my $var_val = $_;
|
|
my ($var, $val) = $var_val =~ m/([^\s=]+)=(\S+)/;
|
|
die "Invalid --set-vars value: $var_val\n" unless $var && defined $val;
|
|
$var => {
|
|
val => $val,
|
|
default => 1,
|
|
};
|
|
} split("\n", $default_vars);
|
|
}
|
|
|
|
my %vars = (
|
|
%default_vars, # first the tool's defaults
|
|
%user_vars, # then the user's which overwrite the defaults
|
|
);
|
|
PTDEBUG && _d('--set-vars:', Dumper(\%vars));
|
|
return \%vars;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
if ( PTDEBUG ) {
|
|
print STDERR '# ', $^X, ' ', $], "\n";
|
|
if ( my $uname = `uname -a` ) {
|
|
$uname =~ s/\s+/ /g;
|
|
print STDERR "# $uname\n";
|
|
}
|
|
print STDERR '# Arguments: ',
|
|
join(' ', map { my $a = "_[$_]_"; $a =~ s/\n/\n# /g; $a; } @ARGV), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End OptionParser package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Lmo::Utils package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Lmo/Utils.pm
|
|
# t/lib/Lmo/Utils.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Lmo::Utils;
|
|
|
|
use strict;
|
|
use warnings qw( FATAL all );
|
|
require Exporter;
|
|
our (@ISA, @EXPORT, @EXPORT_OK);
|
|
|
|
BEGIN {
|
|
@ISA = qw(Exporter);
|
|
@EXPORT = @EXPORT_OK = qw(
|
|
_install_coderef
|
|
_unimport_coderefs
|
|
_glob_for
|
|
_stash_for
|
|
);
|
|
}
|
|
|
|
{
|
|
no strict 'refs';
|
|
sub _glob_for {
|
|
return \*{shift()}
|
|
}
|
|
|
|
sub _stash_for {
|
|
return \%{ shift() . "::" };
|
|
}
|
|
}
|
|
|
|
sub _install_coderef {
|
|
my ($to, $code) = @_;
|
|
|
|
return *{ _glob_for $to } = $code;
|
|
}
|
|
|
|
sub _unimport_coderefs {
|
|
my ($target, @names) = @_;
|
|
return unless @names;
|
|
my $stash = _stash_for($target);
|
|
foreach my $name (@names) {
|
|
if ($stash->{$name} and defined(&{$stash->{$name}})) {
|
|
delete $stash->{$name};
|
|
}
|
|
}
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Lmo::Utils package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Lmo::Meta package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Lmo/Meta.pm
|
|
# t/lib/Lmo/Meta.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Lmo::Meta;
|
|
use strict;
|
|
use warnings qw( FATAL all );
|
|
|
|
my %metadata_for;
|
|
|
|
sub new {
|
|
my $class = shift;
|
|
return bless { @_ }, $class
|
|
}
|
|
|
|
sub metadata_for {
|
|
my $self = shift;
|
|
my ($class) = @_;
|
|
|
|
return $metadata_for{$class} ||= {};
|
|
}
|
|
|
|
sub class { shift->{class} }
|
|
|
|
sub attributes {
|
|
my $self = shift;
|
|
return keys %{$self->metadata_for($self->class)}
|
|
}
|
|
|
|
sub attributes_for_new {
|
|
my $self = shift;
|
|
my @attributes;
|
|
|
|
my $class_metadata = $self->metadata_for($self->class);
|
|
while ( my ($attr, $meta) = each %$class_metadata ) {
|
|
if ( exists $meta->{init_arg} ) {
|
|
push @attributes, $meta->{init_arg}
|
|
if defined $meta->{init_arg};
|
|
}
|
|
else {
|
|
push @attributes, $attr;
|
|
}
|
|
}
|
|
return @attributes;
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Lmo::Meta package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Lmo::Object package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Lmo/Object.pm
|
|
# t/lib/Lmo/Object.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Lmo::Object;
|
|
|
|
use strict;
|
|
use warnings qw( FATAL all );
|
|
|
|
use Carp ();
|
|
use Scalar::Util qw(blessed);
|
|
|
|
use Lmo::Meta;
|
|
use Lmo::Utils qw(_glob_for);
|
|
|
|
sub new {
|
|
my $class = shift;
|
|
my $args = $class->BUILDARGS(@_);
|
|
|
|
my $class_metadata = Lmo::Meta->metadata_for($class);
|
|
|
|
my @args_to_delete;
|
|
while ( my ($attr, $meta) = each %$class_metadata ) {
|
|
next unless exists $meta->{init_arg};
|
|
my $init_arg = $meta->{init_arg};
|
|
|
|
if ( defined $init_arg ) {
|
|
$args->{$attr} = delete $args->{$init_arg};
|
|
}
|
|
else {
|
|
push @args_to_delete, $attr;
|
|
}
|
|
}
|
|
|
|
delete $args->{$_} for @args_to_delete;
|
|
|
|
for my $attribute ( keys %$args ) {
|
|
if ( my $coerce = $class_metadata->{$attribute}{coerce} ) {
|
|
$args->{$attribute} = $coerce->($args->{$attribute});
|
|
}
|
|
if ( my $isa_check = $class_metadata->{$attribute}{isa} ) {
|
|
my ($check_name, $check_sub) = @$isa_check;
|
|
$check_sub->($args->{$attribute});
|
|
}
|
|
}
|
|
|
|
while ( my ($attribute, $meta) = each %$class_metadata ) {
|
|
next unless $meta->{required};
|
|
Carp::confess("Attribute ($attribute) is required for $class")
|
|
if ! exists $args->{$attribute}
|
|
}
|
|
|
|
my $self = bless $args, $class;
|
|
|
|
my @build_subs;
|
|
my $linearized_isa = mro::get_linear_isa($class);
|
|
|
|
for my $isa_class ( @$linearized_isa ) {
|
|
unshift @build_subs, *{ _glob_for "${isa_class}::BUILD" }{CODE};
|
|
}
|
|
my @args = %$args;
|
|
for my $sub (grep { defined($_) && exists &$_ } @build_subs) {
|
|
$sub->( $self, @args);
|
|
}
|
|
return $self;
|
|
}
|
|
|
|
sub BUILDARGS {
|
|
shift; # No need for the classname
|
|
if ( @_ == 1 && ref($_[0]) ) {
|
|
Carp::confess("Single parameters to new() must be a HASH ref, not $_[0]")
|
|
unless ref($_[0]) eq ref({});
|
|
return {%{$_[0]}} # We want a new reference, always
|
|
}
|
|
else {
|
|
return { @_ };
|
|
}
|
|
}
|
|
|
|
sub meta {
|
|
my $class = shift;
|
|
$class = Scalar::Util::blessed($class) || $class;
|
|
return Lmo::Meta->new(class => $class);
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Lmo::Object package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Lmo::Types package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Lmo/Types.pm
|
|
# t/lib/Lmo/Types.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Lmo::Types;
|
|
|
|
use strict;
|
|
use warnings qw( FATAL all );
|
|
|
|
use Carp ();
|
|
use Scalar::Util qw(looks_like_number blessed);
|
|
|
|
|
|
our %TYPES = (
|
|
Bool => sub { !$_[0] || (defined $_[0] && looks_like_number($_[0]) && $_[0] == 1) },
|
|
Num => sub { defined $_[0] && looks_like_number($_[0]) },
|
|
Int => sub { defined $_[0] && looks_like_number($_[0]) && $_[0] == int($_[0]) },
|
|
Str => sub { defined $_[0] },
|
|
Object => sub { defined $_[0] && blessed($_[0]) },
|
|
FileHandle => sub { local $@; require IO::Handle; fileno($_[0]) && $_[0]->opened },
|
|
|
|
map {
|
|
my $type = /R/ ? $_ : uc $_;
|
|
$_ . "Ref" => sub { ref $_[0] eq $type }
|
|
} qw(Array Code Hash Regexp Glob Scalar)
|
|
);
|
|
|
|
sub check_type_constraints {
|
|
my ($attribute, $type_check, $check_name, $val) = @_;
|
|
( ref($type_check) eq 'CODE'
|
|
? $type_check->($val)
|
|
: (ref $val eq $type_check
|
|
|| ($val && $val eq $type_check)
|
|
|| (exists $TYPES{$type_check} && $TYPES{$type_check}->($val)))
|
|
)
|
|
|| Carp::confess(
|
|
qq<Attribute ($attribute) does not pass the type constraint because: >
|
|
. qq<Validation failed for '$check_name' with value >
|
|
. (defined $val ? Lmo::Dumper($val) : 'undef') )
|
|
}
|
|
|
|
sub _nested_constraints {
|
|
my ($attribute, $aggregate_type, $type) = @_;
|
|
|
|
my $inner_types;
|
|
if ( $type =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) {
|
|
$inner_types = _nested_constraints($1, $2);
|
|
}
|
|
else {
|
|
$inner_types = $TYPES{$type};
|
|
}
|
|
|
|
if ( $aggregate_type eq 'ArrayRef' ) {
|
|
return sub {
|
|
my ($val) = @_;
|
|
return unless ref($val) eq ref([]);
|
|
|
|
if ($inner_types) {
|
|
for my $value ( @{$val} ) {
|
|
return unless $inner_types->($value)
|
|
}
|
|
}
|
|
else {
|
|
for my $value ( @{$val} ) {
|
|
return unless $value && ($value eq $type
|
|
|| (Scalar::Util::blessed($value) && $value->isa($type)));
|
|
}
|
|
}
|
|
return 1;
|
|
};
|
|
}
|
|
elsif ( $aggregate_type eq 'Maybe' ) {
|
|
return sub {
|
|
my ($value) = @_;
|
|
return 1 if ! defined($value);
|
|
if ($inner_types) {
|
|
return unless $inner_types->($value)
|
|
}
|
|
else {
|
|
return unless $value eq $type
|
|
|| (Scalar::Util::blessed($value) && $value->isa($type));
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
else {
|
|
Carp::confess("Nested aggregate types are only implemented for ArrayRefs and Maybe");
|
|
}
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Lmo::Types package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Lmo package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Lmo.pm
|
|
# t/lib/Lmo.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
BEGIN {
|
|
$INC{"Lmo.pm"} = __FILE__;
|
|
package Lmo;
|
|
our $VERSION = '0.30_Percona'; # Forked from 0.30 of Mo.
|
|
|
|
|
|
use strict;
|
|
use warnings qw( FATAL all );
|
|
|
|
use Carp ();
|
|
use Scalar::Util qw(looks_like_number blessed);
|
|
|
|
use Lmo::Meta;
|
|
use Lmo::Object;
|
|
use Lmo::Types;
|
|
|
|
use Lmo::Utils;
|
|
|
|
my %export_for;
|
|
sub import {
|
|
warnings->import(qw(FATAL all));
|
|
strict->import();
|
|
|
|
my $caller = scalar caller(); # Caller's package
|
|
my %exports = (
|
|
extends => \&extends,
|
|
has => \&has,
|
|
with => \&with,
|
|
override => \&override,
|
|
confess => \&Carp::confess,
|
|
);
|
|
|
|
$export_for{$caller} = \%exports;
|
|
|
|
for my $keyword ( keys %exports ) {
|
|
_install_coderef "${caller}::$keyword" => $exports{$keyword};
|
|
}
|
|
|
|
if ( !@{ *{ _glob_for "${caller}::ISA" }{ARRAY} || [] } ) {
|
|
@_ = "Lmo::Object";
|
|
goto *{ _glob_for "${caller}::extends" }{CODE};
|
|
}
|
|
}
|
|
|
|
sub extends {
|
|
my $caller = scalar caller();
|
|
for my $class ( @_ ) {
|
|
_load_module($class);
|
|
}
|
|
_set_package_isa($caller, @_);
|
|
_set_inherited_metadata($caller);
|
|
}
|
|
|
|
sub _load_module {
|
|
my ($class) = @_;
|
|
|
|
(my $file = $class) =~ s{::|'}{/}g;
|
|
$file .= '.pm';
|
|
{ local $@; eval { require "$file" } } # or warn $@;
|
|
return;
|
|
}
|
|
|
|
sub with {
|
|
my $package = scalar caller();
|
|
require Role::Tiny;
|
|
for my $role ( @_ ) {
|
|
_load_module($role);
|
|
_role_attribute_metadata($package, $role);
|
|
}
|
|
Role::Tiny->apply_roles_to_package($package, @_);
|
|
}
|
|
|
|
sub _role_attribute_metadata {
|
|
my ($package, $role) = @_;
|
|
|
|
my $package_meta = Lmo::Meta->metadata_for($package);
|
|
my $role_meta = Lmo::Meta->metadata_for($role);
|
|
|
|
%$package_meta = (%$role_meta, %$package_meta);
|
|
}
|
|
|
|
sub has {
|
|
my $names = shift;
|
|
my $caller = scalar caller();
|
|
|
|
my $class_metadata = Lmo::Meta->metadata_for($caller);
|
|
|
|
for my $attribute ( ref $names ? @$names : $names ) {
|
|
my %args = @_;
|
|
my $method = ($args{is} || '') eq 'ro'
|
|
? sub {
|
|
Carp::confess("Cannot assign a value to a read-only accessor at reader ${caller}::${attribute}")
|
|
if $#_;
|
|
return $_[0]{$attribute};
|
|
}
|
|
: sub {
|
|
return $#_
|
|
? $_[0]{$attribute} = $_[1]
|
|
: $_[0]{$attribute};
|
|
};
|
|
|
|
$class_metadata->{$attribute} = ();
|
|
|
|
if ( my $type_check = $args{isa} ) {
|
|
my $check_name = $type_check;
|
|
|
|
if ( my ($aggregate_type, $inner_type) = $type_check =~ /\A(ArrayRef|Maybe)\[(.*)\]\z/ ) {
|
|
$type_check = Lmo::Types::_nested_constraints($attribute, $aggregate_type, $inner_type);
|
|
}
|
|
|
|
my $check_sub = sub {
|
|
my ($new_val) = @_;
|
|
Lmo::Types::check_type_constraints($attribute, $type_check, $check_name, $new_val);
|
|
};
|
|
|
|
$class_metadata->{$attribute}{isa} = [$check_name, $check_sub];
|
|
my $orig_method = $method;
|
|
$method = sub {
|
|
$check_sub->($_[1]) if $#_;
|
|
goto &$orig_method;
|
|
};
|
|
}
|
|
|
|
if ( my $builder = $args{builder} ) {
|
|
my $original_method = $method;
|
|
$method = sub {
|
|
$#_
|
|
? goto &$original_method
|
|
: ! exists $_[0]{$attribute}
|
|
? $_[0]{$attribute} = $_[0]->$builder
|
|
: goto &$original_method
|
|
};
|
|
}
|
|
|
|
if ( my $code = $args{default} ) {
|
|
Carp::confess("${caller}::${attribute}'s default is $code, but should be a coderef")
|
|
unless ref($code) eq 'CODE';
|
|
my $original_method = $method;
|
|
$method = sub {
|
|
$#_
|
|
? goto &$original_method
|
|
: ! exists $_[0]{$attribute}
|
|
? $_[0]{$attribute} = $_[0]->$code
|
|
: goto &$original_method
|
|
};
|
|
}
|
|
|
|
if ( my $role = $args{does} ) {
|
|
my $original_method = $method;
|
|
$method = sub {
|
|
if ( $#_ ) {
|
|
Carp::confess(qq<Attribute ($attribute) doesn't consume a '$role' role">)
|
|
unless Scalar::Util::blessed($_[1]) && eval { $_[1]->does($role) }
|
|
}
|
|
goto &$original_method
|
|
};
|
|
}
|
|
|
|
if ( my $coercion = $args{coerce} ) {
|
|
$class_metadata->{$attribute}{coerce} = $coercion;
|
|
my $original_method = $method;
|
|
$method = sub {
|
|
if ( $#_ ) {
|
|
return $original_method->($_[0], $coercion->($_[1]))
|
|
}
|
|
goto &$original_method;
|
|
}
|
|
}
|
|
|
|
_install_coderef "${caller}::$attribute" => $method;
|
|
|
|
if ( $args{required} ) {
|
|
$class_metadata->{$attribute}{required} = 1;
|
|
}
|
|
|
|
if ($args{clearer}) {
|
|
_install_coderef "${caller}::$args{clearer}"
|
|
=> sub { delete shift->{$attribute} }
|
|
}
|
|
|
|
if ($args{predicate}) {
|
|
_install_coderef "${caller}::$args{predicate}"
|
|
=> sub { exists shift->{$attribute} }
|
|
}
|
|
|
|
if ($args{handles}) {
|
|
_has_handles($caller, $attribute, \%args);
|
|
}
|
|
|
|
if (exists $args{init_arg}) {
|
|
$class_metadata->{$attribute}{init_arg} = $args{init_arg};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub _has_handles {
|
|
my ($caller, $attribute, $args) = @_;
|
|
my $handles = $args->{handles};
|
|
|
|
my $ref = ref $handles;
|
|
my $kv;
|
|
if ( $ref eq ref [] ) {
|
|
$kv = { map { $_,$_ } @{$handles} };
|
|
}
|
|
elsif ( $ref eq ref {} ) {
|
|
$kv = $handles;
|
|
}
|
|
elsif ( $ref eq ref qr// ) {
|
|
Carp::confess("Cannot delegate methods based on a Regexp without a type constraint (isa)")
|
|
unless $args->{isa};
|
|
my $target_class = $args->{isa};
|
|
$kv = {
|
|
map { $_, $_ }
|
|
grep { $_ =~ $handles }
|
|
grep { !exists $Lmo::Object::{$_} && $target_class->can($_) }
|
|
grep { !$export_for{$target_class}->{$_} }
|
|
keys %{ _stash_for $target_class }
|
|
};
|
|
}
|
|
else {
|
|
Carp::confess("handles for $ref not yet implemented");
|
|
}
|
|
|
|
while ( my ($method, $target) = each %{$kv} ) {
|
|
my $name = _glob_for "${caller}::$method";
|
|
Carp::confess("You cannot overwrite a locally defined method ($method) with a delegation")
|
|
if defined &$name;
|
|
|
|
my ($target, @curried_args) = ref($target) ? @$target : $target;
|
|
*$name = sub {
|
|
my $self = shift;
|
|
my $delegate_to = $self->$attribute();
|
|
my $error = "Cannot delegate $method to $target because the value of $attribute";
|
|
Carp::confess("$error is not defined") unless $delegate_to;
|
|
Carp::confess("$error is not an object (got '$delegate_to')")
|
|
unless Scalar::Util::blessed($delegate_to) || (!ref($delegate_to) && $delegate_to->can($target));
|
|
return $delegate_to->$target(@curried_args, @_);
|
|
}
|
|
}
|
|
}
|
|
|
|
sub _set_package_isa {
|
|
my ($package, @new_isa) = @_;
|
|
my $package_isa = \*{ _glob_for "${package}::ISA" };
|
|
@{*$package_isa} = @new_isa;
|
|
}
|
|
|
|
sub _set_inherited_metadata {
|
|
my $class = shift;
|
|
my $class_metadata = Lmo::Meta->metadata_for($class);
|
|
my $linearized_isa = mro::get_linear_isa($class);
|
|
my %new_metadata;
|
|
|
|
for my $isa_class (reverse @$linearized_isa) {
|
|
my $isa_metadata = Lmo::Meta->metadata_for($isa_class);
|
|
%new_metadata = (
|
|
%new_metadata,
|
|
%$isa_metadata,
|
|
);
|
|
}
|
|
%$class_metadata = %new_metadata;
|
|
}
|
|
|
|
sub unimport {
|
|
my $caller = scalar caller();
|
|
my $target = caller;
|
|
_unimport_coderefs($target, keys %{$export_for{$caller}});
|
|
}
|
|
|
|
sub Dumper {
|
|
require Data::Dumper;
|
|
local $Data::Dumper::Indent = 0;
|
|
local $Data::Dumper::Sortkeys = 0;
|
|
local $Data::Dumper::Quotekeys = 0;
|
|
local $Data::Dumper::Terse = 1;
|
|
|
|
Data::Dumper::Dumper(@_)
|
|
}
|
|
|
|
BEGIN {
|
|
if ($] >= 5.010) {
|
|
{ local $@; require mro; }
|
|
}
|
|
else {
|
|
local $@;
|
|
eval {
|
|
require MRO::Compat;
|
|
} or do {
|
|
*mro::get_linear_isa = *mro::get_linear_isa_dfs = sub {
|
|
no strict 'refs';
|
|
|
|
my $classname = shift;
|
|
|
|
my @lin = ($classname);
|
|
my %stored;
|
|
foreach my $parent (@{"$classname\::ISA"}) {
|
|
my $plin = mro::get_linear_isa_dfs($parent);
|
|
foreach (@$plin) {
|
|
next if exists $stored{$_};
|
|
push(@lin, $_);
|
|
$stored{$_} = 1;
|
|
}
|
|
}
|
|
return \@lin;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub override {
|
|
my ($methods, $code) = @_;
|
|
my $caller = scalar caller;
|
|
|
|
for my $method ( ref($methods) ? @$methods : $methods ) {
|
|
my $full_method = "${caller}::${method}";
|
|
*{_glob_for $full_method} = $code;
|
|
}
|
|
}
|
|
|
|
}
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Lmo package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# VersionParser package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/VersionParser.pm
|
|
# t/lib/VersionParser.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package VersionParser;
|
|
|
|
use Lmo;
|
|
use Scalar::Util qw(blessed);
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use overload (
|
|
'""' => "version",
|
|
'<=>' => "cmp",
|
|
'cmp' => "cmp",
|
|
fallback => 1,
|
|
);
|
|
|
|
use Carp ();
|
|
|
|
has major => (
|
|
is => 'ro',
|
|
isa => 'Int',
|
|
required => 1,
|
|
);
|
|
|
|
has [qw( minor revision )] => (
|
|
is => 'ro',
|
|
isa => 'Num',
|
|
);
|
|
|
|
has flavor => (
|
|
is => 'ro',
|
|
isa => 'Str',
|
|
default => sub { 'Unknown' },
|
|
);
|
|
|
|
has innodb_version => (
|
|
is => 'ro',
|
|
isa => 'Str',
|
|
default => sub { 'NO' },
|
|
);
|
|
|
|
sub series {
|
|
my $self = shift;
|
|
return $self->_join_version($self->major, $self->minor);
|
|
}
|
|
|
|
sub version {
|
|
my $self = shift;
|
|
return $self->_join_version($self->major, $self->minor, $self->revision);
|
|
}
|
|
|
|
sub is_in {
|
|
my ($self, $target) = @_;
|
|
|
|
return $self eq $target;
|
|
}
|
|
|
|
sub _join_version {
|
|
my ($self, @parts) = @_;
|
|
|
|
return join ".", map { my $c = $_; $c =~ s/^0\./0/; $c } grep defined, @parts;
|
|
}
|
|
sub _split_version {
|
|
my ($self, $str) = @_;
|
|
my @version_parts = map { s/^0(?=\d)/0./; $_ } $str =~ m/(\d+)/g;
|
|
return @version_parts[0..2];
|
|
}
|
|
|
|
sub normalized_version {
|
|
my ( $self ) = @_;
|
|
my $result = sprintf('%d%02d%02d', map { $_ || 0 } $self->major,
|
|
$self->minor,
|
|
$self->revision);
|
|
PTDEBUG && _d($self->version, 'normalizes to', $result);
|
|
return $result;
|
|
}
|
|
|
|
sub comment {
|
|
my ( $self, $cmd ) = @_;
|
|
my $v = $self->normalized_version();
|
|
|
|
return "/*!$v $cmd */"
|
|
}
|
|
|
|
my @methods = qw(major minor revision);
|
|
sub cmp {
|
|
my ($left, $right) = @_;
|
|
my $right_obj = (blessed($right) && $right->isa(ref($left)))
|
|
? $right
|
|
: ref($left)->new($right);
|
|
|
|
my $retval = 0;
|
|
for my $m ( @methods ) {
|
|
last unless defined($left->$m) && defined($right_obj->$m);
|
|
$retval = $left->$m <=> $right_obj->$m;
|
|
last if $retval;
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
sub BUILDARGS {
|
|
my $self = shift;
|
|
|
|
if ( @_ == 1 ) {
|
|
my %args;
|
|
if ( blessed($_[0]) && $_[0]->can("selectrow_hashref") ) {
|
|
PTDEBUG && _d("VersionParser got a dbh, trying to get the version");
|
|
my $dbh = $_[0];
|
|
local $dbh->{FetchHashKeyName} = 'NAME_lc';
|
|
my $query = eval {
|
|
$dbh->selectall_arrayref(q/SHOW VARIABLES LIKE 'version%'/, { Slice => {} })
|
|
};
|
|
if ( $query ) {
|
|
$query = { map { $_->{variable_name} => $_->{value} } @$query };
|
|
@args{@methods} = $self->_split_version($query->{version});
|
|
$args{flavor} = delete $query->{version_comment}
|
|
if $query->{version_comment};
|
|
}
|
|
elsif ( eval { ($query) = $dbh->selectrow_array(q/SELECT VERSION()/) } ) {
|
|
@args{@methods} = $self->_split_version($query);
|
|
}
|
|
else {
|
|
Carp::confess("Couldn't get the version from the dbh while "
|
|
. "creating a VersionParser object: $@");
|
|
}
|
|
$args{innodb_version} = eval { $self->_innodb_version($dbh) };
|
|
}
|
|
elsif ( !ref($_[0]) ) {
|
|
@args{@methods} = $self->_split_version($_[0]);
|
|
}
|
|
|
|
for my $method (@methods) {
|
|
delete $args{$method} unless defined $args{$method};
|
|
}
|
|
@_ = %args if %args;
|
|
}
|
|
|
|
return $self->SUPER::BUILDARGS(@_);
|
|
}
|
|
|
|
sub _innodb_version {
|
|
my ( $self, $dbh ) = @_;
|
|
return unless $dbh;
|
|
my $innodb_version = "NO";
|
|
|
|
my ($innodb) =
|
|
grep { $_->{engine} =~ m/InnoDB/i }
|
|
map {
|
|
my %hash;
|
|
@hash{ map { lc $_ } keys %$_ } = values %$_;
|
|
\%hash;
|
|
}
|
|
@{ $dbh->selectall_arrayref("SHOW ENGINES", {Slice=>{}}) };
|
|
if ( $innodb ) {
|
|
PTDEBUG && _d("InnoDB support:", $innodb->{support});
|
|
if ( $innodb->{support} =~ m/YES|DEFAULT/i ) {
|
|
my $vars = $dbh->selectrow_hashref(
|
|
"SHOW VARIABLES LIKE 'innodb_version'");
|
|
$innodb_version = !$vars ? "BUILTIN"
|
|
: ($vars->{Value} || $vars->{value});
|
|
}
|
|
else {
|
|
$innodb_version = $innodb->{support}; # probably DISABLED or NO
|
|
}
|
|
}
|
|
|
|
PTDEBUG && _d("InnoDB version:", $innodb_version);
|
|
return $innodb_version;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
no Lmo;
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End VersionParser package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Quoter package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Quoter.pm
|
|
# t/lib/Quoter.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Quoter;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 1;
|
|
$Data::Dumper::Sortkeys = 1;
|
|
$Data::Dumper::Quotekeys = 0;
|
|
|
|
sub new {
|
|
my ( $class, %args ) = @_;
|
|
return bless {}, $class;
|
|
}
|
|
|
|
sub quote {
|
|
my ( $self, @vals ) = @_;
|
|
foreach my $val ( @vals ) {
|
|
$val =~ s/`/``/g;
|
|
}
|
|
return join('.', map { '`' . $_ . '`' } @vals);
|
|
}
|
|
|
|
sub quote_val {
|
|
my ( $self, $val, %args ) = @_;
|
|
|
|
return 'NULL' unless defined $val; # undef = NULL
|
|
return "''" if $val eq ''; # blank string = ''
|
|
return $val if $val =~ m/^0x[0-9a-fA-F]+$/ # quote hex data
|
|
&& !$args{is_char}; # unless is_char is true
|
|
|
|
if ( $args{is_float} ) {
|
|
return sprintf("%.17g", $val) if $val - "$val" != 0;
|
|
return $val;
|
|
}
|
|
|
|
$val =~ s/(['\\])/\\$1/g;
|
|
return "'$val'";
|
|
}
|
|
|
|
sub split_unquote {
|
|
my ( $self, $db_tbl, $default_db ) = @_;
|
|
my ( $db, $tbl ) = split(/[.]/, $db_tbl);
|
|
if ( !$tbl ) {
|
|
$tbl = $db;
|
|
$db = $default_db;
|
|
}
|
|
for ($db, $tbl) {
|
|
next unless $_;
|
|
s/\A`//;
|
|
s/`\z//;
|
|
s/``/`/g;
|
|
}
|
|
|
|
return ($db, $tbl);
|
|
}
|
|
|
|
sub literal_like {
|
|
my ( $self, $like ) = @_;
|
|
return unless $like;
|
|
$like =~ s/([%_])/\\$1/g;
|
|
return "'$like'";
|
|
}
|
|
|
|
sub join_quote {
|
|
my ( $self, $default_db, $db_tbl ) = @_;
|
|
return unless $db_tbl;
|
|
my ($db, $tbl) = split(/[.]/, $db_tbl);
|
|
if ( !$tbl ) {
|
|
$tbl = $db;
|
|
$db = $default_db;
|
|
}
|
|
$db = "`$db`" if $db && $db !~ m/^`/;
|
|
$tbl = "`$tbl`" if $tbl && $tbl !~ m/^`/;
|
|
return $db ? "$db.$tbl" : $tbl;
|
|
}
|
|
|
|
sub serialize_list {
|
|
my ( $self, @args ) = @_;
|
|
PTDEBUG && _d('Serializing', Dumper(\@args));
|
|
return unless @args;
|
|
|
|
my @parts;
|
|
foreach my $arg ( @args ) {
|
|
if ( defined $arg ) {
|
|
$arg =~ s/,/\\,/g; # escape commas
|
|
$arg =~ s/\\N/\\\\N/g; # escape literal \N
|
|
push @parts, $arg;
|
|
}
|
|
else {
|
|
push @parts, '\N';
|
|
}
|
|
}
|
|
|
|
my $string = join(',', @parts);
|
|
PTDEBUG && _d('Serialized: <', $string, '>');
|
|
return $string;
|
|
}
|
|
|
|
sub deserialize_list {
|
|
my ( $self, $string ) = @_;
|
|
PTDEBUG && _d('Deserializing <', $string, '>');
|
|
die "Cannot deserialize an undefined string" unless defined $string;
|
|
|
|
my @parts;
|
|
foreach my $arg ( split(/(?<!\\),/, $string) ) {
|
|
if ( $arg eq '\N' ) {
|
|
$arg = undef;
|
|
}
|
|
else {
|
|
$arg =~ s/\\,/,/g;
|
|
$arg =~ s/\\\\N/\\N/g;
|
|
}
|
|
push @parts, $arg;
|
|
}
|
|
|
|
if ( !@parts ) {
|
|
my $n_empty_strings = $string =~ tr/,//;
|
|
$n_empty_strings++;
|
|
PTDEBUG && _d($n_empty_strings, 'empty strings');
|
|
map { push @parts, '' } 1..$n_empty_strings;
|
|
}
|
|
elsif ( $string =~ m/(?<!\\),$/ ) {
|
|
PTDEBUG && _d('Last value is an empty string');
|
|
push @parts, '';
|
|
}
|
|
|
|
PTDEBUG && _d('Deserialized', Dumper(\@parts));
|
|
return @parts;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Quoter package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# DSNParser package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/DSNParser.pm
|
|
# t/lib/DSNParser.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package DSNParser;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 0;
|
|
$Data::Dumper::Quotekeys = 0;
|
|
|
|
my $dsn_sep = qr/(?<!\\),/;
|
|
|
|
eval {
|
|
require DBI;
|
|
};
|
|
my $have_dbi = $EVAL_ERROR ? 0 : 1;
|
|
|
|
sub new {
|
|
my ( $class, %args ) = @_;
|
|
foreach my $arg ( qw(opts) ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my $self = {
|
|
opts => {} # h, P, u, etc. Should come from DSN OPTIONS section in POD.
|
|
};
|
|
foreach my $opt ( @{$args{opts}} ) {
|
|
if ( !$opt->{key} || !$opt->{desc} ) {
|
|
die "Invalid DSN option: ", Dumper($opt);
|
|
}
|
|
PTDEBUG && _d('DSN option:',
|
|
join(', ',
|
|
map { "$_=" . (defined $opt->{$_} ? ($opt->{$_} || '') : 'undef') }
|
|
keys %$opt
|
|
)
|
|
);
|
|
$self->{opts}->{$opt->{key}} = {
|
|
dsn => $opt->{dsn},
|
|
desc => $opt->{desc},
|
|
copy => $opt->{copy} || 0,
|
|
};
|
|
}
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub prop {
|
|
my ( $self, $prop, $value ) = @_;
|
|
if ( @_ > 2 ) {
|
|
PTDEBUG && _d('Setting', $prop, 'property');
|
|
$self->{$prop} = $value;
|
|
}
|
|
return $self->{$prop};
|
|
}
|
|
|
|
sub parse {
|
|
my ( $self, $dsn, $prev, $defaults ) = @_;
|
|
if ( !$dsn ) {
|
|
PTDEBUG && _d('No DSN to parse');
|
|
return;
|
|
}
|
|
PTDEBUG && _d('Parsing', $dsn);
|
|
$prev ||= {};
|
|
$defaults ||= {};
|
|
my %given_props;
|
|
my %final_props;
|
|
my $opts = $self->{opts};
|
|
|
|
foreach my $dsn_part ( split($dsn_sep, $dsn) ) {
|
|
$dsn_part =~ s/\\,/,/g;
|
|
if ( my ($prop_key, $prop_val) = $dsn_part =~ m/^(.)=(.*)$/ ) {
|
|
$given_props{$prop_key} = $prop_val;
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Interpreting', $dsn_part, 'as h=', $dsn_part);
|
|
$given_props{h} = $dsn_part;
|
|
}
|
|
}
|
|
|
|
foreach my $key ( keys %$opts ) {
|
|
PTDEBUG && _d('Finding value for', $key);
|
|
$final_props{$key} = $given_props{$key};
|
|
if ( !defined $final_props{$key}
|
|
&& defined $prev->{$key} && $opts->{$key}->{copy} )
|
|
{
|
|
$final_props{$key} = $prev->{$key};
|
|
PTDEBUG && _d('Copying value for', $key, 'from previous DSN');
|
|
}
|
|
if ( !defined $final_props{$key} ) {
|
|
$final_props{$key} = $defaults->{$key};
|
|
PTDEBUG && _d('Copying value for', $key, 'from defaults');
|
|
}
|
|
}
|
|
|
|
foreach my $key ( keys %given_props ) {
|
|
die "Unknown DSN option '$key' in '$dsn'. For more details, "
|
|
. "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
|
|
. "for complete documentation."
|
|
unless exists $opts->{$key};
|
|
}
|
|
if ( (my $required = $self->prop('required')) ) {
|
|
foreach my $key ( keys %$required ) {
|
|
die "Missing required DSN option '$key' in '$dsn'. For more details, "
|
|
. "please use the --help option, or try 'perldoc $PROGRAM_NAME' "
|
|
. "for complete documentation."
|
|
unless $final_props{$key};
|
|
}
|
|
}
|
|
|
|
return \%final_props;
|
|
}
|
|
|
|
sub parse_options {
|
|
my ( $self, $o ) = @_;
|
|
die 'I need an OptionParser object' unless ref $o eq 'OptionParser';
|
|
my $dsn_string
|
|
= join(',',
|
|
map { "$_=".$o->get($_); }
|
|
grep { $o->has($_) && $o->get($_) }
|
|
keys %{$self->{opts}}
|
|
);
|
|
PTDEBUG && _d('DSN string made from options:', $dsn_string);
|
|
return $self->parse($dsn_string);
|
|
}
|
|
|
|
sub as_string {
|
|
my ( $self, $dsn, $props ) = @_;
|
|
return $dsn unless ref $dsn;
|
|
my @keys = $props ? @$props : sort keys %$dsn;
|
|
return join(',',
|
|
map { "$_=" . ($_ eq 'p' ? '...' : $dsn->{$_}) }
|
|
grep {
|
|
exists $self->{opts}->{$_}
|
|
&& exists $dsn->{$_}
|
|
&& defined $dsn->{$_}
|
|
} @keys);
|
|
}
|
|
|
|
sub usage {
|
|
my ( $self ) = @_;
|
|
my $usage
|
|
= "DSN syntax is key=value[,key=value...] Allowable DSN keys:\n\n"
|
|
. " KEY COPY MEANING\n"
|
|
. " === ==== =============================================\n";
|
|
my %opts = %{$self->{opts}};
|
|
foreach my $key ( sort keys %opts ) {
|
|
$usage .= " $key "
|
|
. ($opts{$key}->{copy} ? 'yes ' : 'no ')
|
|
. ($opts{$key}->{desc} || '[No description]')
|
|
. "\n";
|
|
}
|
|
$usage .= "\n If the DSN is a bareword, the word is treated as the 'h' key.\n";
|
|
return $usage;
|
|
}
|
|
|
|
sub get_cxn_params {
|
|
my ( $self, $info ) = @_;
|
|
my $dsn;
|
|
my %opts = %{$self->{opts}};
|
|
my $driver = $self->prop('dbidriver') || '';
|
|
if ( $driver eq 'Pg' ) {
|
|
$dsn = 'DBI:Pg:dbname=' . ( $info->{D} || '' ) . ';'
|
|
. join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
|
|
grep { defined $info->{$_} }
|
|
qw(h P));
|
|
}
|
|
else {
|
|
$dsn = 'DBI:mysql:' . ( $info->{D} || '' ) . ';'
|
|
. join(';', map { "$opts{$_}->{dsn}=$info->{$_}" }
|
|
grep { defined $info->{$_} }
|
|
qw(F h P S A s))
|
|
. ';mysql_read_default_group=client'
|
|
. ($info->{L} ? ';mysql_local_infile=1' : '');
|
|
}
|
|
PTDEBUG && _d($dsn);
|
|
return ($dsn, $info->{u}, $info->{p});
|
|
}
|
|
|
|
sub fill_in_dsn {
|
|
my ( $self, $dbh, $dsn ) = @_;
|
|
my $vars = $dbh->selectall_hashref('SHOW VARIABLES', 'Variable_name');
|
|
my ($user, $db) = $dbh->selectrow_array('SELECT USER(), DATABASE()');
|
|
$user =~ s/@.*//;
|
|
$dsn->{h} ||= $vars->{hostname}->{Value};
|
|
$dsn->{S} ||= $vars->{'socket'}->{Value};
|
|
$dsn->{P} ||= $vars->{port}->{Value};
|
|
$dsn->{u} ||= $user;
|
|
$dsn->{D} ||= $db;
|
|
}
|
|
|
|
sub get_dbh {
|
|
my ( $self, $cxn_string, $user, $pass, $opts ) = @_;
|
|
$opts ||= {};
|
|
my $defaults = {
|
|
AutoCommit => 0,
|
|
RaiseError => 1,
|
|
PrintError => 0,
|
|
ShowErrorStatement => 1,
|
|
mysql_enable_utf8 => ($cxn_string =~ m/charset=utf8/i ? 1 : 0),
|
|
};
|
|
@{$defaults}{ keys %$opts } = values %$opts;
|
|
if (delete $defaults->{L}) { # L for LOAD DATA LOCAL INFILE, our own extension
|
|
$defaults->{mysql_local_infile} = 1;
|
|
}
|
|
|
|
if ( $opts->{mysql_use_result} ) {
|
|
$defaults->{mysql_use_result} = 1;
|
|
}
|
|
|
|
if ( !$have_dbi ) {
|
|
die "Cannot connect to MySQL because the Perl DBI module is not "
|
|
. "installed or not found. Run 'perl -MDBI' to see the directories "
|
|
. "that Perl searches for DBI. If DBI is not installed, try:\n"
|
|
. " Debian/Ubuntu apt-get install libdbi-perl\n"
|
|
. " RHEL/CentOS yum install perl-DBI\n"
|
|
. " OpenSolaris pkg install pkg:/SUNWpmdbi\n";
|
|
|
|
}
|
|
|
|
my $dbh;
|
|
my $tries = 2;
|
|
while ( !$dbh && $tries-- ) {
|
|
PTDEBUG && _d($cxn_string, ' ', $user, ' ', $pass,
|
|
join(', ', map { "$_=>$defaults->{$_}" } keys %$defaults ));
|
|
|
|
$dbh = eval { DBI->connect($cxn_string, $user, $pass, $defaults) };
|
|
|
|
if ( !$dbh && $EVAL_ERROR ) {
|
|
if ( $EVAL_ERROR =~ m/locate DBD\/mysql/i ) {
|
|
die "Cannot connect to MySQL because the Perl DBD::mysql module is "
|
|
. "not installed or not found. Run 'perl -MDBD::mysql' to see "
|
|
. "the directories that Perl searches for DBD::mysql. If "
|
|
. "DBD::mysql is not installed, try:\n"
|
|
. " Debian/Ubuntu apt-get install libdbd-mysql-perl\n"
|
|
. " RHEL/CentOS yum install perl-DBD-MySQL\n"
|
|
. " OpenSolaris pkg install pkg:/SUNWapu13dbd-mysql\n";
|
|
}
|
|
elsif ( $EVAL_ERROR =~ m/not a compiled character set|character set utf8/ ) {
|
|
PTDEBUG && _d('Going to try again without utf8 support');
|
|
delete $defaults->{mysql_enable_utf8};
|
|
}
|
|
if ( !$tries ) {
|
|
die $EVAL_ERROR;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $cxn_string =~ m/mysql/i ) {
|
|
my $sql;
|
|
|
|
my ($charset) = $cxn_string =~ m/charset=([\w]+)/;
|
|
if ( $charset ) {
|
|
$sql = qq{/*!40101 SET NAMES "$charset"*/};
|
|
PTDEBUG && _d($dbh, $sql);
|
|
eval { $dbh->do($sql) };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Error setting NAMES to $charset: $EVAL_ERROR";
|
|
}
|
|
}
|
|
else {
|
|
my ($mysql_version) = eval { $dbh->selectrow_array('SELECT VERSION()') };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Cannot get MySQL version: $EVAL_ERROR";
|
|
}
|
|
my (undef, $character_set_server) = eval { $dbh->selectrow_array("SHOW VARIABLES LIKE 'character_set_server'") };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Cannot get MySQL var character_set_server: $EVAL_ERROR";
|
|
}
|
|
|
|
if ( $mysql_version =~ m/^(\d+)\.(\d)\.(\d+).*/ ) {
|
|
if ( $1 >= 8 && $character_set_server =~ m/^utf8/ ) {
|
|
$dbh->{mysql_enable_utf8} = 1;
|
|
$charset = $character_set_server;
|
|
my $msg = "MySQL version $mysql_version >= 8 and character_set_server = $character_set_server\n".
|
|
"Setting: SET NAMES $character_set_server";
|
|
PTDEBUG && _d($msg);
|
|
eval { $dbh->do("SET NAMES '$character_set_server'") };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Cannot SET NAMES $character_set_server: $EVAL_ERROR";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
PTDEBUG && _d('Enabling charset for STDOUT');
|
|
if ( $charset && $charset =~ m/^utf8/ ) {
|
|
binmode(STDOUT, ':utf8')
|
|
or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
|
|
binmode(STDERR, ':utf8')
|
|
or die "Can't binmode(STDERR, ':utf8'): $OS_ERROR";
|
|
}
|
|
else {
|
|
binmode(STDOUT) or die "Can't binmode(STDOUT): $OS_ERROR";
|
|
}
|
|
|
|
if ( my $vars = $self->prop('set-vars') ) {
|
|
$self->set_vars($dbh, $vars);
|
|
}
|
|
|
|
$sql = 'SELECT @@SQL_MODE';
|
|
PTDEBUG && _d($dbh, $sql);
|
|
my ($sql_mode) = eval { $dbh->selectrow_array($sql) };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Error getting the current SQL_MODE: $EVAL_ERROR";
|
|
}
|
|
|
|
$sql = 'SET @@SQL_QUOTE_SHOW_CREATE = 1'
|
|
. '/*!40101, @@SQL_MODE=\'NO_AUTO_VALUE_ON_ZERO'
|
|
. ($sql_mode ? ",$sql_mode" : '')
|
|
. '\'*/';
|
|
PTDEBUG && _d($dbh, $sql);
|
|
eval { $dbh->do($sql) };
|
|
if ( $EVAL_ERROR ) {
|
|
die "Error setting SQL_QUOTE_SHOW_CREATE, SQL_MODE"
|
|
. ($sql_mode ? " and $sql_mode" : '')
|
|
. ": $EVAL_ERROR";
|
|
}
|
|
}
|
|
|
|
PTDEBUG && _d('DBH info: ',
|
|
$dbh,
|
|
Dumper($dbh->selectrow_hashref(
|
|
'SELECT DATABASE(), CONNECTION_ID(), VERSION()/*!50038 , @@hostname*/')),
|
|
'Connection info:', $dbh->{mysql_hostinfo},
|
|
'Character set info:', Dumper($dbh->selectall_arrayref(
|
|
"SHOW VARIABLES LIKE 'character_set%'", { Slice => {}})),
|
|
'$DBD::mysql::VERSION:', $DBD::mysql::VERSION,
|
|
'$DBI::VERSION:', $DBI::VERSION,
|
|
);
|
|
|
|
return $dbh;
|
|
}
|
|
|
|
sub get_hostname {
|
|
my ( $self, $dbh ) = @_;
|
|
if ( my ($host) = ($dbh->{mysql_hostinfo} || '') =~ m/^(\w+) via/ ) {
|
|
return $host;
|
|
}
|
|
my ( $hostname, $one ) = $dbh->selectrow_array(
|
|
'SELECT /*!50038 @@hostname, */ 1');
|
|
return $hostname;
|
|
}
|
|
|
|
sub disconnect {
|
|
my ( $self, $dbh ) = @_;
|
|
PTDEBUG && $self->print_active_handles($dbh);
|
|
$dbh->disconnect;
|
|
}
|
|
|
|
sub print_active_handles {
|
|
my ( $self, $thing, $level ) = @_;
|
|
$level ||= 0;
|
|
printf("# Active %sh: %s %s %s\n", ($thing->{Type} || 'undef'), "\t" x $level,
|
|
$thing, (($thing->{Type} || '') eq 'st' ? $thing->{Statement} || '' : ''))
|
|
or die "Cannot print: $OS_ERROR";
|
|
foreach my $handle ( grep {defined} @{ $thing->{ChildHandles} } ) {
|
|
$self->print_active_handles( $handle, $level + 1 );
|
|
}
|
|
}
|
|
|
|
sub copy {
|
|
my ( $self, $dsn_1, $dsn_2, %args ) = @_;
|
|
die 'I need a dsn_1 argument' unless $dsn_1;
|
|
die 'I need a dsn_2 argument' unless $dsn_2;
|
|
my %new_dsn = map {
|
|
my $key = $_;
|
|
my $val;
|
|
if ( $args{overwrite} ) {
|
|
$val = defined $dsn_1->{$key} ? $dsn_1->{$key} : $dsn_2->{$key};
|
|
}
|
|
else {
|
|
$val = defined $dsn_2->{$key} ? $dsn_2->{$key} : $dsn_1->{$key};
|
|
}
|
|
$key => $val;
|
|
} keys %{$self->{opts}};
|
|
return \%new_dsn;
|
|
}
|
|
|
|
sub set_vars {
|
|
my ($self, $dbh, $vars) = @_;
|
|
|
|
return unless $vars;
|
|
|
|
foreach my $var ( sort keys %$vars ) {
|
|
my $val = $vars->{$var}->{val};
|
|
|
|
(my $quoted_var = $var) =~ s/_/\\_/;
|
|
my ($var_exists, $current_val);
|
|
eval {
|
|
($var_exists, $current_val) = $dbh->selectrow_array(
|
|
"SHOW VARIABLES LIKE '$quoted_var'");
|
|
};
|
|
my $e = $EVAL_ERROR;
|
|
if ( $e ) {
|
|
PTDEBUG && _d($e);
|
|
}
|
|
|
|
if ( $vars->{$var}->{default} && !$var_exists ) {
|
|
PTDEBUG && _d('Not setting default var', $var,
|
|
'because it does not exist');
|
|
next;
|
|
}
|
|
|
|
if ( $current_val && $current_val eq $val ) {
|
|
PTDEBUG && _d('Not setting var', $var, 'because its value',
|
|
'is already', $val);
|
|
next;
|
|
}
|
|
|
|
my $sql = "SET SESSION $var=$val";
|
|
PTDEBUG && _d($dbh, $sql);
|
|
eval { $dbh->do($sql) };
|
|
if ( my $set_error = $EVAL_ERROR ) {
|
|
chomp($set_error);
|
|
$set_error =~ s/ at \S+ line \d+//;
|
|
my $msg = "Error setting $var: $set_error";
|
|
if ( $current_val ) {
|
|
$msg .= " The current value for $var is $current_val. "
|
|
. "If the variable is read only (not dynamic), specify "
|
|
. "--set-vars $var=$current_val to avoid this warning, "
|
|
. "else manually set the variable and restart MySQL.";
|
|
}
|
|
warn $msg . "\n\n";
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End DSNParser package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Cxn package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Cxn.pm
|
|
# t/lib/Cxn.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Cxn;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use Scalar::Util qw(blessed);
|
|
use constant {
|
|
PTDEBUG => $ENV{PTDEBUG} || 0,
|
|
PERCONA_TOOLKIT_TEST_USE_DSN_NAMES => $ENV{PERCONA_TOOLKIT_TEST_USE_DSN_NAMES} || 0,
|
|
};
|
|
|
|
sub new {
|
|
my ( $class, %args ) = @_;
|
|
my @required_args = qw(DSNParser OptionParser);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
};
|
|
my ($dp, $o) = @args{@required_args};
|
|
|
|
my $dsn_defaults = $dp->parse_options($o);
|
|
my $prev_dsn = $args{prev_dsn};
|
|
my $dsn = $args{dsn};
|
|
if ( !$dsn ) {
|
|
$args{dsn_string} ||= 'h=' . ($dsn_defaults->{h} || 'localhost');
|
|
|
|
$dsn = $dp->parse(
|
|
$args{dsn_string}, $prev_dsn, $dsn_defaults);
|
|
}
|
|
elsif ( $prev_dsn ) {
|
|
$dsn = $dp->copy($prev_dsn, $dsn);
|
|
}
|
|
|
|
my $dsn_name = $dp->as_string($dsn, [qw(h P S)])
|
|
|| $dp->as_string($dsn, [qw(F)])
|
|
|| '';
|
|
|
|
my $self = {
|
|
dsn => $dsn,
|
|
dbh => $args{dbh},
|
|
dsn_name => $dsn_name,
|
|
hostname => '',
|
|
set => $args{set},
|
|
NAME_lc => defined($args{NAME_lc}) ? $args{NAME_lc} : 1,
|
|
dbh_set => 0,
|
|
ask_pass => $o->get('ask-pass'),
|
|
DSNParser => $dp,
|
|
is_cluster_node => undef,
|
|
parent => $args{parent},
|
|
};
|
|
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub connect {
|
|
my ( $self, %opts ) = @_;
|
|
my $dsn = $opts{dsn} || $self->{dsn};
|
|
my $dp = $self->{DSNParser};
|
|
|
|
my $dbh = $self->{dbh};
|
|
if ( !$dbh || ( $dbh && $self->{dbh_set} && !$self->_ping($dbh) ) ) {
|
|
if ( $self->{ask_pass} && !$self->{asked_for_pass} && !defined $dsn->{p} ) {
|
|
$dsn->{p} = OptionParser::prompt_noecho("Enter MySQL password: ");
|
|
$self->{asked_for_pass} = 1;
|
|
}
|
|
$dbh = $dp->get_dbh(
|
|
$dp->get_cxn_params($dsn),
|
|
{
|
|
AutoCommit => 1,
|
|
%opts,
|
|
},
|
|
);
|
|
}
|
|
|
|
$dbh = $self->set_dbh($dbh);
|
|
if ( $opts{dsn} ) {
|
|
$self->{dsn} = $dsn;
|
|
$self->{dsn_name} = $dp->as_string($dsn, [qw(h P S)])
|
|
|| $dp->as_string($dsn, [qw(F)])
|
|
|| '';
|
|
|
|
}
|
|
PTDEBUG && _d($dbh, 'Connected dbh to', $self->{hostname},$self->{dsn_name});
|
|
return $dbh;
|
|
}
|
|
|
|
sub set_dbh {
|
|
my ($self, $dbh) = @_;
|
|
|
|
PTDEBUG && _d($dbh, 'Setting dbh');
|
|
|
|
$dbh->{FetchHashKeyName} = 'NAME_lc' if $self->{NAME_lc};
|
|
|
|
my $sql = 'SELECT @@server_id /*!50038 , @@hostname*/, CONNECTION_ID() as connection_id';
|
|
PTDEBUG && _d($dbh, $sql);
|
|
my ($server_id, $hostname, $connection_id) = $dbh->selectrow_array($sql);
|
|
PTDEBUG && _d($dbh, 'hostname:', $hostname, $server_id);
|
|
|
|
if ( $self->{dbh} && $self->{dbh} == $dbh && $self->{dbh_set} && $self->{dbh_set} == $connection_id) {
|
|
PTDEBUG && _d($dbh, 'Already set dbh');
|
|
return $dbh;
|
|
}
|
|
|
|
if ( $hostname ) {
|
|
$self->{hostname} = $hostname;
|
|
}
|
|
|
|
if ( $self->{parent} ) {
|
|
PTDEBUG && _d($dbh, 'Setting InactiveDestroy=1 in parent');
|
|
$dbh->{InactiveDestroy} = 1;
|
|
}
|
|
|
|
if ( my $set = $self->{set}) {
|
|
$set->($dbh);
|
|
}
|
|
|
|
$self->{dbh} = $dbh;
|
|
$self->{dbh_set} = $connection_id;
|
|
return $dbh;
|
|
}
|
|
|
|
sub lost_connection {
|
|
my ($self, $e) = @_;
|
|
return 0 unless $e;
|
|
return $e =~ m/MySQL server has gone away/
|
|
|| $e =~ m/Server has gone away/
|
|
|| $e =~ m/Lost connection to MySQL server/
|
|
|| $e =~ m/Lost connection to server/
|
|
|| $e =~ m/Server shutdown in progress/;
|
|
}
|
|
|
|
sub dbh {
|
|
my ($self) = @_;
|
|
return $self->{dbh};
|
|
}
|
|
|
|
sub dsn {
|
|
my ($self) = @_;
|
|
return $self->{dsn};
|
|
}
|
|
|
|
sub name {
|
|
my ($self) = @_;
|
|
return $self->{dsn_name} if PERCONA_TOOLKIT_TEST_USE_DSN_NAMES;
|
|
return $self->{hostname} || $self->{dsn_name} || 'unknown host';
|
|
}
|
|
|
|
sub description {
|
|
my ($self) = @_;
|
|
return sprintf("%s -> %s:%s", $self->name(), $self->{dsn}->{h} || 'localhost' , $self->{dsn}->{P} || 'socket');
|
|
}
|
|
|
|
sub get_id {
|
|
my ($self, $cxn) = @_;
|
|
|
|
$cxn ||= $self;
|
|
|
|
my $unique_id;
|
|
if ($cxn->is_cluster_node()) { # for cluster we concatenate various variables to maximize id 'uniqueness' across versions
|
|
my $sql = q{SHOW STATUS LIKE 'wsrep\_local\_index'};
|
|
my (undef, $wsrep_local_index) = $cxn->dbh->selectrow_array($sql);
|
|
PTDEBUG && _d("Got cluster wsrep_local_index: ",$wsrep_local_index);
|
|
$unique_id = $wsrep_local_index."|";
|
|
foreach my $val ('server\_id', 'wsrep\_sst\_receive\_address', 'wsrep\_node\_name', 'wsrep\_node\_address') {
|
|
my $sql = "SHOW VARIABLES LIKE '$val'";
|
|
PTDEBUG && _d($cxn->name, $sql);
|
|
my (undef, $val) = $cxn->dbh->selectrow_array($sql);
|
|
$unique_id .= "|$val";
|
|
}
|
|
} else {
|
|
my $sql = 'SELECT @@SERVER_ID';
|
|
PTDEBUG && _d($sql);
|
|
$unique_id = $cxn->dbh->selectrow_array($sql);
|
|
}
|
|
PTDEBUG && _d("Generated unique id for cluster:", $unique_id);
|
|
return $unique_id;
|
|
}
|
|
|
|
|
|
sub is_cluster_node {
|
|
my ($self, $cxn) = @_;
|
|
|
|
$cxn ||= $self;
|
|
|
|
my $sql = "SHOW VARIABLES LIKE 'wsrep\_on'";
|
|
|
|
my $dbh;
|
|
if ($cxn->isa('DBI::db')) {
|
|
$dbh = $cxn;
|
|
PTDEBUG && _d($sql); #don't invoke name() if it's not a Cxn!
|
|
}
|
|
else {
|
|
$dbh = $cxn->dbh();
|
|
PTDEBUG && _d($cxn->name, $sql);
|
|
}
|
|
|
|
my $row = $dbh->selectrow_arrayref($sql);
|
|
return $row && $row->[1] && ($row->[1] eq 'ON' || $row->[1] eq '1') ? 1 : 0;
|
|
|
|
}
|
|
|
|
sub remove_duplicate_cxns {
|
|
my ($self, %args) = @_;
|
|
my @cxns = @{$args{cxns}};
|
|
my $seen_ids = $args{seen_ids} || {};
|
|
PTDEBUG && _d("Removing duplicates from ", join(" ", map { $_->name } @cxns));
|
|
my @trimmed_cxns;
|
|
|
|
for my $cxn ( @cxns ) {
|
|
|
|
my $id = $cxn->get_id();
|
|
PTDEBUG && _d('Server ID for ', $cxn->name, ': ', $id);
|
|
|
|
if ( ! $seen_ids->{$id}++ ) {
|
|
push @trimmed_cxns, $cxn
|
|
}
|
|
else {
|
|
PTDEBUG && _d("Removing ", $cxn->name,
|
|
", ID ", $id, ", because we've already seen it");
|
|
}
|
|
}
|
|
|
|
return \@trimmed_cxns;
|
|
}
|
|
|
|
sub DESTROY {
|
|
my ($self) = @_;
|
|
|
|
PTDEBUG && _d('Destroying cxn');
|
|
|
|
if ( $self->{parent} ) {
|
|
PTDEBUG && _d($self->{dbh}, 'Not disconnecting dbh in parent');
|
|
}
|
|
elsif ( $self->{dbh}
|
|
&& blessed($self->{dbh})
|
|
&& $self->{dbh}->can("disconnect") )
|
|
{
|
|
PTDEBUG && _d($self->{dbh}, 'Disconnecting dbh on', $self->{hostname},
|
|
$self->{dsn_name});
|
|
$self->{dbh}->disconnect();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _ping() {
|
|
my ( $self, $dbh ) = @_;
|
|
if (!$dbh->ping()) {
|
|
return 0;
|
|
}
|
|
my $sql = 'SELECT CONNECTION_ID() as connection_id';
|
|
PTDEBUG && _d($dbh, $sql);
|
|
my ($connection_id) = $dbh->selectrow_array($sql);
|
|
return $self->{dbh_set} == $connection_id;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Cxn package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Daemon package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Daemon.pm
|
|
# t/lib/Daemon.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Daemon;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use POSIX qw(setsid);
|
|
use Fcntl qw(:DEFAULT);
|
|
|
|
sub new {
|
|
my ($class, %args) = @_;
|
|
my $self = {
|
|
log_file => $args{log_file},
|
|
pid_file => $args{pid_file},
|
|
daemonize => $args{daemonize},
|
|
force_log_file => $args{force_log_file},
|
|
parent_exit => $args{parent_exit},
|
|
pid_file_owner => 0,
|
|
utf8 => $args{utf8} // 0,
|
|
};
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub run {
|
|
my ($self) = @_;
|
|
|
|
my $daemonize = $self->{daemonize};
|
|
my $pid_file = $self->{pid_file};
|
|
my $log_file = $self->{log_file};
|
|
my $force_log_file = $self->{force_log_file};
|
|
my $parent_exit = $self->{parent_exit};
|
|
my $utf8 = $self->{utf8};
|
|
|
|
PTDEBUG && _d('Starting daemon');
|
|
|
|
if ( $pid_file ) {
|
|
eval {
|
|
$self->_make_pid_file(
|
|
pid => $PID, # parent's pid
|
|
pid_file => $pid_file,
|
|
);
|
|
};
|
|
die "$EVAL_ERROR\n" if $EVAL_ERROR;
|
|
if ( !$daemonize ) {
|
|
$self->{pid_file_owner} = $PID; # parent's pid
|
|
}
|
|
}
|
|
|
|
if ( $daemonize ) {
|
|
defined (my $child_pid = fork()) or die "Cannot fork: $OS_ERROR";
|
|
if ( $child_pid ) {
|
|
PTDEBUG && _d('Forked child', $child_pid);
|
|
$parent_exit->($child_pid) if $parent_exit;
|
|
exit 0;
|
|
}
|
|
|
|
POSIX::setsid() or die "Cannot start a new session: $OS_ERROR";
|
|
chdir '/' or die "Cannot chdir to /: $OS_ERROR";
|
|
|
|
if ( $pid_file ) {
|
|
$self->_update_pid_file(
|
|
pid => $PID, # child's pid
|
|
pid_file => $pid_file,
|
|
);
|
|
$self->{pid_file_owner} = $PID;
|
|
}
|
|
}
|
|
|
|
if ( $daemonize || $force_log_file ) {
|
|
PTDEBUG && _d('Redirecting STDIN to /dev/null');
|
|
close STDIN;
|
|
open STDIN, '/dev/null'
|
|
or die "Cannot reopen STDIN to /dev/null: $OS_ERROR";
|
|
if ( $log_file ) {
|
|
PTDEBUG && _d('Redirecting STDOUT and STDERR to', $log_file);
|
|
close STDOUT;
|
|
open STDOUT, '>>', $log_file
|
|
or die "Cannot open log file $log_file: $OS_ERROR";
|
|
if ( $utf8 ) {
|
|
binmode(STDOUT, ':utf8')
|
|
or die "Can't binmode(STDOUT, ':utf8'): $OS_ERROR";
|
|
}
|
|
|
|
|
|
close STDERR;
|
|
open STDERR, ">&STDOUT"
|
|
or die "Cannot dupe STDERR to STDOUT: $OS_ERROR";
|
|
if ( $utf8 ) {
|
|
binmode(STDERR, ':utf8')
|
|
or die "Can't binmode(STDERR, ':utf8'): $OS_ERROR";
|
|
}
|
|
}
|
|
else {
|
|
if ( -t STDOUT ) {
|
|
PTDEBUG && _d('No log file and STDOUT is a terminal;',
|
|
'redirecting to /dev/null');
|
|
close STDOUT;
|
|
open STDOUT, '>', '/dev/null'
|
|
or die "Cannot reopen STDOUT to /dev/null: $OS_ERROR";
|
|
}
|
|
if ( -t STDERR ) {
|
|
PTDEBUG && _d('No log file and STDERR is a terminal;',
|
|
'redirecting to /dev/null');
|
|
close STDERR;
|
|
open STDERR, '>', '/dev/null'
|
|
or die "Cannot reopen STDERR to /dev/null: $OS_ERROR";
|
|
}
|
|
}
|
|
|
|
$OUTPUT_AUTOFLUSH = 1;
|
|
}
|
|
|
|
PTDEBUG && _d('Daemon running');
|
|
return;
|
|
}
|
|
|
|
sub _make_pid_file {
|
|
my ($self, %args) = @_;
|
|
my @required_args = qw(pid pid_file);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
};
|
|
my $pid = $args{pid};
|
|
my $pid_file = $args{pid_file};
|
|
|
|
eval {
|
|
sysopen(PID_FH, $pid_file, O_RDWR|O_CREAT|O_EXCL) or die $OS_ERROR;
|
|
print PID_FH $PID, "\n";
|
|
close PID_FH;
|
|
};
|
|
if ( my $e = $EVAL_ERROR ) {
|
|
if ( $e =~ m/file exists/i ) {
|
|
my $old_pid = $self->_check_pid_file(
|
|
pid_file => $pid_file,
|
|
pid => $PID,
|
|
);
|
|
if ( $old_pid ) {
|
|
warn "Overwriting PID file $pid_file because PID $old_pid "
|
|
. "is not running.\n";
|
|
}
|
|
$self->_update_pid_file(
|
|
pid => $PID,
|
|
pid_file => $pid_file
|
|
);
|
|
}
|
|
else {
|
|
die "Error creating PID file $pid_file: $e\n";
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _check_pid_file {
|
|
my ($self, %args) = @_;
|
|
my @required_args = qw(pid_file pid);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
};
|
|
my $pid_file = $args{pid_file};
|
|
my $pid = $args{pid};
|
|
|
|
PTDEBUG && _d('Checking if PID in', $pid_file, 'is running');
|
|
|
|
if ( ! -f $pid_file ) {
|
|
PTDEBUG && _d('PID file', $pid_file, 'does not exist');
|
|
return;
|
|
}
|
|
|
|
open my $fh, '<', $pid_file
|
|
or die "Error opening $pid_file: $OS_ERROR";
|
|
my $existing_pid = do { local $/; <$fh> };
|
|
chomp($existing_pid) if $existing_pid;
|
|
close $fh
|
|
or die "Error closing $pid_file: $OS_ERROR";
|
|
|
|
if ( $existing_pid ) {
|
|
if ( $existing_pid == $pid ) {
|
|
warn "The current PID $pid already holds the PID file $pid_file\n";
|
|
return;
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Checking if PID', $existing_pid, 'is running');
|
|
my $pid_is_alive = kill 0, $existing_pid;
|
|
if ( $pid_is_alive ) {
|
|
die "PID file $pid_file exists and PID $existing_pid is running\n";
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
die "PID file $pid_file exists but it is empty. Remove the file "
|
|
. "if the process is no longer running.\n";
|
|
}
|
|
|
|
return $existing_pid;
|
|
}
|
|
|
|
sub _update_pid_file {
|
|
my ($self, %args) = @_;
|
|
my @required_args = qw(pid pid_file);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
};
|
|
my $pid = $args{pid};
|
|
my $pid_file = $args{pid_file};
|
|
|
|
open my $fh, '>', $pid_file
|
|
or die "Cannot open $pid_file: $OS_ERROR";
|
|
print { $fh } $pid, "\n"
|
|
or die "Cannot print to $pid_file: $OS_ERROR";
|
|
close $fh
|
|
or warn "Cannot close $pid_file: $OS_ERROR";
|
|
|
|
return;
|
|
}
|
|
|
|
sub remove_pid_file {
|
|
my ($self, $pid_file) = @_;
|
|
$pid_file ||= $self->{pid_file};
|
|
if ( $pid_file && -f $pid_file ) {
|
|
unlink $self->{pid_file}
|
|
or warn "Cannot remove PID file $pid_file: $OS_ERROR";
|
|
PTDEBUG && _d('Removed PID file');
|
|
}
|
|
else {
|
|
PTDEBUG && _d('No PID to remove');
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub DESTROY {
|
|
my ($self) = @_;
|
|
|
|
if ( $self->{pid_file_owner} == $PID ) {
|
|
$self->remove_pid_file();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Daemon package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# HTTP::Micro package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/HTTP/Micro.pm
|
|
# t/lib/HTTP/Micro.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package HTTP::Micro;
|
|
|
|
our $VERSION = '0.01';
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use Carp ();
|
|
|
|
my @attributes;
|
|
BEGIN {
|
|
@attributes = qw(agent timeout);
|
|
no strict 'refs';
|
|
for my $accessor ( @attributes ) {
|
|
*{$accessor} = sub {
|
|
@_ > 1 ? $_[0]->{$accessor} = $_[1] : $_[0]->{$accessor};
|
|
};
|
|
}
|
|
}
|
|
|
|
sub new {
|
|
my($class, %args) = @_;
|
|
(my $agent = $class) =~ s{::}{-}g;
|
|
my $self = {
|
|
agent => $agent . "/" . ($class->VERSION || 0),
|
|
timeout => 60,
|
|
};
|
|
for my $key ( @attributes ) {
|
|
$self->{$key} = $args{$key} if exists $args{$key}
|
|
}
|
|
return bless $self, $class;
|
|
}
|
|
|
|
my %DefaultPort = (
|
|
http => 80,
|
|
https => 443,
|
|
);
|
|
|
|
sub request {
|
|
my ($self, $method, $url, $args) = @_;
|
|
@_ == 3 || (@_ == 4 && ref $args eq 'HASH')
|
|
or Carp::croak(q/Usage: $http->request(METHOD, URL, [HASHREF])/);
|
|
$args ||= {}; # we keep some state in this during _request
|
|
|
|
my $response;
|
|
for ( 0 .. 1 ) {
|
|
$response = eval { $self->_request($method, $url, $args) };
|
|
last unless $@ && $method eq 'GET'
|
|
&& $@ =~ m{^(?:Socket closed|Unexpected end)};
|
|
}
|
|
|
|
if (my $e = "$@") {
|
|
$response = {
|
|
success => q{},
|
|
status => 599,
|
|
reason => 'Internal Exception',
|
|
content => $e,
|
|
headers => {
|
|
'content-type' => 'text/plain',
|
|
'content-length' => length $e,
|
|
}
|
|
};
|
|
}
|
|
return $response;
|
|
}
|
|
|
|
sub _request {
|
|
my ($self, $method, $url, $args) = @_;
|
|
|
|
my ($scheme, $host, $port, $path_query) = $self->_split_url($url);
|
|
|
|
my $request = {
|
|
method => $method,
|
|
scheme => $scheme,
|
|
host_port => ($port == $DefaultPort{$scheme} ? $host : "$host:$port"),
|
|
uri => $path_query,
|
|
headers => {},
|
|
};
|
|
|
|
my $handle = HTTP::Micro::Handle->new(timeout => $self->{timeout});
|
|
|
|
$handle->connect($scheme, $host, $port);
|
|
|
|
$self->_prepare_headers_and_cb($request, $args);
|
|
$handle->write_request_header(@{$request}{qw/method uri headers/});
|
|
$handle->write_content_body($request) if $request->{content};
|
|
|
|
my $response;
|
|
do { $response = $handle->read_response_header }
|
|
until (substr($response->{status},0,1) ne '1');
|
|
|
|
if (!($method eq 'HEAD' || $response->{status} =~ /^[23]04/)) {
|
|
$response->{content} = '';
|
|
$handle->read_content_body(sub { $_[1]->{content} .= $_[0] }, $response);
|
|
}
|
|
|
|
$handle->close;
|
|
$response->{success} = substr($response->{status},0,1) eq '2';
|
|
return $response;
|
|
}
|
|
|
|
sub _prepare_headers_and_cb {
|
|
my ($self, $request, $args) = @_;
|
|
|
|
for ($args->{headers}) {
|
|
next unless defined;
|
|
while (my ($k, $v) = each %$_) {
|
|
$request->{headers}{lc $k} = $v;
|
|
}
|
|
}
|
|
$request->{headers}{'host'} = $request->{host_port};
|
|
$request->{headers}{'connection'} = "close";
|
|
$request->{headers}{'user-agent'} ||= $self->{agent};
|
|
|
|
if (defined $args->{content}) {
|
|
$request->{headers}{'content-type'} ||= "application/octet-stream";
|
|
utf8::downgrade($args->{content}, 1)
|
|
or Carp::croak(q/Wide character in request message body/);
|
|
$request->{headers}{'content-length'} = length $args->{content};
|
|
$request->{content} = $args->{content};
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub _split_url {
|
|
my $url = pop;
|
|
|
|
my ($scheme, $authority, $path_query) = $url =~ m<\A([^:/?#]+)://([^/?#]*)([^#]*)>
|
|
or Carp::croak(qq/Cannot parse URL: '$url'/);
|
|
|
|
$scheme = lc $scheme;
|
|
$path_query = "/$path_query" unless $path_query =~ m<\A/>;
|
|
|
|
my $host = (length($authority)) ? lc $authority : 'localhost';
|
|
$host =~ s/\A[^@]*@//; # userinfo
|
|
my $port = do {
|
|
$host =~ s/:([0-9]*)\z// && length $1
|
|
? $1
|
|
: $DefaultPort{$scheme}
|
|
};
|
|
|
|
return ($scheme, $host, $port, $path_query);
|
|
}
|
|
|
|
} # HTTP::Micro
|
|
|
|
{
|
|
package HTTP::Micro::Handle;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
|
|
use Carp qw(croak);
|
|
use Errno qw(EINTR EPIPE);
|
|
use IO::Socket qw(SOCK_STREAM);
|
|
|
|
sub BUFSIZE () { 32768 }
|
|
|
|
my $Printable = sub {
|
|
local $_ = shift;
|
|
s/\r/\\r/g;
|
|
s/\n/\\n/g;
|
|
s/\t/\\t/g;
|
|
s/([^\x20-\x7E])/sprintf('\\x%.2X', ord($1))/ge;
|
|
$_;
|
|
};
|
|
|
|
sub new {
|
|
my ($class, %args) = @_;
|
|
return bless {
|
|
rbuf => '',
|
|
timeout => 60,
|
|
max_line_size => 16384,
|
|
%args
|
|
}, $class;
|
|
}
|
|
|
|
my $ssl_verify_args = {
|
|
check_cn => "when_only",
|
|
wildcards_in_alt => "anywhere",
|
|
wildcards_in_cn => "anywhere"
|
|
};
|
|
|
|
sub connect {
|
|
@_ == 4 || croak(q/Usage: $handle->connect(scheme, host, port)/);
|
|
my ($self, $scheme, $host, $port) = @_;
|
|
|
|
if ( $scheme eq 'https' ) {
|
|
eval "require IO::Socket::SSL"
|
|
unless exists $INC{'IO/Socket/SSL.pm'};
|
|
croak(qq/IO::Socket::SSL must be installed for https support\n/)
|
|
unless $INC{'IO/Socket/SSL.pm'};
|
|
}
|
|
elsif ( $scheme ne 'http' ) {
|
|
croak(qq/Unsupported URL scheme '$scheme'\n/);
|
|
}
|
|
|
|
$self->{fh} = IO::Socket::INET->new(
|
|
PeerHost => $host,
|
|
PeerPort => $port,
|
|
Proto => 'tcp',
|
|
Type => SOCK_STREAM,
|
|
Timeout => $self->{timeout}
|
|
) or croak(qq/Could not connect to '$host:$port': $@/);
|
|
|
|
binmode($self->{fh})
|
|
or croak(qq/Could not binmode() socket: '$!'/);
|
|
|
|
if ( $scheme eq 'https') {
|
|
IO::Socket::SSL->start_SSL(
|
|
$self->{fh},
|
|
SSL_verifycn_name => $host,
|
|
);
|
|
ref($self->{fh}) eq 'IO::Socket::SSL'
|
|
or die(qq/SSL connection failed for $host\n/);
|
|
if ( $self->{fh}->can("verify_hostname") ) {
|
|
$self->{fh}->verify_hostname( $host, $ssl_verify_args )
|
|
or die(qq/SSL certificate not valid for $host\n/);
|
|
}
|
|
else {
|
|
my $fh = $self->{fh};
|
|
_verify_hostname_of_cert($host, _peer_certificate($fh), $ssl_verify_args)
|
|
or die(qq/SSL certificate not valid for $host\n/);
|
|
}
|
|
}
|
|
|
|
$self->{host} = $host;
|
|
$self->{port} = $port;
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub close {
|
|
@_ == 1 || croak(q/Usage: $handle->close()/);
|
|
my ($self) = @_;
|
|
CORE::close($self->{fh})
|
|
or croak(qq/Could not close socket: '$!'/);
|
|
}
|
|
|
|
sub write {
|
|
@_ == 2 || croak(q/Usage: $handle->write(buf)/);
|
|
my ($self, $buf) = @_;
|
|
|
|
my $len = length $buf;
|
|
my $off = 0;
|
|
|
|
local $SIG{PIPE} = 'IGNORE';
|
|
|
|
while () {
|
|
$self->can_write
|
|
or croak(q/Timed out while waiting for socket to become ready for writing/);
|
|
my $r = syswrite($self->{fh}, $buf, $len, $off);
|
|
if (defined $r) {
|
|
$len -= $r;
|
|
$off += $r;
|
|
last unless $len > 0;
|
|
}
|
|
elsif ($! == EPIPE) {
|
|
croak(qq/Socket closed by remote server: $!/);
|
|
}
|
|
elsif ($! != EINTR) {
|
|
croak(qq/Could not write to socket: '$!'/);
|
|
}
|
|
}
|
|
return $off;
|
|
}
|
|
|
|
sub read {
|
|
@_ == 2 || @_ == 3 || croak(q/Usage: $handle->read(len)/);
|
|
my ($self, $len) = @_;
|
|
|
|
my $buf = '';
|
|
my $got = length $self->{rbuf};
|
|
|
|
if ($got) {
|
|
my $take = ($got < $len) ? $got : $len;
|
|
$buf = substr($self->{rbuf}, 0, $take, '');
|
|
$len -= $take;
|
|
}
|
|
|
|
while ($len > 0) {
|
|
$self->can_read
|
|
or croak(q/Timed out while waiting for socket to become ready for reading/);
|
|
my $r = sysread($self->{fh}, $buf, $len, length $buf);
|
|
if (defined $r) {
|
|
last unless $r;
|
|
$len -= $r;
|
|
}
|
|
elsif ($! != EINTR) {
|
|
croak(qq/Could not read from socket: '$!'/);
|
|
}
|
|
}
|
|
if ($len) {
|
|
croak(q/Unexpected end of stream/);
|
|
}
|
|
return $buf;
|
|
}
|
|
|
|
sub readline {
|
|
@_ == 1 || croak(q/Usage: $handle->readline()/);
|
|
my ($self) = @_;
|
|
|
|
while () {
|
|
if ($self->{rbuf} =~ s/\A ([^\x0D\x0A]* \x0D?\x0A)//x) {
|
|
return $1;
|
|
}
|
|
$self->can_read
|
|
or croak(q/Timed out while waiting for socket to become ready for reading/);
|
|
my $r = sysread($self->{fh}, $self->{rbuf}, BUFSIZE, length $self->{rbuf});
|
|
if (defined $r) {
|
|
last unless $r;
|
|
}
|
|
elsif ($! != EINTR) {
|
|
croak(qq/Could not read from socket: '$!'/);
|
|
}
|
|
}
|
|
croak(q/Unexpected end of stream while looking for line/);
|
|
}
|
|
|
|
sub read_header_lines {
|
|
@_ == 1 || @_ == 2 || croak(q/Usage: $handle->read_header_lines([headers])/);
|
|
my ($self, $headers) = @_;
|
|
$headers ||= {};
|
|
my $lines = 0;
|
|
my $val;
|
|
|
|
while () {
|
|
my $line = $self->readline;
|
|
|
|
if ($line =~ /\A ([^\x00-\x1F\x7F:]+) : [\x09\x20]* ([^\x0D\x0A]*)/x) {
|
|
my ($field_name) = lc $1;
|
|
$val = \($headers->{$field_name} = $2);
|
|
}
|
|
elsif ($line =~ /\A [\x09\x20]+ ([^\x0D\x0A]*)/x) {
|
|
$val
|
|
or croak(q/Unexpected header continuation line/);
|
|
next unless length $1;
|
|
$$val .= ' ' if length $$val;
|
|
$$val .= $1;
|
|
}
|
|
elsif ($line =~ /\A \x0D?\x0A \z/x) {
|
|
last;
|
|
}
|
|
else {
|
|
croak(q/Malformed header line: / . $Printable->($line));
|
|
}
|
|
}
|
|
return $headers;
|
|
}
|
|
|
|
sub write_header_lines {
|
|
(@_ == 2 && ref $_[1] eq 'HASH') || croak(q/Usage: $handle->write_header_lines(headers)/);
|
|
my($self, $headers) = @_;
|
|
|
|
my $buf = '';
|
|
while (my ($k, $v) = each %$headers) {
|
|
my $field_name = lc $k;
|
|
$field_name =~ /\A [\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7A\x7C\x7E]+ \z/x
|
|
or croak(q/Invalid HTTP header field name: / . $Printable->($field_name));
|
|
$field_name =~ s/\b(\w)/\u$1/g;
|
|
$buf .= "$field_name: $v\x0D\x0A";
|
|
}
|
|
$buf .= "\x0D\x0A";
|
|
return $self->write($buf);
|
|
}
|
|
|
|
sub read_content_body {
|
|
@_ == 3 || @_ == 4 || croak(q/Usage: $handle->read_content_body(callback, response, [read_length])/);
|
|
my ($self, $cb, $response, $len) = @_;
|
|
$len ||= $response->{headers}{'content-length'};
|
|
|
|
croak("No content-length in the returned response, and this "
|
|
. "UA doesn't implement chunking") unless defined $len;
|
|
|
|
while ($len > 0) {
|
|
my $read = ($len > BUFSIZE) ? BUFSIZE : $len;
|
|
$cb->($self->read($read), $response);
|
|
$len -= $read;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub write_content_body {
|
|
@_ == 2 || croak(q/Usage: $handle->write_content_body(request)/);
|
|
my ($self, $request) = @_;
|
|
my ($len, $content_length) = (0, $request->{headers}{'content-length'});
|
|
|
|
$len += $self->write($request->{content});
|
|
|
|
$len == $content_length
|
|
or croak(qq/Content-Length mismatch (got: $len expected: $content_length)/);
|
|
|
|
return $len;
|
|
}
|
|
|
|
sub read_response_header {
|
|
@_ == 1 || croak(q/Usage: $handle->read_response_header()/);
|
|
my ($self) = @_;
|
|
|
|
my $line = $self->readline;
|
|
|
|
$line =~ /\A (HTTP\/(0*\d+\.0*\d+)) [\x09\x20]+ ([0-9]{3}) [\x09\x20]+ ([^\x0D\x0A]*) \x0D?\x0A/x
|
|
or croak(q/Malformed Status-Line: / . $Printable->($line));
|
|
|
|
my ($protocol, $version, $status, $reason) = ($1, $2, $3, $4);
|
|
|
|
return {
|
|
status => $status,
|
|
reason => $reason,
|
|
headers => $self->read_header_lines,
|
|
protocol => $protocol,
|
|
};
|
|
}
|
|
|
|
sub write_request_header {
|
|
@_ == 4 || croak(q/Usage: $handle->write_request_header(method, request_uri, headers)/);
|
|
my ($self, $method, $request_uri, $headers) = @_;
|
|
|
|
return $self->write("$method $request_uri HTTP/1.1\x0D\x0A")
|
|
+ $self->write_header_lines($headers);
|
|
}
|
|
|
|
sub _do_timeout {
|
|
my ($self, $type, $timeout) = @_;
|
|
$timeout = $self->{timeout}
|
|
unless defined $timeout && $timeout >= 0;
|
|
|
|
my $fd = fileno $self->{fh};
|
|
defined $fd && $fd >= 0
|
|
or croak(q/select(2): 'Bad file descriptor'/);
|
|
|
|
my $initial = time;
|
|
my $pending = $timeout;
|
|
my $nfound;
|
|
|
|
vec(my $fdset = '', $fd, 1) = 1;
|
|
|
|
while () {
|
|
$nfound = ($type eq 'read')
|
|
? select($fdset, undef, undef, $pending)
|
|
: select(undef, $fdset, undef, $pending) ;
|
|
if ($nfound == -1) {
|
|
$! == EINTR
|
|
or croak(qq/select(2): '$!'/);
|
|
redo if !$timeout || ($pending = $timeout - (time - $initial)) > 0;
|
|
$nfound = 0;
|
|
}
|
|
last;
|
|
}
|
|
$! = 0;
|
|
return $nfound;
|
|
}
|
|
|
|
sub can_read {
|
|
@_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_read([timeout])/);
|
|
my $self = shift;
|
|
return $self->_do_timeout('read', @_)
|
|
}
|
|
|
|
sub can_write {
|
|
@_ == 1 || @_ == 2 || croak(q/Usage: $handle->can_write([timeout])/);
|
|
my $self = shift;
|
|
return $self->_do_timeout('write', @_)
|
|
}
|
|
} # HTTP::Micro::Handle
|
|
|
|
my $prog = <<'EOP';
|
|
BEGIN {
|
|
if ( defined &IO::Socket::SSL::CAN_IPV6 ) {
|
|
*CAN_IPV6 = \*IO::Socket::SSL::CAN_IPV6;
|
|
}
|
|
else {
|
|
constant->import( CAN_IPV6 => '' );
|
|
}
|
|
my %const = (
|
|
NID_CommonName => 13,
|
|
GEN_DNS => 2,
|
|
GEN_IPADD => 7,
|
|
);
|
|
while ( my ($name,$value) = each %const ) {
|
|
no strict 'refs';
|
|
*{$name} = UNIVERSAL::can( 'Net::SSLeay', $name ) || sub { $value };
|
|
}
|
|
}
|
|
{
|
|
use Carp qw(croak);
|
|
my %dispatcher = (
|
|
issuer => sub { Net::SSLeay::X509_NAME_oneline( Net::SSLeay::X509_get_issuer_name( shift )) },
|
|
subject => sub { Net::SSLeay::X509_NAME_oneline( Net::SSLeay::X509_get_subject_name( shift )) },
|
|
);
|
|
if ( $Net::SSLeay::VERSION >= 1.30 ) {
|
|
$dispatcher{commonName} = sub {
|
|
my $cn = Net::SSLeay::X509_NAME_get_text_by_NID(
|
|
Net::SSLeay::X509_get_subject_name( shift ), NID_CommonName);
|
|
$cn =~s{\0$}{}; # work around Bug in Net::SSLeay <1.33
|
|
$cn;
|
|
}
|
|
} else {
|
|
$dispatcher{commonName} = sub {
|
|
croak "you need at least Net::SSLeay version 1.30 for getting commonName"
|
|
}
|
|
}
|
|
|
|
if ( $Net::SSLeay::VERSION >= 1.33 ) {
|
|
$dispatcher{subjectAltNames} = sub { Net::SSLeay::X509_get_subjectAltNames( shift ) };
|
|
} else {
|
|
$dispatcher{subjectAltNames} = sub {
|
|
return;
|
|
};
|
|
}
|
|
|
|
$dispatcher{authority} = $dispatcher{issuer};
|
|
$dispatcher{owner} = $dispatcher{subject};
|
|
$dispatcher{cn} = $dispatcher{commonName};
|
|
|
|
sub _peer_certificate {
|
|
my ($self, $field) = @_;
|
|
my $ssl = $self->_get_ssl_object or return;
|
|
|
|
my $cert = ${*$self}{_SSL_certificate}
|
|
||= Net::SSLeay::get_peer_certificate($ssl)
|
|
or return $self->error("Could not retrieve peer certificate");
|
|
|
|
if ($field) {
|
|
my $sub = $dispatcher{$field} or croak
|
|
"invalid argument for peer_certificate, valid are: ".join( " ",keys %dispatcher ).
|
|
"\nMaybe you need to upgrade your Net::SSLeay";
|
|
return $sub->($cert);
|
|
} else {
|
|
return $cert
|
|
}
|
|
}
|
|
|
|
|
|
my %scheme = (
|
|
ldap => {
|
|
wildcards_in_cn => 0,
|
|
wildcards_in_alt => 'leftmost',
|
|
check_cn => 'always',
|
|
},
|
|
http => {
|
|
wildcards_in_cn => 'anywhere',
|
|
wildcards_in_alt => 'anywhere',
|
|
check_cn => 'when_only',
|
|
},
|
|
smtp => {
|
|
wildcards_in_cn => 0,
|
|
wildcards_in_alt => 0,
|
|
check_cn => 'always'
|
|
},
|
|
none => {}, # do not check
|
|
);
|
|
|
|
$scheme{www} = $scheme{http}; # alias
|
|
$scheme{xmpp} = $scheme{http}; # rfc 3920
|
|
$scheme{pop3} = $scheme{ldap}; # rfc 2595
|
|
$scheme{imap} = $scheme{ldap}; # rfc 2595
|
|
$scheme{acap} = $scheme{ldap}; # rfc 2595
|
|
$scheme{nntp} = $scheme{ldap}; # rfc 4642
|
|
$scheme{ftp} = $scheme{http}; # rfc 4217
|
|
|
|
|
|
sub _verify_hostname_of_cert {
|
|
my $identity = shift;
|
|
my $cert = shift;
|
|
my $scheme = shift || 'none';
|
|
if ( ! ref($scheme) ) {
|
|
$scheme = $scheme{$scheme} or croak "scheme $scheme not defined";
|
|
}
|
|
|
|
return 1 if ! %$scheme; # 'none'
|
|
|
|
my $commonName = $dispatcher{cn}->($cert);
|
|
my @altNames = $dispatcher{subjectAltNames}->($cert);
|
|
|
|
if ( my $sub = $scheme->{callback} ) {
|
|
return $sub->($identity,$commonName,@altNames);
|
|
}
|
|
|
|
|
|
my $ipn;
|
|
if ( CAN_IPV6 and $identity =~m{:} ) {
|
|
$ipn = IO::Socket::SSL::inet_pton(IO::Socket::SSL::AF_INET6,$identity)
|
|
or croak "'$identity' is not IPv6, but neither IPv4 nor hostname";
|
|
} elsif ( $identity =~m{^\d+\.\d+\.\d+\.\d+$} ) {
|
|
$ipn = IO::Socket::SSL::inet_aton( $identity ) or croak "'$identity' is not IPv4, but neither IPv6 nor hostname";
|
|
} else {
|
|
if ( $identity =~m{[^a-zA-Z0-9_.\-]} ) {
|
|
$identity =~m{\0} and croak("name '$identity' has \\0 byte");
|
|
$identity = IO::Socket::SSL::idn_to_ascii($identity) or
|
|
croak "Warning: Given name '$identity' could not be converted to IDNA!";
|
|
}
|
|
}
|
|
|
|
my $check_name = sub {
|
|
my ($name,$identity,$wtyp) = @_;
|
|
$wtyp ||= '';
|
|
my $pattern;
|
|
if ( $wtyp eq 'anywhere' and $name =~m{^([a-zA-Z0-9_\-]*)\*(.+)} ) {
|
|
$pattern = qr{^\Q$1\E[a-zA-Z0-9_\-]*\Q$2\E$}i;
|
|
} elsif ( $wtyp eq 'leftmost' and $name =~m{^\*(\..+)$} ) {
|
|
$pattern = qr{^[a-zA-Z0-9_\-]*\Q$1\E$}i;
|
|
} else {
|
|
$pattern = qr{^\Q$name\E$}i;
|
|
}
|
|
return $identity =~ $pattern;
|
|
};
|
|
|
|
my $alt_dnsNames = 0;
|
|
while (@altNames) {
|
|
my ($type, $name) = splice (@altNames, 0, 2);
|
|
if ( $ipn and $type == GEN_IPADD ) {
|
|
return 1 if $ipn eq $name;
|
|
|
|
} elsif ( ! $ipn and $type == GEN_DNS ) {
|
|
$name =~s/\s+$//; $name =~s/^\s+//;
|
|
$alt_dnsNames++;
|
|
$check_name->($name,$identity,$scheme->{wildcards_in_alt})
|
|
and return 1;
|
|
}
|
|
}
|
|
|
|
if ( ! $ipn and (
|
|
$scheme->{check_cn} eq 'always' or
|
|
$scheme->{check_cn} eq 'when_only' and !$alt_dnsNames)) {
|
|
$check_name->($commonName,$identity,$scheme->{wildcards_in_cn})
|
|
and return 1;
|
|
}
|
|
|
|
return 0; # no match
|
|
}
|
|
}
|
|
EOP
|
|
|
|
eval { require IO::Socket::SSL };
|
|
if ( $INC{"IO/Socket/SSL.pm"} ) {
|
|
eval $prog;
|
|
die $@ if $@;
|
|
}
|
|
|
|
1;
|
|
# ###########################################################################
|
|
# End HTTP::Micro package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# VersionCheck package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/VersionCheck.pm
|
|
# t/lib/VersionCheck.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package VersionCheck;
|
|
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
use Data::Dumper;
|
|
local $Data::Dumper::Indent = 1;
|
|
local $Data::Dumper::Sortkeys = 1;
|
|
local $Data::Dumper::Quotekeys = 0;
|
|
|
|
use Digest::MD5 qw(md5_hex);
|
|
use Sys::Hostname qw(hostname);
|
|
use File::Basename qw();
|
|
use File::Spec;
|
|
use FindBin qw();
|
|
|
|
eval {
|
|
require Percona::Toolkit;
|
|
require HTTP::Micro;
|
|
};
|
|
|
|
my $home = $ENV{HOME} || $ENV{HOMEPATH} || $ENV{USERPROFILE} || '.';
|
|
my @vc_dirs = (
|
|
'/etc/percona',
|
|
'/etc/percona-toolkit',
|
|
'/tmp',
|
|
"$home",
|
|
);
|
|
|
|
{
|
|
my $file = 'percona-version-check';
|
|
|
|
sub version_check_file {
|
|
foreach my $dir ( @vc_dirs ) {
|
|
if ( -d $dir && -w $dir ) {
|
|
PTDEBUG && _d('Version check file', $file, 'in', $dir);
|
|
return $dir . '/' . $file;
|
|
}
|
|
}
|
|
PTDEBUG && _d('Version check file', $file, 'in', $ENV{PWD});
|
|
return $file; # in the CWD
|
|
}
|
|
}
|
|
|
|
sub version_check_time_limit {
|
|
return 60 * 60 * 24; # one day
|
|
}
|
|
|
|
|
|
sub version_check {
|
|
my (%args) = @_;
|
|
|
|
my $instances = $args{instances} || [];
|
|
my $instances_to_check;
|
|
|
|
PTDEBUG && _d('FindBin::Bin:', $FindBin::Bin);
|
|
if ( !$args{force} ) {
|
|
if ( $FindBin::Bin
|
|
&& (-d "$FindBin::Bin/../.bzr" ||
|
|
-d "$FindBin::Bin/../../.bzr" ||
|
|
-d "$FindBin::Bin/../.git" ||
|
|
-d "$FindBin::Bin/../../.git"
|
|
)
|
|
) {
|
|
PTDEBUG && _d("$FindBin::Bin/../.bzr disables --version-check");
|
|
return;
|
|
}
|
|
}
|
|
|
|
eval {
|
|
foreach my $instance ( @$instances ) {
|
|
my ($name, $id) = get_instance_id($instance);
|
|
$instance->{name} = $name;
|
|
$instance->{id} = $id;
|
|
}
|
|
|
|
push @$instances, { name => 'system', id => 0 };
|
|
|
|
$instances_to_check = get_instances_to_check(
|
|
instances => $instances,
|
|
vc_file => $args{vc_file}, # testing
|
|
now => $args{now}, # testing
|
|
);
|
|
PTDEBUG && _d(scalar @$instances_to_check, 'instances to check');
|
|
return unless @$instances_to_check;
|
|
|
|
my $protocol = 'https';
|
|
eval { require IO::Socket::SSL; };
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d($EVAL_ERROR);
|
|
PTDEBUG && _d("SSL not available, won't run version_check");
|
|
return;
|
|
}
|
|
PTDEBUG && _d('Using', $protocol);
|
|
my $url = $args{url} # testing
|
|
|| $ENV{PERCONA_VERSION_CHECK_URL} # testing
|
|
|| "$protocol://v.percona.com";
|
|
PTDEBUG && _d('API URL:', $url);
|
|
|
|
my $advice = pingback(
|
|
instances => $instances_to_check,
|
|
protocol => $protocol,
|
|
url => $url,
|
|
);
|
|
if ( $advice ) {
|
|
PTDEBUG && _d('Advice:', Dumper($advice));
|
|
if ( scalar @$advice > 1) {
|
|
print "\n# " . scalar @$advice . " software updates are "
|
|
. "available:\n";
|
|
}
|
|
else {
|
|
print "\n# A software update is available:\n";
|
|
}
|
|
print join("\n", map { "# * $_" } @$advice), "\n\n";
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d('Version check failed:', $EVAL_ERROR);
|
|
}
|
|
|
|
if ( @$instances_to_check ) {
|
|
eval {
|
|
update_check_times(
|
|
instances => $instances_to_check,
|
|
vc_file => $args{vc_file}, # testing
|
|
now => $args{now}, # testing
|
|
);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d('Error updating version check file:', $EVAL_ERROR);
|
|
}
|
|
}
|
|
|
|
if ( $ENV{PTDEBUG_VERSION_CHECK} ) {
|
|
warn "Exiting because the PTDEBUG_VERSION_CHECK "
|
|
. "environment variable is defined.\n";
|
|
exit 255;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub get_instances_to_check {
|
|
my (%args) = @_;
|
|
|
|
my $instances = $args{instances};
|
|
my $now = $args{now} || int(time);
|
|
my $vc_file = $args{vc_file} || version_check_file();
|
|
|
|
if ( !-f $vc_file ) {
|
|
PTDEBUG && _d('Version check file', $vc_file, 'does not exist;',
|
|
'version checking all instances');
|
|
return $instances;
|
|
}
|
|
|
|
open my $fh, '<', $vc_file or die "Cannot open $vc_file: $OS_ERROR";
|
|
chomp(my $file_contents = do { local $/ = undef; <$fh> });
|
|
PTDEBUG && _d('Version check file', $vc_file, 'contents:', $file_contents);
|
|
close $fh;
|
|
my %last_check_time_for = $file_contents =~ /^([^,]+),(.+)$/mg;
|
|
|
|
my $check_time_limit = version_check_time_limit();
|
|
my @instances_to_check;
|
|
foreach my $instance ( @$instances ) {
|
|
my $last_check_time = $last_check_time_for{ $instance->{id} };
|
|
PTDEBUG && _d('Instance', $instance->{id}, 'last checked',
|
|
$last_check_time, 'now', $now, 'diff', $now - ($last_check_time || 0),
|
|
'hours until next check',
|
|
sprintf '%.2f',
|
|
($check_time_limit - ($now - ($last_check_time || 0))) / 3600);
|
|
if ( !defined $last_check_time
|
|
|| ($now - $last_check_time) >= $check_time_limit ) {
|
|
PTDEBUG && _d('Time to check', Dumper($instance));
|
|
push @instances_to_check, $instance;
|
|
}
|
|
}
|
|
|
|
return \@instances_to_check;
|
|
}
|
|
|
|
sub update_check_times {
|
|
my (%args) = @_;
|
|
|
|
my $instances = $args{instances};
|
|
my $now = $args{now} || int(time);
|
|
my $vc_file = $args{vc_file} || version_check_file();
|
|
PTDEBUG && _d('Updating last check time:', $now);
|
|
|
|
my %all_instances = map {
|
|
$_->{id} => { name => $_->{name}, ts => $now }
|
|
} @$instances;
|
|
|
|
if ( -f $vc_file ) {
|
|
open my $fh, '<', $vc_file or die "Cannot read $vc_file: $OS_ERROR";
|
|
my $contents = do { local $/ = undef; <$fh> };
|
|
close $fh;
|
|
|
|
foreach my $line ( split("\n", ($contents || '')) ) {
|
|
my ($id, $ts) = split(',', $line);
|
|
if ( !exists $all_instances{$id} ) {
|
|
$all_instances{$id} = { ts => $ts }; # original ts, not updated
|
|
}
|
|
}
|
|
}
|
|
|
|
open my $fh, '>', $vc_file or die "Cannot write to $vc_file: $OS_ERROR";
|
|
foreach my $id ( sort keys %all_instances ) {
|
|
PTDEBUG && _d('Updated:', $id, Dumper($all_instances{$id}));
|
|
print { $fh } $id . ',' . $all_instances{$id}->{ts} . "\n";
|
|
}
|
|
close $fh;
|
|
|
|
return;
|
|
}
|
|
|
|
sub get_instance_id {
|
|
my ($instance) = @_;
|
|
|
|
my $dbh = $instance->{dbh};
|
|
my $dsn = $instance->{dsn};
|
|
|
|
my $sql = q{SELECT CONCAT(@@hostname, @@port)};
|
|
PTDEBUG && _d($sql);
|
|
my ($name) = eval { $dbh->selectrow_array($sql) };
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d($EVAL_ERROR);
|
|
$sql = q{SELECT @@hostname};
|
|
PTDEBUG && _d($sql);
|
|
($name) = eval { $dbh->selectrow_array($sql) };
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d($EVAL_ERROR);
|
|
$name = ($dsn->{h} || 'localhost') . ($dsn->{P} || 3306);
|
|
}
|
|
else {
|
|
$sql = q{SHOW VARIABLES LIKE 'port'};
|
|
PTDEBUG && _d($sql);
|
|
my (undef, $port) = eval { $dbh->selectrow_array($sql) };
|
|
PTDEBUG && _d('port:', $port);
|
|
$name .= $port || '';
|
|
}
|
|
}
|
|
my $id = md5_hex($name);
|
|
|
|
PTDEBUG && _d('MySQL instance:', $id, $name, Dumper($dsn));
|
|
|
|
return $name, $id;
|
|
}
|
|
|
|
|
|
sub get_uuid {
|
|
my $uuid_file = '/.percona-toolkit.uuid';
|
|
foreach my $dir (@vc_dirs) {
|
|
my $filename = $dir.$uuid_file;
|
|
my $uuid=_read_uuid($filename);
|
|
return $uuid if $uuid;
|
|
}
|
|
|
|
my $filename = $ENV{"HOME"} . $uuid_file;
|
|
my $uuid = _generate_uuid();
|
|
|
|
my $fh;
|
|
eval {
|
|
open($fh, '>', $filename);
|
|
};
|
|
if (!$EVAL_ERROR) {
|
|
print $fh $uuid;
|
|
close $fh;
|
|
}
|
|
|
|
return $uuid;
|
|
}
|
|
|
|
sub _generate_uuid {
|
|
return sprintf+($}="%04x")."$}-$}-$}-$}-".$}x3,map rand 65537,0..7;
|
|
}
|
|
|
|
sub _read_uuid {
|
|
my $filename = shift;
|
|
my $fh;
|
|
|
|
eval {
|
|
open($fh, '<:encoding(UTF-8)', $filename);
|
|
};
|
|
return if ($EVAL_ERROR);
|
|
|
|
my $uuid;
|
|
eval { $uuid = <$fh>; };
|
|
return if ($EVAL_ERROR);
|
|
|
|
chomp $uuid;
|
|
return $uuid;
|
|
}
|
|
|
|
|
|
sub pingback {
|
|
my (%args) = @_;
|
|
my @required_args = qw(url instances);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my $url = $args{url};
|
|
my $instances = $args{instances};
|
|
|
|
my $ua = $args{ua} || HTTP::Micro->new( timeout => 3 );
|
|
|
|
my $response = $ua->request('GET', $url);
|
|
PTDEBUG && _d('Server response:', Dumper($response));
|
|
die "No response from GET $url"
|
|
if !$response;
|
|
die("GET on $url returned HTTP status $response->{status}; expected 200\n",
|
|
($response->{content} || '')) if $response->{status} != 200;
|
|
die("GET on $url did not return any programs to check")
|
|
if !$response->{content};
|
|
|
|
my $items = parse_server_response(
|
|
response => $response->{content}
|
|
);
|
|
die "Failed to parse server requested programs: $response->{content}"
|
|
if !scalar keys %$items;
|
|
|
|
my $versions = get_versions(
|
|
items => $items,
|
|
instances => $instances,
|
|
);
|
|
die "Failed to get any program versions; should have at least gotten Perl"
|
|
if !scalar keys %$versions;
|
|
|
|
my $client_content = encode_client_response(
|
|
items => $items,
|
|
versions => $versions,
|
|
general_id => get_uuid(),
|
|
);
|
|
|
|
my $tool_name = $ENV{XTRABACKUP_VERSION} ? "Percona XtraBackup" : File::Basename::basename($0);
|
|
my $client_response = {
|
|
headers => { "X-Percona-Toolkit-Tool" => $tool_name },
|
|
content => $client_content,
|
|
};
|
|
PTDEBUG && _d('Client response:', Dumper($client_response));
|
|
|
|
$response = $ua->request('POST', $url, $client_response);
|
|
PTDEBUG && _d('Server suggestions:', Dumper($response));
|
|
die "No response from POST $url $client_response"
|
|
if !$response;
|
|
die "POST $url returned HTTP status $response->{status}; expected 200"
|
|
if $response->{status} != 200;
|
|
|
|
return unless $response->{content};
|
|
|
|
$items = parse_server_response(
|
|
response => $response->{content},
|
|
split_vars => 0,
|
|
);
|
|
die "Failed to parse server suggestions: $response->{content}"
|
|
if !scalar keys %$items;
|
|
my @suggestions = map { $_->{vars} }
|
|
sort { $a->{item} cmp $b->{item} }
|
|
values %$items;
|
|
|
|
return \@suggestions;
|
|
}
|
|
|
|
sub encode_client_response {
|
|
my (%args) = @_;
|
|
my @required_args = qw(items versions general_id);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my ($items, $versions, $general_id) = @args{@required_args};
|
|
|
|
my @lines;
|
|
foreach my $item ( sort keys %$items ) {
|
|
next unless exists $versions->{$item};
|
|
if ( ref($versions->{$item}) eq 'HASH' ) {
|
|
my $mysql_versions = $versions->{$item};
|
|
for my $id ( sort keys %$mysql_versions ) {
|
|
push @lines, join(';', $id, $item, $mysql_versions->{$id});
|
|
}
|
|
}
|
|
else {
|
|
push @lines, join(';', $general_id, $item, $versions->{$item});
|
|
}
|
|
}
|
|
|
|
my $client_response = join("\n", @lines) . "\n";
|
|
return $client_response;
|
|
}
|
|
|
|
sub parse_server_response {
|
|
my (%args) = @_;
|
|
my @required_args = qw(response);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my ($response) = @args{@required_args};
|
|
|
|
my %items = map {
|
|
my ($item, $type, $vars) = split(";", $_);
|
|
if ( !defined $args{split_vars} || $args{split_vars} ) {
|
|
$vars = [ split(",", ($vars || '')) ];
|
|
}
|
|
$item => {
|
|
item => $item,
|
|
type => $type,
|
|
vars => $vars,
|
|
};
|
|
} split("\n", $response);
|
|
|
|
PTDEBUG && _d('Items:', Dumper(\%items));
|
|
|
|
return \%items;
|
|
}
|
|
|
|
my %sub_for_type = (
|
|
os_version => \&get_os_version,
|
|
perl_version => \&get_perl_version,
|
|
perl_module_version => \&get_perl_module_version,
|
|
mysql_variable => \&get_mysql_variable,
|
|
xtrabackup => \&get_xtrabackup_version,
|
|
);
|
|
|
|
sub valid_item {
|
|
my ($item) = @_;
|
|
return unless $item;
|
|
if ( !exists $sub_for_type{ $item->{type} } ) {
|
|
PTDEBUG && _d('Invalid type:', $item->{type});
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub get_versions {
|
|
my (%args) = @_;
|
|
my @required_args = qw(items);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my ($items) = @args{@required_args};
|
|
|
|
my %versions;
|
|
foreach my $item ( values %$items ) {
|
|
next unless valid_item($item);
|
|
eval {
|
|
my $version = $sub_for_type{ $item->{type} }->(
|
|
item => $item,
|
|
instances => $args{instances},
|
|
);
|
|
if ( $version ) {
|
|
chomp $version unless ref($version);
|
|
$versions{$item->{item}} = $version;
|
|
}
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
PTDEBUG && _d('Error getting version for', Dumper($item), $EVAL_ERROR);
|
|
}
|
|
}
|
|
|
|
return \%versions;
|
|
}
|
|
|
|
|
|
sub get_os_version {
|
|
if ( $OSNAME eq 'MSWin32' ) {
|
|
require Win32;
|
|
return Win32::GetOSDisplayName();
|
|
}
|
|
|
|
chomp(my $platform = `uname -s`);
|
|
PTDEBUG && _d('platform:', $platform);
|
|
return $OSNAME unless $platform;
|
|
|
|
chomp(my $lsb_release
|
|
= `which lsb_release 2>/dev/null | awk '{print \$1}'` || '');
|
|
PTDEBUG && _d('lsb_release:', $lsb_release);
|
|
|
|
my $release = "";
|
|
|
|
if ( $platform eq 'Linux' ) {
|
|
if ( -f "/etc/fedora-release" ) {
|
|
$release = `cat /etc/fedora-release`;
|
|
}
|
|
elsif ( -f "/etc/redhat-release" ) {
|
|
$release = `cat /etc/redhat-release`;
|
|
}
|
|
elsif ( -f "/etc/system-release" ) {
|
|
$release = `cat /etc/system-release`;
|
|
}
|
|
elsif ( $lsb_release ) {
|
|
$release = `$lsb_release -ds`;
|
|
}
|
|
elsif ( -f "/etc/lsb-release" ) {
|
|
$release = `grep DISTRIB_DESCRIPTION /etc/lsb-release`;
|
|
$release =~ s/^\w+="([^"]+)".+/$1/;
|
|
}
|
|
elsif ( -f "/etc/debian_version" ) {
|
|
chomp(my $rel = `cat /etc/debian_version`);
|
|
$release = "Debian $rel";
|
|
if ( -f "/etc/apt/sources.list" ) {
|
|
chomp(my $code_name = `awk '/^deb/ {print \$3}' /etc/apt/sources.list | awk -F/ '{print \$1}'| awk 'BEGIN {FS="|"} {print \$1}' | sort | uniq -c | sort -rn | head -n1 | awk '{print \$2}'`);
|
|
$release .= " ($code_name)" if $code_name;
|
|
}
|
|
}
|
|
elsif ( -f "/etc/os-release" ) { # openSUSE
|
|
chomp($release = `grep PRETTY_NAME /etc/os-release`);
|
|
$release =~ s/^PRETTY_NAME="(.+)"$/$1/;
|
|
}
|
|
elsif ( `ls /etc/*release 2>/dev/null` ) {
|
|
if ( `grep DISTRIB_DESCRIPTION /etc/*release 2>/dev/null` ) {
|
|
$release = `grep DISTRIB_DESCRIPTION /etc/*release | head -n1`;
|
|
}
|
|
else {
|
|
$release = `cat /etc/*release | head -n1`;
|
|
}
|
|
}
|
|
}
|
|
elsif ( $platform =~ m/(?:BSD|^Darwin)$/ ) {
|
|
my $rel = `uname -r`;
|
|
$release = "$platform $rel";
|
|
}
|
|
elsif ( $platform eq "SunOS" ) {
|
|
my $rel = `head -n1 /etc/release` || `uname -r`;
|
|
$release = "$platform $rel";
|
|
}
|
|
|
|
if ( !$release ) {
|
|
PTDEBUG && _d('Failed to get the release, using platform');
|
|
$release = $platform;
|
|
}
|
|
chomp($release);
|
|
|
|
$release =~ s/^"|"$//g;
|
|
|
|
PTDEBUG && _d('OS version =', $release);
|
|
return $release;
|
|
}
|
|
|
|
sub get_perl_version {
|
|
my (%args) = @_;
|
|
my $item = $args{item};
|
|
return unless $item;
|
|
|
|
my $version = sprintf '%vd', $PERL_VERSION;
|
|
PTDEBUG && _d('Perl version', $version);
|
|
return $version;
|
|
}
|
|
|
|
sub get_xtrabackup_version {
|
|
return $ENV{XTRABACKUP_VERSION};
|
|
}
|
|
|
|
sub get_perl_module_version {
|
|
my (%args) = @_;
|
|
my $item = $args{item};
|
|
return unless $item;
|
|
|
|
my $var = '$' . $item->{item} . '::VERSION';
|
|
my $version = eval "use $item->{item}; $var;";
|
|
PTDEBUG && _d('Perl version for', $var, '=', $version);
|
|
return $version;
|
|
}
|
|
|
|
sub get_mysql_variable {
|
|
return get_from_mysql(
|
|
show => 'VARIABLES',
|
|
@_,
|
|
);
|
|
}
|
|
|
|
sub get_from_mysql {
|
|
my (%args) = @_;
|
|
my $show = $args{show};
|
|
my $item = $args{item};
|
|
my $instances = $args{instances};
|
|
return unless $show && $item;
|
|
|
|
if ( !$instances || !@$instances ) {
|
|
PTDEBUG && _d('Cannot check', $item,
|
|
'because there are no MySQL instances');
|
|
return;
|
|
}
|
|
|
|
if ($item->{item} eq 'MySQL' && $item->{type} eq 'mysql_variable') {
|
|
@{$item->{vars}} = grep { $_ eq 'version' || $_ eq 'version_comment' } @{$item->{vars}};
|
|
}
|
|
|
|
|
|
my @versions;
|
|
my %version_for;
|
|
foreach my $instance ( @$instances ) {
|
|
next unless $instance->{id}; # special system instance has id=0
|
|
my $dbh = $instance->{dbh};
|
|
local $dbh->{FetchHashKeyName} = 'NAME_lc';
|
|
my $sql = qq/SHOW $show/;
|
|
PTDEBUG && _d($sql);
|
|
my $rows = $dbh->selectall_hashref($sql, 'variable_name');
|
|
|
|
my @versions;
|
|
foreach my $var ( @{$item->{vars}} ) {
|
|
$var = lc($var);
|
|
my $version = $rows->{$var}->{value};
|
|
PTDEBUG && _d('MySQL version for', $item->{item}, '=', $version,
|
|
'on', $instance->{name});
|
|
push @versions, $version;
|
|
}
|
|
$version_for{ $instance->{id} } = join(' ', @versions);
|
|
}
|
|
|
|
return \%version_for;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End VersionCheck package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# Runtime package
|
|
# This package is a copy without comments from the original. The original
|
|
# with comments and its test file can be found in the GitHub repository at,
|
|
# lib/Runtime.pm
|
|
# t/lib/Runtime.t
|
|
# See https://github.com/percona/percona-toolkit for more information.
|
|
# ###########################################################################
|
|
{
|
|
package Runtime;
|
|
|
|
use strict;
|
|
use warnings FATAL => 'all';
|
|
use English qw(-no_match_vars);
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
sub new {
|
|
my ( $class, %args ) = @_;
|
|
my @required_args = qw(now);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless exists $args{$arg};
|
|
}
|
|
|
|
my $run_time = $args{run_time};
|
|
if ( defined $run_time ) {
|
|
die "run_time must be > 0" if $run_time <= 0;
|
|
}
|
|
|
|
my $now = $args{now};
|
|
die "now must be a callback" unless ref $now eq 'CODE';
|
|
|
|
my $self = {
|
|
run_time => $run_time,
|
|
now => $now,
|
|
start_time => undef,
|
|
end_time => undef,
|
|
time_left => undef,
|
|
stop => 0,
|
|
};
|
|
|
|
return bless $self, $class;
|
|
}
|
|
|
|
sub time_left {
|
|
my ( $self, %args ) = @_;
|
|
|
|
if ( $self->{stop} ) {
|
|
PTDEBUG && _d("No time left because stop was called");
|
|
return 0;
|
|
}
|
|
|
|
my $now = $self->{now}->(%args);
|
|
PTDEBUG && _d("Current time:", $now);
|
|
|
|
if ( !defined $self->{start_time} ) {
|
|
$self->{start_time} = $now;
|
|
}
|
|
|
|
return unless defined $now;
|
|
|
|
my $run_time = $self->{run_time};
|
|
return unless defined $run_time;
|
|
|
|
if ( !$self->{end_time} ) {
|
|
$self->{end_time} = $now + $run_time;
|
|
PTDEBUG && _d("End time:", $self->{end_time});
|
|
}
|
|
|
|
$self->{time_left} = $self->{end_time} - $now;
|
|
PTDEBUG && _d("Time left:", $self->{time_left});
|
|
return $self->{time_left};
|
|
}
|
|
|
|
sub have_time {
|
|
my ( $self, %args ) = @_;
|
|
my $time_left = $self->time_left(%args);
|
|
return 1 if !defined $time_left; # run forever
|
|
return $time_left <= 0 ? 0 : 1; # <=0s means run time has elapsed
|
|
}
|
|
|
|
sub time_elapsed {
|
|
my ( $self, %args ) = @_;
|
|
|
|
my $start_time = $self->{start_time};
|
|
return 0 unless $start_time;
|
|
|
|
my $now = $self->{now}->(%args);
|
|
PTDEBUG && _d("Current time:", $now);
|
|
|
|
my $time_elapsed = $now - $start_time;
|
|
PTDEBUG && _d("Time elapsed:", $time_elapsed);
|
|
if ( $time_elapsed < 0 ) {
|
|
warn "Current time $now is earlier than start time $start_time";
|
|
}
|
|
return $time_elapsed;
|
|
}
|
|
|
|
sub reset {
|
|
my ( $self ) = @_;
|
|
$self->{start_time} = undef;
|
|
$self->{end_time} = undef;
|
|
$self->{time_left} = undef;
|
|
$self->{stop} = 0;
|
|
PTDEBUG && _d("Reset run time");
|
|
return;
|
|
}
|
|
|
|
sub stop {
|
|
my ( $self ) = @_;
|
|
$self->{stop} = 1;
|
|
return;
|
|
}
|
|
|
|
sub start {
|
|
my ( $self ) = @_;
|
|
$self->{stop} = 0;
|
|
return;
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
1;
|
|
}
|
|
# ###########################################################################
|
|
# End Runtime package
|
|
# ###########################################################################
|
|
|
|
# ###########################################################################
|
|
# This is a combination of modules and programs in one -- a runnable module.
|
|
# http://www.perl.com/pub/a/2006/07/13/lightning-articles.html?page=last
|
|
# Or, look it up in the Camel book on pages 642 and 643 in the 3rd edition.
|
|
#
|
|
# Check at the end of this package for the call to main() which actually runs
|
|
# the program.
|
|
# ###########################################################################
|
|
package pt_deadlock_logger;
|
|
|
|
use English qw(-no_match_vars);
|
|
use List::Util qw(max);
|
|
use Socket qw(inet_aton);
|
|
use Time::HiRes qw(sleep);
|
|
use File::Temp qw(tempfile);
|
|
use File::Spec;
|
|
|
|
use sigtrap 'handler', \&sig_int, 'normal-signals';
|
|
|
|
use Percona::Toolkit;
|
|
use constant PTDEBUG => $ENV{PTDEBUG} || 0;
|
|
|
|
# Some common patterns and variables
|
|
my $d = qr/(\d+)/; # Digit
|
|
my $t = qr/((?:\d+ \d+)|(?:[A-Fa-f0-9]+))/; # Transaction ID
|
|
my $i = qr/((?:\d{1,3}\.){3}\d+)/; # IP address
|
|
my $n = qr/([^`\s]+)/; # MySQL object name
|
|
my $u = qr/(\S+)/; # Username. This is somewhat wrong, but
|
|
# usernames with spaces are rare enough.
|
|
my $s = qr/((?:\d{6}|\d{4}-\d\d-\d\d) .\d:\d\d:\d\d)(?: [xA-Fa-f0-9]+)?/; # InnoDB timestamp
|
|
|
|
# A thread's proc_info can be at least 98 different things I've found in the
|
|
# source. Fortunately, most of them begin with a gerunded verb. These are
|
|
# the ones that don't.
|
|
my %is_proc_info = (
|
|
'After create' => 1,
|
|
'Execution of init_command' => 1,
|
|
'FULLTEXT initialization' => 1,
|
|
'Reopen tables' => 1,
|
|
'Repair done' => 1,
|
|
'Repair with keycache' => 1,
|
|
'System lock' => 1,
|
|
'Table lock' => 1,
|
|
'Thread initialized' => 1,
|
|
'User lock' => 1,
|
|
'copy to tmp table' => 1,
|
|
'discard_or_import_tablespace' => 1,
|
|
'end' => 1,
|
|
'got handler lock' => 1,
|
|
'got old table' => 1,
|
|
'init' => 1,
|
|
'key cache' => 1,
|
|
'locks' => 1,
|
|
'malloc' => 1,
|
|
'query end' => 1,
|
|
'rename result table' => 1,
|
|
'rename' => 1,
|
|
'setup' => 1,
|
|
'statistics' => 1,
|
|
'status' => 1,
|
|
'table cache' => 1,
|
|
'update' => 1,
|
|
);
|
|
|
|
my $oktorun = 1;
|
|
my $exit_status = 0;
|
|
|
|
sub main {
|
|
local @ARGV = @_; # set global ARGV for this package
|
|
$oktorun = 1;
|
|
$exit_status = 0;
|
|
|
|
# ########################################################################
|
|
# Get configuration information.
|
|
# ########################################################################
|
|
my $o = new OptionParser();
|
|
$o->get_specs();
|
|
$o->get_opts();
|
|
|
|
my $dp = $o->DSNParser();
|
|
$dp->prop('set-vars', $o->set_vars());
|
|
|
|
my $src = Cxn->new(
|
|
dsn_string => shift @ARGV,
|
|
parent => $o->get('daemonize'),
|
|
DSNParser => $dp,
|
|
OptionParser => $o,
|
|
);
|
|
|
|
my $dst;
|
|
if ( my $dst_dsn = $o->get('dest') ) {
|
|
|
|
# set time_zone = SYSTEM , addresses https://bugs.launchpad.net/percona-toolkit/+bug/1295667
|
|
my $set_tz = sub {
|
|
my ($dbh) = @_;
|
|
my $sql = "SET time_zone=SYSTEM /* pt-deadlock-logger */";
|
|
eval {
|
|
PTDEBUG && _d($dbh, $sql);
|
|
$dbh->do($sql);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
die "Failed to $sql: $EVAL_ERROR\n";
|
|
}
|
|
};
|
|
|
|
$dst = Cxn->new(
|
|
dsn => $dst_dsn,
|
|
prev_dsn => ($src ? $src->dsn : undef),
|
|
parent => $o->get('daemonize'),
|
|
DSNParser => $dp,
|
|
OptionParser => $o,
|
|
set => $set_tz,
|
|
);
|
|
}
|
|
|
|
if ( !$o->get('help') ) {
|
|
if ( !$src ) {
|
|
$o->save_error('No DSN was specified.');
|
|
}
|
|
if ( $dst && !$dst->dsn->{D} ) {
|
|
$o->save_error("--dest requires a 'D' (database) part.");
|
|
}
|
|
if ( $dst && !$dst->dsn->{t} ) {
|
|
$o->save_error("--dest requires a 't' (table) part.");
|
|
}
|
|
}
|
|
|
|
$o->usage_or_errors();
|
|
|
|
# ########################################################################
|
|
# Connect to MySQL and set up the --dest, if any.
|
|
# ########################################################################
|
|
my $q = new Quoter();
|
|
|
|
$src->connect();
|
|
|
|
my @cols = @{ $o->get('columns') };
|
|
my $ins_sth;
|
|
my $ins_sql;
|
|
if ( $dst ) {
|
|
$dst->connect(AutoCommit => 0);
|
|
|
|
my $db_tbl = $q->join_quote($dst->dsn->{D}, $dst->dsn->{t});
|
|
my $cols = join(',', map { $q->quote($_) } @cols);
|
|
my $parms = join(',', map { '?' } @cols);
|
|
$ins_sql = "INSERT IGNORE INTO $db_tbl ($cols) VALUES ($parms) "
|
|
. "/* pt-deadlock-logger */";
|
|
PTDEBUG && _d($ins_sql);
|
|
$ins_sth = $dst->dbh->prepare($ins_sql);
|
|
|
|
if ( $o->get('create-dest-table') ) {
|
|
my $sql = $o->read_para_after(__FILE__, qr/MAGIC_dest_table/);
|
|
$sql =~ s/deadlocks/IF NOT EXISTS $db_tbl/;
|
|
PTDEBUG && _d($sql);
|
|
$dst->dbh->do($sql);
|
|
if ( $EVAL_ERROR ) {
|
|
_die("Cannot connect to MySQL: $EVAL_ERROR", 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
# ########################################################################
|
|
# Daemonize only after (potentially) asking for passwords for --ask-pass.
|
|
# If option daemonize is not provided while option pid is provided,
|
|
# we're not daemoninzing, it just handles PID stuff.
|
|
# ########################################################################
|
|
my $daemon;
|
|
if ( $o->get('daemonize') || $o->get('pid')) {
|
|
$daemon = new Daemon(
|
|
log_file => $o->get('log'),
|
|
pid_file => $o->get('pid'),
|
|
daemonize => $o->get('daemonize'),
|
|
);
|
|
$daemon->run();
|
|
PTDEBUG && $o->get('daemonize') && _d('I am a daemon now');
|
|
}
|
|
|
|
# If we daemonized, the parent has already exited and we're the child.
|
|
# We shared a copy of every Cxn with the parent, and the parent's copies
|
|
# were destroyed but the dbhs were not disconnected because the parent
|
|
# attrib was true. Now, as the child, set it false so the dbhs will be
|
|
# disconnected when our Cxn copies are destroyed. If we didn't daemonize,
|
|
# then we're not really a parent (since we have no children), so set it
|
|
# false to auto-disconnect the dbhs when our Cxns are destroyed.
|
|
$src->{parent} = 0;
|
|
$dst->{parent} = 0 if $dst;
|
|
|
|
# ########################################################################
|
|
# Do the version-check
|
|
# ########################################################################
|
|
if ( $o->get('version-check') && (!$o->has('quiet') || !$o->get('quiet')) ) {
|
|
VersionCheck::version_check(
|
|
force => $o->got('version-check'),
|
|
instances => [
|
|
{ dbh => $src->dbh, dsn => $src->dsn },
|
|
($dst ? { dbh => $dst->dbh, dsn => $dst->dsn } : ())
|
|
],
|
|
);
|
|
}
|
|
|
|
# ########################################################################
|
|
# Set upt the --clear-deadlocks table.
|
|
# ########################################################################
|
|
my $clear_deadlocks_table_def;
|
|
my $clear_deadlocks_table = $o->get('clear-deadlocks');
|
|
if ( $clear_deadlocks_table ) {
|
|
$clear_deadlocks_table_def
|
|
= $o->read_para_after(__FILE__, qr/MAGIC_clear_deadlocks/);
|
|
if ( VersionParser->new($src->dbh) < '4.1.2') {
|
|
$clear_deadlocks_table_def =~ s/ENGINE=/TYPE=/;
|
|
}
|
|
$clear_deadlocks_table_def
|
|
=~ s/percona_schema.clear_deadlocks/$clear_deadlocks_table/;
|
|
PTDEBUG && _d('--clear-deadlocks table:', $clear_deadlocks_table_def);
|
|
}
|
|
|
|
# ########################################################################
|
|
# Start looking for and logging deadlocks.
|
|
# ########################################################################
|
|
my $sep = $o->get('tab') ? "\t" : ' ';
|
|
my $last_fingerprint = '';
|
|
my $parse_deadlocks_options = {
|
|
'server' => $src->dsn->{h} || $src->{hostname},
|
|
'numeric-ip' => $o->got('numeric-ip'),
|
|
};
|
|
|
|
my $run_time = Runtime->new(
|
|
run_time => $o->get('run-time'),
|
|
now => sub { return time },
|
|
);
|
|
|
|
my $interval = $o->get('interval');
|
|
my $iters = $o->get('iterations');
|
|
PTDEBUG && _d('iterations:', $iters, 'interval:', $interval);
|
|
|
|
ITERATION:
|
|
while (
|
|
$oktorun
|
|
&& $run_time->have_time()
|
|
&& (!defined $iters || $iters--)
|
|
) {
|
|
|
|
my %txns;
|
|
my $fingerprint;
|
|
eval {
|
|
my $sql = "SHOW /*!40100 ENGINE*/ INNODB STATUS "
|
|
. "/* pt-deadlock-logger */";
|
|
my $text = $src->dbh->selectrow_hashref($sql)->{status};
|
|
|
|
%txns = %{parse_deadlocks($text, $parse_deadlocks_options)};
|
|
$fingerprint = fingerprint(\%txns);
|
|
};
|
|
if ( my $e = $EVAL_ERROR ) {
|
|
PTDEBUG && _d('Error getting InnoDB status:', $e);
|
|
if ( $src->lost_connection($e) ) {
|
|
eval { $src->connect() };
|
|
if ( $EVAL_ERROR ) {
|
|
warn "Lost connection to " . $src->name . ". Will try "
|
|
. "to reconnect in the next iteration.\n";
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Reconnected to MySQL');
|
|
redo ITERATION;
|
|
}
|
|
}
|
|
else {
|
|
warn "Error getting SHOW ENGINE INNODB STATUS: $EVAL_ERROR";
|
|
$exit_status |= 1;
|
|
}
|
|
}
|
|
else {
|
|
if ( $fingerprint ne $last_fingerprint ) {
|
|
PTDEBUG && _d('New deadlock');
|
|
|
|
if ( $ins_sth ) {
|
|
eval {
|
|
PTDEBUG && _d('Saving deadlock to --dest');
|
|
foreach my $txn (
|
|
sort { $a->{thread} <=> $b->{thread} } values %txns
|
|
) {
|
|
$ins_sth->execute(@{$txn}{@cols});
|
|
}
|
|
$dst->dbh->commit();
|
|
};
|
|
if ( my $e = $EVAL_ERROR ) {
|
|
PTDEBUG && _d('Error saving to --dest:', $e);
|
|
if ( $dst->lost_connection($e) ) {
|
|
eval {
|
|
$ins_sth->finish() if $ins_sth;
|
|
$dst->dbh->disconnect() if $dst->dbh;
|
|
$dst->connect(AutoCommit => 0);
|
|
$ins_sth = $dst->dbh->prepare($ins_sql);
|
|
};
|
|
if ( $EVAL_ERROR ) {
|
|
warn "Lost connection to " . $dst->name . ". Will try "
|
|
. "to reconnect in the next iteration.\n";
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Reconnected to MySQL (--dest)');
|
|
redo ITERATION;
|
|
}
|
|
}
|
|
else {
|
|
warn "Error saving to --dest: $EVAL_ERROR";
|
|
$exit_status |= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !$o->get('quiet') ) {
|
|
print join($sep, @cols), "\n";
|
|
foreach my $txn (
|
|
sort { $a->{thread} <=> $b->{thread} } values %txns
|
|
) {
|
|
$txn->{query} =~ s/\s+/ /g;
|
|
print join($sep, map { $txn->{$_} } @cols), "\n";
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
PTDEBUG && _d('Same deadlock, not printing');
|
|
}
|
|
|
|
$last_fingerprint = $fingerprint;
|
|
|
|
if ( $clear_deadlocks_table ) {
|
|
clear_deadlocks(
|
|
dsn => $src->dsn,
|
|
table => $clear_deadlocks_table,
|
|
table_def => $clear_deadlocks_table_def,
|
|
DSNParser => $dp,
|
|
);
|
|
}
|
|
}
|
|
|
|
# Sleep if there's an --iteration left.
|
|
if ( !defined $iters || $iters ) {
|
|
PTDEBUG && _d('Sleeping', $interval, 'seconds');
|
|
sleep $interval;
|
|
}
|
|
}
|
|
|
|
PTDEBUG && _d('Done running, exiting', $exit_status);
|
|
return $exit_status;
|
|
}
|
|
|
|
# ############################################################################
|
|
# Subroutines
|
|
# ############################################################################
|
|
|
|
sub parse_deadlocks {
|
|
my ( $text, $args ) = @_;
|
|
$args ||= {};
|
|
# Pull out the deadlock section
|
|
my $dl_text;
|
|
my @matches = $text =~ m#\n(---+)\n([A-Z /]+)\n\1\n(.*?)(?=\n(---+)\n[A-Z /]+\n\4\n|$)#gs;
|
|
while ( my ( $start, $name, $text, $end ) = splice(@matches, 0, 4) ) {
|
|
next unless $name eq 'LATEST DETECTED DEADLOCK';
|
|
$dl_text = $text;
|
|
last;
|
|
}
|
|
|
|
return {} unless $dl_text;
|
|
|
|
my @sections
|
|
= $dl_text
|
|
=~ m{
|
|
^\*{3}\s([^\n]*) # *** (1) WAITING FOR THIS...
|
|
(.*?) # Followed by anything, non-greedy
|
|
(?=(?:^\*{3})|\z) # Followed by another three-stars or EOF
|
|
}gmsx;
|
|
|
|
# Loop through each section. There are no assumptions about how many
|
|
# there are, who holds and wants what locks, and who gets rolled back.
|
|
my %txns;
|
|
while ( my ($header, $body) = splice(@sections, 0, 2) ) {
|
|
my ( $txn_id, $what ) = $header =~ m/^\($d\) (.*):$/m;
|
|
next unless $txn_id;
|
|
$txns{$txn_id} ||= { id => $txn_id };
|
|
my $hash = $txns{$txn_id};
|
|
|
|
if ( $what eq 'TRANSACTION' ) {
|
|
@{$hash}{qw(txn_time)} = $body =~ m/ACTIVE $d sec/;
|
|
|
|
# Parsing the line that begins 'MySQL thread id' is complicated.
|
|
# The only thing always in the line is the thread and query id.
|
|
# See function innobase_mysql_print_thd in InnoDB source file
|
|
# sql/ha_innodb.cc.
|
|
my ( $thread_line ) = $body =~ m/^(MySQL thread id .*)$/m;
|
|
my ($mysql_thread_id, $query_id, $hostname, $ip, $user, $query_status);
|
|
if ( $thread_line ) {
|
|
# These parts can always be gotten.
|
|
( $mysql_thread_id, $query_id )
|
|
= $thread_line =~ m/^MySQL thread id $d,.* query id $d/m;
|
|
# If it's a master/slave thread, "Has (read|sent) all" may be the
|
|
# thread's proc_info. In these cases, there won't be any
|
|
# host/ip/user info.
|
|
( $query_status ) = $thread_line =~ m/(Has (?:read|sent) all .*$)/m;
|
|
if ( defined($query_status) ) {
|
|
$user = 'system user';
|
|
}
|
|
|
|
# The query id might be the last thing in the line.
|
|
elsif ( $thread_line =~ m/query id \d+ / ) {
|
|
# The IP address is the only non-word thing left, so it's
|
|
# the most useful marker for where I have to start guessing.
|
|
( $hostname, $ip ) = $thread_line =~ m/query id \d+(?: ([A-Za-z]\S+))? $i/m;
|
|
if ( defined $ip ) {
|
|
($user, $query_status) = $thread_line =~ m/$ip $u(?: (.*))?$/;
|
|
}
|
|
else {
|
|
# OK, there wasn't an IP address.
|
|
# There might not be ANYTHING except the query status.
|
|
( $query_status ) = $thread_line =~ m/query id \d+ (.*)$/;
|
|
if ( $query_status !~ m/^\w+ing/
|
|
&& !exists($is_proc_info{$query_status}) ) {
|
|
# The remaining tokens are, in order: hostname, user,
|
|
# query_status.
|
|
# It's basically impossible to know which is which.
|
|
( $hostname, $user, $query_status ) = $thread_line
|
|
=~ m/query id \d+(?: ([A-Za-z]\S+))?(?: $u(?: (.*))?)?$/m;
|
|
}
|
|
else {
|
|
$user = 'system user';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
my ( $query_text ) = $body =~ m/\nMySQL thread id .*\n((?s).*)/;
|
|
$query_text =~ s/\s+$//;
|
|
$query_text =~ s/\s+/ /g;
|
|
|
|
@{$hash}{qw(thread hostname ip user query)}
|
|
= ($mysql_thread_id, $hostname, $ip, $user, $query_text);
|
|
foreach my $key ( keys %$hash ) {
|
|
if ( !defined $hash->{$key} ) {
|
|
$hash->{$key} = '';
|
|
}
|
|
}
|
|
|
|
}
|
|
else {
|
|
# Prefer information about locks waited-for over locks-held.
|
|
if ( $what eq 'WAITING FOR THIS LOCK TO BE GRANTED' || !$hash->{lock_type} ) {
|
|
$hash->{wait_hold} = $what eq 'WAITING FOR THIS LOCK TO BE GRANTED' ? 'w' : 'h';
|
|
@{$hash}{ qw(lock_type idx db tbl txn_id lock_mode) }
|
|
= $body
|
|
=~ m{^(RECORD|TABLE) LOCKS? (?:space id \d+ page no \d+ n bits \d+ index `?$n`? of )?table `$n(?:/|`\.`)$n`.*?trx id $t lock.mode (\S+)}m;
|
|
if ( $hash->{txn_id} ) {
|
|
my ( $high, $low ) = $hash->{txn_id} =~ m/^(\d+) (\d+)$/;
|
|
$hash->{txn_id} = $high ? ( $low + ($high << 32) ) : $low;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Ensure all values are defined
|
|
map { $hash->{$_} = 0 unless defined $hash->{$_} }
|
|
qw(thread txn_id txn_time);
|
|
map { $hash->{$_} = '' unless defined $hash->{$_} }
|
|
qw(user hostname db tbl idx lock_type lock_mode query);
|
|
}
|
|
|
|
# Extract some miscellaneous data from the deadlock.
|
|
my ( $ts ) = $dl_text =~ m/^$s$/m;
|
|
if ( !$ts ) {
|
|
# https://bugs.launchpad.net/percona-toolkit/+bug/1195034
|
|
# 130624 17:39:24TOO DEEP OR LONG SEARCH IN THE LOCK TABLE ...
|
|
($ts) = $dl_text =~ m/^${s}TOO DEEP/m;
|
|
}
|
|
my ( $year, $mon, $day, $hour, $min, $sec ) = $ts =~ m/^((?:\d\d)?\d\d)-?(\d\d)-?(\d\d) +(\d+):(\d+):(\d+)$/;
|
|
if ( length($year) == 2 ) {
|
|
$year += 2000;
|
|
}
|
|
$ts = sprintf('%02d-%02d-%02dT%02d:%02d:%02d', $year, $mon, $day, $hour, $min, $sec);
|
|
my ( $victim ) = $dl_text =~ m/^\*\*\* WE ROLL BACK TRANSACTION \((\d+)\)$/m;
|
|
$victim ||= 0;
|
|
|
|
# Stick the misc data into the transactions.
|
|
foreach my $txn ( values %txns ) {
|
|
$txn->{victim} = $txn->{id} == $victim ? 1 : 0;
|
|
$txn->{ts} = $ts;
|
|
$txn->{server} = $args->{server} || '';
|
|
$txn->{ip} = inet_aton($txn->{ip}) if $args->{'numeric-ip'};
|
|
}
|
|
|
|
return \%txns;
|
|
}
|
|
|
|
sub clear_deadlocks {
|
|
my (%args) = @_;
|
|
my @required_args = qw(dsn table table_def DSNParser);
|
|
foreach my $arg ( @required_args ) {
|
|
die "I need a $arg argument" unless $args{$arg};
|
|
}
|
|
my $dsn = $args{dsn};
|
|
my $table = $args{table};
|
|
my $table_def = $args{table_def};
|
|
my $dp = $args{DSNParser};
|
|
PTDEBUG && _d('Clearing deadlocks with table', $table, $table_def);
|
|
|
|
my $parent_dbh = $dp->get_dbh($dp->get_cxn_params($dsn), { AutoCommit=>0 });
|
|
$parent_dbh->{InactiveDestroy} = 1; # because of forking
|
|
|
|
# Create the deadlocks table.
|
|
PTDEBUG && _d($table_def);
|
|
$parent_dbh->do($table_def);
|
|
|
|
# Get a lock on it.
|
|
my $sql = "INSERT INTO $table (a) VALUES (1) "
|
|
. "/* pt-deadlock-logger clear deadlocks parent */";
|
|
PTDEBUG && _d($sql);
|
|
$parent_dbh->do($sql);
|
|
|
|
my ($sync_fh, $sync_file) = tempfile(
|
|
'pt-deadlock-logger-clear-deadlocks.XXXXXXX',
|
|
DIR => File::Spec->tmpdir(),
|
|
);
|
|
PTDEBUG && _d('Sync file:', $sync_file);
|
|
close $sync_fh;
|
|
unlink $sync_file;
|
|
|
|
# Fork a child to try to take a lock on the table.
|
|
my $pid = fork();
|
|
if ( defined($pid) && $pid == 0 ) {
|
|
# I am the child
|
|
PTDEBUG && _d('Clear deadlocks child', $PID);
|
|
my $child_dbh = $dp->get_dbh($dp->get_cxn_params($dsn), {AutoCommit=>0});
|
|
my $sql = "SELECT * FROM $table FOR UPDATE "
|
|
. "/* pt-deadlock-logger clear deadlocks child */";
|
|
PTDEBUG && _d($sql);
|
|
open my $fh, '>', $sync_file
|
|
or die "Error creating $sync_file: $OS_ERROR";
|
|
close $fh;
|
|
PTDEBUG && _d('Clear deadlocks child ready (child)');
|
|
eval { $child_dbh->do($sql); }; # Should block against parent.
|
|
PTDEBUG && _d($EVAL_ERROR); # Parent inserted value 0.
|
|
$child_dbh->commit();
|
|
$child_dbh->disconnect();
|
|
exit;
|
|
}
|
|
elsif ( !defined($pid) ) {
|
|
die "Failed to fork for --clear-deadlocks: " . ($OS_ERROR || '');
|
|
}
|
|
|
|
# Wait up to 10s for the child to connect and become ready.
|
|
for ( 1..40 ) {
|
|
last if -f $sync_file;
|
|
PTDEBUG && _d('Waiting for the clear deadlocks child');
|
|
sleep 0.25;
|
|
}
|
|
PTDEBUG && _d('Clear deadlocks child ready (parent)');
|
|
sleep 0.25; # wait for child to exec its SELECT statement
|
|
|
|
# Make the child deadlock.
|
|
$sql = "INSERT INTO $table (a) VALUES (0) "
|
|
. "/* pt-deadlock-logger clear deadlocks parent */";
|
|
PTDEBUG && _d($sql);
|
|
eval { $parent_dbh->do($sql); };
|
|
PTDEBUG && _d($EVAL_ERROR);
|
|
|
|
# Reap the child.
|
|
waitpid($pid, 0);
|
|
|
|
# Drop the table.
|
|
$sql = "DROP TABLE IF EXISTS $table";
|
|
PTDEBUG && _d($sql);
|
|
$parent_dbh->do($sql);
|
|
|
|
$parent_dbh->disconnect();
|
|
|
|
unlink $sync_file;
|
|
|
|
return;
|
|
}
|
|
|
|
sub fingerprint {
|
|
my ( $txns ) = @_;
|
|
my $fingerprint = '';
|
|
foreach my $txn ( sort { $a->{thread} <=> $b->{thread} } values %$txns ) {
|
|
$fingerprint = $fingerprint
|
|
. join('', map { $txn->{$_} } qw(server ts thread) );
|
|
}
|
|
PTDEBUG && _d('Deadlock fingerprint:', $fingerprint);
|
|
return $fingerprint;
|
|
}
|
|
|
|
sub sig_int {
|
|
my ( $signal ) = @_;
|
|
$oktorun = 0;
|
|
print STDERR "# Caught SIG$signal. Use 'kill -ABRT $PID' if "
|
|
. "the tool does not exit normally in a few seconds.\n";
|
|
}
|
|
|
|
sub _d {
|
|
my ($package, undef, $line) = caller 0;
|
|
@_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
|
|
map { defined $_ ? $_ : 'undef' }
|
|
@_;
|
|
print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
|
|
}
|
|
|
|
# ############################################################################
|
|
# Run the program.
|
|
# ############################################################################
|
|
if ( !caller ) { exit main(@ARGV); }
|
|
|
|
1; # Because this is a module as well as a script.
|
|
|
|
# ############################################################################
|
|
# Documentation
|
|
# ############################################################################
|
|
=pod
|
|
|
|
=head1 NAME
|
|
|
|
pt-deadlock-logger - Log MySQL deadlocks.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
Usage: pt-deadlock-logger [OPTIONS] DSN
|
|
|
|
pt-deadlock-logger logs information about MySQL deadlocks on the given
|
|
DSN. Information is printed to C<STDOUT>, and it can also be saved to a
|
|
table by specifying L<"--dest">. The tool runs for forever unless
|
|
L<"--run-time"> or L<"--iterations"> is specified.
|
|
|
|
Print deadlocks on host1:
|
|
|
|
pt-deadlock-logger h=host1
|
|
|
|
Print deadlocks on host1 once then exit:
|
|
|
|
pt-deadlock-logger h=host1 --iterations 1
|
|
|
|
Save deadlocks on host1 to percona_schema.deadlocks on host2:
|
|
|
|
pt-deadlock-logger h=host1 --dest h=host2,D=percona_schema,t=deadlocks
|
|
|
|
=head1 RISKS
|
|
|
|
Percona Toolkit is mature, proven in the real world, and well tested,
|
|
but all database tools can pose a risk to the system and the database
|
|
server. Before using this tool, please:
|
|
|
|
=over
|
|
|
|
=item * Read the tool's documentation
|
|
|
|
=item * Review the tool's known L<"BUGS">
|
|
|
|
=item * Test the tool on a non-production server
|
|
|
|
=item * Backup your production server and verify the backups
|
|
|
|
=back
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
pt-deadlock-logger prints information about MySQL deadlocks by polling
|
|
and parsing C<SHOW ENGINE INNODB STATUS>. When a new deadlock occurs,
|
|
it's printed to C<STDOUT> and, if specified, saved to L<"--dest">.
|
|
|
|
Only new deadlocks are printed. A fingerprint for each deadlock is created
|
|
using the deadlock's server, ts, and thread values (even if these
|
|
columns are not specified by L<"--columns">). A deadlock is printed if
|
|
its fingerprint is different than the last deadlock's fingerprint.
|
|
|
|
The L<"--dest"> statement uses C<INSERT IGNORE> to eliminate duplicate
|
|
deadlocks, so every deadlock is saved for every L<"--iterations">.
|
|
|
|
=head1 OUTPUT
|
|
|
|
New deadlocks are printed to C<STDOUT>, unless L<"--quiet"> is specified.
|
|
Errors and warnings are printed to C<STDERR>.
|
|
|
|
See also L<"--columns"> and L<"--tab">.
|
|
|
|
=head1 INNODB CAVEATS AND DETAILS
|
|
|
|
InnoDB's output is hard to parse and sometimes there's no way to do it right.
|
|
|
|
Sometimes not all information (for example, username or IP address) is included
|
|
in the deadlock information. In this case there's nothing for the tool to put
|
|
in those columns. It may also be the case that the deadlock output is so long
|
|
(because there were a lot of locks) that the whole thing is truncated.
|
|
|
|
Though there are usually two transactions involved in a deadlock, there are more
|
|
locks than that; at a minimum, one more lock than transactions is necessary to
|
|
create a cycle in the waits-for graph. pt-deadlock-logger prints the
|
|
transactions (always two in the InnoDB output, even when there are more
|
|
transactions in the waits-for graph than that) and fills in locks. It prefers
|
|
waited-for over held when choosing lock information to output, but you can
|
|
figure out the rest with a moment's thought. If you see one wait-for and one
|
|
held lock, you're looking at the same lock, so of course you'd prefer to see
|
|
both wait-for locks and get more information. If the two waited-for locks are
|
|
not on the same table, more than two transactions were involved in the deadlock.
|
|
|
|
Finally, keep in mind that, because usernames with spaces are not quoted by
|
|
InnoDB, the tool will generally misreport the second word of these usernames
|
|
as the hostname.
|
|
|
|
=head1 OPTIONS
|
|
|
|
This tool accepts additional command-line arguments. Refer to the
|
|
L<"SYNOPSIS"> and usage information for details.
|
|
|
|
=over
|
|
|
|
=item --ask-pass
|
|
|
|
Prompt for a password when connecting to MySQL.
|
|
|
|
=item --charset
|
|
|
|
short form: -A; type: string
|
|
|
|
Default character set. If the value is utf8, sets Perl's binmode on
|
|
STDOUT to utf8, passes the mysql_enable_utf8 option to DBD::mysql, and runs SET
|
|
NAMES UTF8 after connecting to MySQL. Any other value sets binmode on STDOUT
|
|
without the utf8 layer, and runs SET NAMES after connecting to MySQL.
|
|
|
|
=item --clear-deadlocks
|
|
|
|
type: string
|
|
|
|
Use this table to create a small deadlock. This usually has the effect of
|
|
clearing out a huge deadlock, which otherwise consumes the entire output of
|
|
C<SHOW INNODB STATUS>. The table must not exist. pt-deadlock-logger will
|
|
create it with the following structure:
|
|
|
|
=for comment ignore-pt-internal-value
|
|
MAGIC_clear_deadlocks
|
|
|
|
CREATE TABLE percona_schema.clear_deadlocks (
|
|
a INT PRIMARY KEY
|
|
) ENGINE=InnoDB
|
|
|
|
After creating the table and causing a small deadlock, the tool will drop the
|
|
table again.
|
|
|
|
=item --columns
|
|
|
|
type: Array; default: server, ts, thread, txn_id, txn_time, user, hostname, ip, db, tbl, idx, lock_type, lock_mode, wait_hold, victim, query
|
|
|
|
The columns are:
|
|
|
|
=over
|
|
|
|
=item server
|
|
|
|
The (source) server on which the deadlock occurred. This might be useful if
|
|
you're tracking deadlocks on many servers.
|
|
|
|
=item ts
|
|
|
|
The date and time of the last detected deadlock.
|
|
|
|
=item thread
|
|
|
|
The MySQL thread number, which is the same as the connection ID in SHOW FULL
|
|
PROCESSLIST.
|
|
|
|
=item txn_id
|
|
|
|
The InnoDB transaction ID, which InnoDB expresses as two unsigned integers. The tool
|
|
multiplies them out to be one number.
|
|
|
|
=item txn_time
|
|
|
|
How long the transaction was active when the deadlock happened.
|
|
|
|
=item user
|
|
|
|
The connection's database username.
|
|
|
|
=item hostname
|
|
|
|
The connection's host.
|
|
|
|
=item ip
|
|
|
|
The connection's IP address. If you specify L<"--numeric-ip">, this is
|
|
converted to an unsigned integer.
|
|
|
|
=item db
|
|
|
|
The database in which the deadlock occurred.
|
|
|
|
=item tbl
|
|
|
|
The table on which the deadlock occurred.
|
|
|
|
=item idx
|
|
|
|
The index on which the deadlock occurred.
|
|
|
|
=item lock_type
|
|
|
|
The lock type the transaction held on the lock that caused the deadlock.
|
|
|
|
=item lock_mode
|
|
|
|
The lock mode of the lock that caused the deadlock.
|
|
|
|
=item wait_hold
|
|
|
|
Whether the transaction was waiting for the lock or holding the lock. Usually
|
|
you will see the two waited-for locks.
|
|
|
|
=item victim
|
|
|
|
Whether the transaction was selected as the deadlock victim and rolled back.
|
|
|
|
=item query
|
|
|
|
The query that caused the deadlock.
|
|
|
|
=back
|
|
|
|
=item --config
|
|
|
|
type: Array
|
|
|
|
Read this comma-separated list of config files; if specified, this must be the
|
|
first option on the command line.
|
|
|
|
=item --create-dest-table
|
|
|
|
Create the table specified by L<"--dest">.
|
|
|
|
Normally the L<"--dest"> table is expected to exist already. This option
|
|
causes pt-deadlock-logger to create the table automatically using the suggested
|
|
table structure.
|
|
|
|
=item --daemonize
|
|
|
|
Fork to the background and detach from the shell. POSIX operating systems only.
|
|
|
|
=item --database
|
|
|
|
short form: -D; type: string
|
|
|
|
Connect to this database.
|
|
|
|
=item --defaults-file
|
|
|
|
short form: -F; type: string
|
|
|
|
Only read mysql options from the given file. You must give an absolute
|
|
pathname.
|
|
|
|
=item --dest
|
|
|
|
type: DSN
|
|
|
|
DSN for where to store deadlocks; specify at least a database (D) and table (t).
|
|
|
|
Missing values are filled in with the same values from the source host, so you
|
|
can usually omit most parts of this argument if you're storing deadlocks on the
|
|
same server on which they happen.
|
|
|
|
The following table structure is suggested if you want to store all the
|
|
information pt-deadlock-logger can extract about deadlocks:
|
|
|
|
=for comment ignore-pt-internal-value
|
|
MAGIC_dest_table
|
|
|
|
CREATE TABLE deadlocks (
|
|
server char(20) NOT NULL,
|
|
ts timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
thread int unsigned NOT NULL,
|
|
txn_id bigint unsigned NOT NULL,
|
|
txn_time smallint unsigned NOT NULL,
|
|
user char(16) NOT NULL,
|
|
hostname char(20) NOT NULL,
|
|
ip char(15) NOT NULL, -- alternatively, ip int unsigned NOT NULL
|
|
db char(64) NOT NULL,
|
|
tbl char(64) NOT NULL,
|
|
idx char(64) NOT NULL,
|
|
lock_type char(16) NOT NULL,
|
|
lock_mode char(1) NOT NULL,
|
|
wait_hold char(1) NOT NULL,
|
|
victim tinyint unsigned NOT NULL,
|
|
query text NOT NULL,
|
|
PRIMARY KEY (server,ts,thread)
|
|
) ENGINE=InnoDB
|
|
|
|
If you use L<"--columns">, you can omit whichever columns you don't want to
|
|
store.
|
|
|
|
=item --help
|
|
|
|
Show help and exit.
|
|
|
|
=item --host
|
|
|
|
short form: -h; type: string
|
|
|
|
Connect to host.
|
|
|
|
=item --interval
|
|
|
|
type: time; default: 30
|
|
|
|
How often to check for deadlocks. If no L<"--run-time"> is specified,
|
|
pt-deadlock-logger runs forever, checking for deadlocks at every interval.
|
|
See also L<"--run-time">.
|
|
|
|
=item --iterations
|
|
|
|
type: int
|
|
|
|
How many times to check for deadlocks. By default, this option
|
|
is undefined which means an infinite number of iterations. The tool always
|
|
exits for L<"--run-time">, regardless of the value specified for this option.
|
|
For example, the tool will exit after 1 minute with
|
|
C<--run-time 1m --iterations 4 --interval 30> because 4 iterations at 30
|
|
second intervals would take 2 minutes, longer than the 1 minute run-time.
|
|
|
|
=item --log
|
|
|
|
type: string
|
|
|
|
Print all output to this file when daemonized.
|
|
|
|
=item --numeric-ip
|
|
|
|
Express IP addresses as integers.
|
|
|
|
=item --password
|
|
|
|
short form: -p; type: string
|
|
|
|
Password to use when connecting.
|
|
If password contains commas they must be escaped with a backslash: "exam\,ple"
|
|
|
|
=item --pid
|
|
|
|
type: string
|
|
|
|
Create the given PID file. The tool won't start if the PID file already
|
|
exists and the PID it contains is different than the current PID. However,
|
|
if the PID file exists and the PID it contains is no longer running, the
|
|
tool will overwrite the PID file with the current PID. The PID file is
|
|
removed automatically when the tool exits.
|
|
|
|
=item --port
|
|
|
|
short form: -P; type: int
|
|
|
|
Port number to use for connection.
|
|
|
|
=item --quiet
|
|
|
|
Do not deadlocks; only print errors and warnings to C<STDERR>.
|
|
|
|
=item --run-time
|
|
|
|
type: time
|
|
|
|
How long to run before exiting. By default pt-deadlock-logger runs forever,
|
|
checking for deadlocks every L<"--interval"> seconds.
|
|
|
|
=item --set-vars
|
|
|
|
type: Array
|
|
|
|
Set the MySQL variables in this comma-separated list of C<variable=value> pairs.
|
|
|
|
By default, the tool sets:
|
|
|
|
=for comment ignore-pt-internal-value
|
|
MAGIC_set_vars
|
|
|
|
wait_timeout=10000
|
|
|
|
Variables specified on the command line override these defaults. For
|
|
example, specifying C<--set-vars wait_timeout=500> overrides the defaultvalue of C<10000>.
|
|
|
|
The tool prints a warning and continues if a variable cannot be set.
|
|
|
|
=item --socket
|
|
|
|
short form: -S; type: string
|
|
|
|
Socket file to use for connection.
|
|
|
|
=item --tab
|
|
|
|
Use tabs to separate columns instead of spaces.
|
|
|
|
=item --user
|
|
|
|
short form: -u; type: string
|
|
|
|
User for login if not current user.
|
|
|
|
=item --version
|
|
|
|
Show version and exit.
|
|
|
|
=item --[no]version-check
|
|
|
|
default: yes
|
|
|
|
Check for the latest version of Percona Toolkit, MySQL, and other programs.
|
|
|
|
This is a standard "check for updates automatically" feature, with two
|
|
additional features. First, the tool checks its own version and also the
|
|
versions of the following software: operating system, Percona Monitoring and
|
|
Management (PMM), MySQL, Perl, MySQL driver for Perl (DBD::mysql), and
|
|
Percona Toolkit. Second, it checks for and warns about versions with known
|
|
problems. For example, MySQL 5.5.25 had a critical bug and was re-released
|
|
as 5.5.25a.
|
|
|
|
A secure connection to Percona's Version Check database server is done to
|
|
perform these checks. Each request is logged by the server, including software
|
|
version numbers and unique ID of the checked system. The ID is generated by the
|
|
Percona Toolkit installation script or when the Version Check database call is
|
|
done for the first time.
|
|
|
|
Any updates or known problems are printed to STDOUT before the tool's normal
|
|
output. This feature should never interfere with the normal operation of the
|
|
tool.
|
|
|
|
For more information, visit L<https://www.percona.com/doc/percona-toolkit/LATEST/version-check.html>.
|
|
|
|
=back
|
|
|
|
=head1 DSN OPTIONS
|
|
|
|
These DSN options are used to create a DSN. Each option is given like
|
|
C<option=value>. The options are case-sensitive, so P and p are not the
|
|
same option. There cannot be whitespace before or after the C<=> and
|
|
if the value contains whitespace it must be quoted. DSN options are
|
|
comma-separated. See the L<percona-toolkit> manpage for full details.
|
|
|
|
=over
|
|
|
|
=item * A
|
|
|
|
dsn: charset; copy: yes
|
|
|
|
Default character set.
|
|
|
|
=item * D
|
|
|
|
dsn: database; copy: yes
|
|
|
|
Default database.
|
|
|
|
=item * F
|
|
|
|
dsn: mysql_read_default_file; copy: yes
|
|
|
|
Only read default options from the given file
|
|
|
|
=item * h
|
|
|
|
dsn: host; copy: yes
|
|
|
|
Connect to host.
|
|
|
|
=item * p
|
|
|
|
dsn: password; copy: yes
|
|
|
|
Password to use when connecting.
|
|
If password contains commas they must be escaped with a backslash: "exam\,ple"
|
|
|
|
=item * P
|
|
|
|
dsn: port; copy: yes
|
|
|
|
Port number to use for connection.
|
|
|
|
=item * S
|
|
|
|
dsn: mysql_socket; copy: yes
|
|
|
|
Socket file to use for connection.
|
|
|
|
=item * t
|
|
|
|
Table in which to store deadlock information.
|
|
|
|
=item * u
|
|
|
|
dsn: user; copy: yes
|
|
|
|
User for login if not current user.
|
|
|
|
=item * s
|
|
|
|
dsn: mysql_ssl; copy: yes
|
|
|
|
Create SSL connection
|
|
|
|
=back
|
|
|
|
=head1 ENVIRONMENT
|
|
|
|
The environment variable C<PTDEBUG> enables verbose debugging output to STDERR.
|
|
To enable debugging and capture all output to a file, run the tool like:
|
|
|
|
PTDEBUG=1 pt-deadlock-logger ... > FILE 2>&1
|
|
|
|
Be careful: debugging output is voluminous and can generate several megabytes
|
|
of output.
|
|
|
|
=head1 ATTENTION
|
|
|
|
Using <PTDEBUG> might expose passwords. When debug is enabled, all command line
|
|
parameters are shown in the output.
|
|
|
|
=head1 SYSTEM REQUIREMENTS
|
|
|
|
You need Perl, DBI, DBD::mysql, and some core packages that ought to be
|
|
installed in any reasonably new version of Perl.
|
|
|
|
=head1 BUGS
|
|
|
|
For a list of known bugs, see L<https://jira.percona.com/projects/PT/issues>.
|
|
|
|
Please report bugs at L<https://jira.percona.com/projects/PT>.
|
|
Include the following information in your bug report:
|
|
|
|
=over
|
|
|
|
=item * Complete command-line used to run the tool
|
|
|
|
=item * Tool L<"--version">
|
|
|
|
=item * MySQL version of all servers involved
|
|
|
|
=item * Output from the tool including STDERR
|
|
|
|
=item * Input files (log/dump/config files, etc.)
|
|
|
|
=back
|
|
|
|
If possible, include debugging output by running the tool with C<PTDEBUG>;
|
|
see L<"ENVIRONMENT">.
|
|
|
|
=head1 DOWNLOADING
|
|
|
|
Visit L<http://www.percona.com/software/percona-toolkit/> to download the
|
|
latest release of Percona Toolkit. Or, get the latest release from the
|
|
command line:
|
|
|
|
wget percona.com/get/percona-toolkit.tar.gz
|
|
|
|
wget percona.com/get/percona-toolkit.rpm
|
|
|
|
wget percona.com/get/percona-toolkit.deb
|
|
|
|
You can also get individual tools from the latest release:
|
|
|
|
wget percona.com/get/TOOL
|
|
|
|
Replace C<TOOL> with the name of any tool.
|
|
|
|
=head1 AUTHORS
|
|
|
|
Baron Schwartz and Daniel Nichter
|
|
|
|
=head1 ABOUT PERCONA TOOLKIT
|
|
|
|
This tool is part of Percona Toolkit, a collection of advanced command-line
|
|
tools for MySQL developed by Percona. Percona Toolkit was forked from two
|
|
projects in June, 2011: Maatkit and Aspersa. Those projects were created by
|
|
Baron Schwartz and primarily developed by him and Daniel Nichter. Visit
|
|
L<http://www.percona.com/software/> to learn about other free, open-source
|
|
software from Percona.
|
|
|
|
=head1 COPYRIGHT, LICENSE, AND WARRANTY
|
|
|
|
This program is copyright 2011-2024 Percona LLC and/or its affiliates,
|
|
2007-2011 Baron Schwartz.
|
|
|
|
THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
|
|
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
|
|
MERCHANTABILITY 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 along with
|
|
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
|
|
Place, Suite 330, Boston, MA 02111-1307 USA.
|
|
|
|
=head1 VERSION
|
|
|
|
pt-deadlock-logger 3.7.0
|
|
|
|
=cut
|