File Coverage

blib/lib/App/Memcached/CLI/Main.pm
Criterion Covered Total %
statement 56 415 13.4
branch 4 122 3.2
condition 2 52 3.8
subroutine 17 54 31.4
pod 0 31 0.0
total 79 674 11.7


line stmt bran cond sub pod time code
1             package App::Memcached::CLI::Main;
2              
3 2     2   15241 use strict;
  2         4  
  2         60  
4 2     2   18 use warnings;
  2         3  
  2         55  
5 2     2   39 use 5.008_001;
  2         6  
6              
7 2     2   7 use Carp;
  2         3  
  2         170  
8 2     2   8 use File::Basename 'basename';
  2         4  
  2         129  
9 2     2   1448 use Getopt::Long qw(:config posix_default no_ignore_case no_ignore_case_always);
  2         18140  
  2         11  
10 2     2   1451 use IO::Socket::INET;
  2         33053  
  2         11  
11 2     2   860 use List::Util qw(first);
  2         3  
  2         183  
12 2     2   978 use Term::ReadLine;
  2         4002  
  2         65  
13 2     2   881 use Time::HiRes;
  2         2044  
  2         7  
14              
15 2     2   530 use App::Memcached::CLI;
  2         4  
  2         54  
16 2     2   764 use App::Memcached::CLI::DataSource;
  2         5  
  2         56  
17 2     2   649 use App::Memcached::CLI::Help;
  2         2  
  2         49  
18 2     2   596 use App::Memcached::CLI::Item;
  2         2  
  2         48  
19 2     2   8 use App::Memcached::CLI::Util ':all';
  2         2  
  2         170  
20              
21 2     2   7 use version; our $VERSION = 'v0.9.4';
  2         3  
  2         6  
22              
23             my $PROGRAM = basename $0;
24              
25             my %COMMAND2ALIASES = (
26             help => ['\h'],
27             version => ['\v'],
28             quit => [qw(\q exit)],
29             display => [qw(\d)],
30             stats => [qw(\s)],
31             settings => [qw(\c config)],
32             cachedump => [qw(\cd)],
33             detaildump => [qw(\dd)],
34             detail => [],
35             dump_all => [],
36             restore_dump => [],
37             randomset => [qw(sample)],
38             get => [],
39             gets => [],
40             set => [],
41             add => [],
42             replace => [],
43             append => [],
44             prepend => [],
45             cas => [],
46             incr => [],
47             decr => [],
48             touch => [],
49             delete => [],
50             flush_all => [qw(flush)],
51             call => [],
52             );
53             my %COMMAND_OF;
54             while (my ($cmd, $aliases) = each %COMMAND2ALIASES) {
55             $COMMAND_OF{$cmd} = $cmd;
56             $COMMAND_OF{$_} = $cmd for @$aliases;
57             }
58              
59             my $DEFAULT_CACHEDUMP_SIZE = 20;
60              
61             sub new {
62 0     0 0 0 my $class = shift;
63 0         0 my %params = @_;
64              
65 0         0 eval {
66             $params{ds}
67             = App::Memcached::CLI::DataSource->connect(
68             $params{addr}, timeout => $params{timeout}
69 0         0 );
70             };
71 0 0       0 if ($@) {
72 0         0 warn "Can't connect to Memcached server! Addr=$params{addr}";
73 0         0 debug "ERROR: " . $@;
74 0         0 return;
75             }
76              
77 0         0 bless \%params, $class;
78             }
79              
80             sub parse_args {
81 6     6 0 7578 my $class = shift;
82              
83 6         12 my %params; # will be passed to new()
84 6 100 66     52 if (defined $ARGV[0] and looks_like_addr($ARGV[0])) {
85 4         25 $params{addr} = shift @ARGV;
86             }
87             GetOptions(
88 6 50       65 \my %opts, 'addr|a=s', 'timeout|t=i',
89             'debug|d', 'help|h', 'man',
90             ) or return +{};
91              
92 6 50       2408 if (defined $opts{debug}) {
93 0         0 $App::Memcached::CLI::DEBUG = 1;
94             }
95              
96 6         33 %params = (%opts, %params);
97 6         39 $params{addr} = create_addr($params{addr});
98              
99 6         32 return \%params;
100             }
101              
102             sub run {
103 0     0 0   my $self = shift;
104 0 0         if (@ARGV) {
105 0           $self->run_batch;
106             } else {
107 0           $self->run_interactive;
108             }
109             }
110              
111             sub run_batch {
112 0     0 0   my $self = shift;
113 0 0         debug "Run batch mode with @ARGV" if (@ARGV);
114 0           my ($_command, @args) = @ARGV;
115 0           my $command = $COMMAND_OF{$_command};
116 0 0         unless ($command) {
    0          
117 0           print "Unknown command - $_command\n";
118 0           return;
119             } elsif ($command eq 'quit') {
120 0           print "Nothing to do with $_command\n";
121 0           return;
122             }
123              
124 0           my $ret = $self->$command(@args);
125 0 0         unless ($ret) {
126 0           print qq[Command seems failed. Run \`$PROGRAM help\` or \`$PROGRAM help $command\` for usage.\n];
127             }
128             }
129              
130             sub run_interactive {
131 0     0 0   my $self = shift;
132 0           debug "Start interactive mode. $self->{addr}";
133 0   0       my $isa_tty = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT));
134 0 0         unless ($isa_tty) {
135 0           croak "TTY Not Found! Quit.";
136             }
137 0           my $exit_loop = 0;
138             local $SIG{INT} = local $SIG{QUIT} = sub {
139 0     0     $exit_loop = 1;
140 0           warn "Caught INT or QUIT. Exiting...";
141 0           };
142              
143 0           $self->{term} = Term::ReadLine->new($PROGRAM);
144 0           print "Type '\\h' or 'help' to show help.\n\n";
145 0           while (! $exit_loop) {
146 0           my ($command, @args) = $self->prompt;
147 0 0         next unless $command;
148 0 0         if ($command eq 'quit') {
149 0           $exit_loop = 1;
150 0           next;
151             }
152 0 0         unless ($self->{ds}->ping) {
153 0           print "Server has gone away.\n";
154 0           $exit_loop = 1;
155 0           next;
156             }
157              
158 0           my $ret = $self->$command(@args);
159 0 0         unless ($ret) {
160 0           print "Command seems failed. Type \\h $command for help.\n\n";
161             }
162             }
163 0           debug "Finish interactive mode. $self->{addr}";
164             }
165              
166             sub prompt {
167 0     0 0   my $self = shift;
168              
169 0           local $| = 1;
170 0           local $\;
171              
172 0           my $input = $self->{term}->readline("memcached\@$self->{addr}> ");
173 0           chomp($input);
174 0 0         return unless $input;
175 0 0         $self->{term}->addhistory($input) if ($input =~ m/\S/);
176              
177 0           my ($_command, @args) = split(m/\s+/, $input);
178 0           my $command = $COMMAND_OF{$_command};
179 0 0         print "Unknown command - $input\n" unless $command;
180              
181 0           return $command, @args;
182             }
183              
184             sub help {
185 0     0 0   my $self = shift;
186 0   0       my $command = shift || q{};
187              
188 0           my @command_info = @App::Memcached::CLI::Help::COMMANDS_INFO;
189              
190 0           my $body = q{};
191 0           my $space = ' ' x 4;
192              
193             # Help for specified command
194 0 0         if (my $function = $COMMAND_OF{$command}) {
    0          
195 0           my $aliases = join(q{, }, _sorted_aliases_of($function));
196 0           my $info = (grep { $_->{command} eq $function } @command_info)[0];
  0            
197 0           $body .= sprintf qq{\n[Command "%s"]\n\n}, $command;
198 0           $body .= "Summary:\n";
199 0           $body .= sprintf "%s%s\n\n", $space, $info->{summary};
200 0           $body .= "Aliases:\n";
201 0           $body .= sprintf "%s%s\n\n", $space, $aliases;
202 0 0         if ($info->{description}) {
203 0           $body .= $info->{description};
204 0           $body .= "\n";
205             }
206 0           print $body;
207 0           return 1;
208             }
209             # Command not found, but continue
210             elsif ($command) {
211 0           $body .= "Unknown command: $command\n";
212             }
213              
214             # General help
215 0           $body .= "\n[Available Commands]\n";
216 0           for my $info (@command_info) {
217 0           my $cmd = $info->{command};
218 0           my $commands = join(q{, }, _sorted_aliases_of($cmd));
219             $body .= sprintf "%s%-20s%s%s\n",
220 0           $space, $commands, $space x 2, $info->{summary};
221             }
222 0           $body .= "\nType \\h for each.\n\n";
223 0           print $body;
224 0           return 1;
225             }
226              
227             sub _sorted_aliases_of {
228 0     0     my $command = shift;
229 0           my @aliases = @{$COMMAND2ALIASES{$command}};
  0            
230 0 0         return (shift @aliases, $command, @aliases) if @aliases;
231 0           return ($command);
232             }
233              
234             sub call {
235 0     0 0   my $self = shift;
236 0           my @commands = @_;
237 0 0         unless (@commands) {
238 0           print "No COMMAND given.\n";
239 0           return;
240             }
241 0           my $response = $self->{ds}->query_any(join q{ }, @commands);
242 0           print $response;
243 0           return 1;
244             }
245              
246             sub get {
247 0     0 0   my $self = shift;
248 0           return $self->_retrieve('get', @_);
249             }
250              
251             sub gets {
252 0     0 0   my $self = shift;
253 0           return $self->_retrieve('gets', @_);
254             }
255              
256             sub _retrieve {
257 0     0     my $self = shift;
258 0           my ($command, @keys) = @_;
259 0 0         unless (@keys) {
260 0           print "No KEY specified.\n";
261 0           return;
262             }
263             my $items = App::Memcached::CLI::Item->find(
264 0           \@keys, $self->{ds}, command => $command,
265             );
266 0 0         unless (@$items) {
267 0           print "Not found - @keys\n";
268 0           return 1;
269             }
270 0 0         if (@$items == 1) {
271 0           $items->[0]->output;
272 0           return 1;
273             }
274 0           for (my $i=0; $i < scalar(@$items); $i++) {
275 0           my $item = $items->[$i];
276 0           $item->output_line;
277             }
278 0           return 1;
279             }
280              
281 0     0 0   sub set { return &_store(shift, 'set', @_); }
282 0     0 0   sub add { return &_store(shift, 'add', @_); }
283 0     0 0   sub replace { return &_store(shift, 'replace', @_); }
284 0     0 0   sub append { return &_store(shift, 'append', @_); }
285 0     0 0   sub prepend { return &_store(shift, 'prepend', @_); }
286              
287             sub _store {
288 0     0     my $self = shift;
289 0           my $command = shift;
290 0           my ($key, $value, $expire, $flags) = @_;
291 0 0 0       unless ($key and $value) {
292 0           print "KEY or VALUE not specified.\n";
293 0           return;
294             }
295 0           my $item = App::Memcached::CLI::Item->new(
296             key => $key,
297             value => $value,
298             expire => $expire,
299             flags => $flags,
300             );
301 0 0         unless ($item->save($self->{ds}, command => $command)) {
302 0           print "Failed to $command item. KEY $key, VALUE $value\n";
303 0           return 1;
304             }
305 0           print "OK\n";
306 0           return 1;
307             }
308              
309             sub cas {
310 0     0 0   my $self = shift;
311 0           my ($key, $value, $cas, $expire, $flags) = @_;
312 0 0 0       unless ($key and $value and $cas) {
      0        
313 0           print "KEY or VALUE or CAS not specified.\n";
314 0           return;
315             }
316 0           my $item = App::Memcached::CLI::Item->new(
317             key => $key,
318             value => $value,
319             expire => $expire,
320             flags => $flags,
321             cas => $cas,
322             );
323 0 0         unless ($item->save($self->{ds}, command => 'cas')) {
324 0           print "Failed to cas item. KEY $key, VALUE $value\n";
325 0           return 1;
326             }
327 0           print "OK\n";
328 0           return 1;
329             }
330              
331 0     0 0   sub incr { return &_incr_decr(shift, 'incr', @_); }
332 0     0 0   sub decr { return &_incr_decr(shift, 'decr', @_); }
333              
334             sub _incr_decr {
335 0     0     my $self = shift;
336 0           my $cmd = shift;
337 0           my $key = shift;
338 0           my $number = shift;
339 0 0 0       unless ($key and defined $number) {
340 0           print "No KEY or VALUE specified.\n";
341 0           return;
342             }
343 0 0         unless ($number =~ m/^\d+$/) {
344 0           print "Give numeric number for $cmd VALUE.\n";
345 0           return;
346             }
347 0           my $new_value = $self->{ds}->$cmd($key, $number);
348 0 0         unless (defined $new_value) {
349 0           print "FAILED - $cmd\n";
350 0           return;
351             }
352 0           print "OK. New VALUE is $new_value\n";
353 0           return 1;
354             }
355              
356             sub touch {
357 0     0 0   my $self = shift;
358 0           my $key = shift;
359 0           my $expire = shift;
360 0 0 0       unless ($key and defined $expire) {
361 0           print "No KEY or EXPIRE specified.\n";
362 0           return;
363             }
364 0 0         unless ($expire =~ m/^\d+$/) {
365 0           print "Give numeric number for EXPIRE.\n";
366 0           return;
367             }
368 0 0         unless ($self->{ds}->touch($key, $expire)) {
369 0           print "Failed to touch. KEY $key maybe missing\n";
370 0           return;
371             }
372 0           print "OK\n";
373 0           return 1;
374             }
375              
376             sub delete {
377 0     0 0   my $self = shift;
378 0           my $key = shift;
379 0 0         unless ($key) {
380 0           print "No KEY specified.\n";
381 0           return;
382             }
383 0           my $item = App::Memcached::CLI::Item->new(key => $key);
384 0 0         unless ($item->remove($self->{ds})) {
385 0           warn "Failed to delete item. KEY $key";
386 0           return;
387             }
388 0           print "OK\n";
389 0           return 1;
390             }
391              
392             sub version {
393 0     0 0   my $self = shift;
394 0           my $version = $self->{ds}->query_one('version');
395 0           print "$version\n";
396 0           return 1;
397             }
398              
399             sub cachedump {
400 0     0 0   my $self = shift;
401 0           my $class = shift;
402 0   0       my $num = shift || $DEFAULT_CACHEDUMP_SIZE;
403              
404 0 0         unless ($class) {
405 0           print "No slab class specified.\n";
406 0           return;
407             }
408 0           my $response = $self->{ds}->query("stats cachedump $class $num");
409 0           my %expires;
410 0           for my $line (@$response) {
411 0 0         if ($line !~ m/^ITEM (\S+) \[(\d+) b; (\d+) s\]/) {
412 0           warn "Unknown response: $line";
413 0           next;
414             }
415 0           my %data = (key => $1, length => $2, expire => $3);
416 0           $expires{$data{key}} = \%data;
417             }
418 0 0         return 1 unless %expires;
419              
420 0           my @keys = keys %expires;
421             my $items = App::Memcached::CLI::Item->find(
422 0           \@keys, $self->{ds}, command => 'gets',
423             );
424 0           for (my $i=0; $i < scalar(@$items); $i++) {
425 0           my $item = $items->[$i];
426 0           $item->{expire} = $expires{$item->{key}}{expire};
427 0           $item->output_line;
428             }
429 0           return 1;
430             }
431              
432             sub display {
433 0     0 0   my $self = shift;
434              
435 0           my %stats;
436 0           my $max = 1;
437              
438 0           my $resp_items = $self->{ds}->query('stats items');
439 0           for my $line (@$resp_items) {
440 0 0         if ($line =~ m/^STAT items:(\d+):(\w+) (\d+)/) {
441 0           $stats{$1}{$2} = $3;
442             }
443             }
444              
445 0           my $resp_slabs = $self->{ds}->query('stats slabs');
446 0           for my $line (@$resp_slabs) {
447 0 0         if ($line =~ m/^STAT (\d+):(\w+) (\d+)/) {
448 0           $stats{$1}{$2} = $3;
449 0           $max = $1;
450             }
451             }
452              
453 0           print " # Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM\n";
454 0           for my $class (1..$max) {
455 0           my $slab = $stats{$class};
456 0 0         next unless $slab->{total_pages};
457              
458             my $size
459             = $slab->{chunk_size} < 1024 ? "$slab->{chunk_size}B"
460 0 0         : sprintf("%.1fK", $slab->{chunk_size} / 1024.0) ;
461              
462 0 0         my $full = ($slab->{free_chunks_end} == 0) ? 'yes' : 'no';
463             printf(
464             "%3d %8s %9ds %7d %7d %7s %8d %8d %4d\n",
465             $class, $size, $slab->{age} || 0, $slab->{total_pages},
466             $slab->{number} || 0, $full, $slab->{evicted} || 0,
467 0   0       $slab->{evicted_time} || 0, $slab->{outofmemory} || 0,
      0        
      0        
      0        
      0        
468             );
469             }
470              
471 0           return 1;
472             }
473              
474             sub stats {
475 0     0 0   my $self = shift;
476 0           my $filter = shift;
477 0           my $response = $self->{ds}->query('stats');
478 0           _print_stats_of_response('stats', $filter, @$response);
479 0           return 1;
480             }
481              
482             sub settings {
483 0     0 0   my $self = shift;
484 0           my $filter = shift;
485 0           my $response = $self->{ds}->query('stats settings');
486 0           _print_stats_of_response('stats settings', $filter, @$response);
487 0           return 1;
488             }
489              
490             sub _print_stats_of_response {
491 0     0     my $title = shift;
492 0           my $filter = shift;
493 0           my @lines = @_;
494              
495 0           my %stats;
496 0           my ($max_key_l, $max_val_l) = (0, 0);
497              
498 0           for my $line (@lines) {
499 0 0         next if ($line !~ m/^STAT\s+(\S*)\s+(.*)/);
500 0           my ($key, $value) = ($1, $2);
501 0 0         if (length $key > $max_key_l) { $max_key_l = length $key; }
  0            
502 0 0         if (length $value > $max_val_l) { $max_val_l = length $value; }
  0            
503 0 0 0       next if ($filter && $key !~ m/$filter/);
504 0           $stats{$key} = $value;
505             }
506              
507 0           print "# $title\n";
508 0           printf "#%${max_key_l}s %${max_val_l}s\n", 'Field', 'Value';
509 0           for my $field (sort {$a cmp $b} (keys %stats)) {
  0            
510 0           printf (" %${max_key_l}s %${max_val_l}s\n", $field, $stats{$field});
511             }
512             }
513              
514             sub detaildump {
515 0     0 0   my $self = shift;
516 0           my $response = $self->{ds}->query("stats detail dump");
517 0           print "$_\n" for @$response;
518 0           return 1;
519             }
520              
521             sub detail {
522 0     0 0   my $self = shift;
523 0   0       my $mode = shift || q{};
524 0 0   0     unless (first { $_ eq $mode } qw/on off/) {
  0            
525 0           print "Mode must be 'on' or 'off'!\n";
526 0           return;
527             }
528 0           my $response = $self->{ds}->query_one("stats detail $mode");
529 0           my %result = (
530             on => 'Enabled',
531             off => 'Disabled',
532             );
533 0 0         if ($response !~ m/^OK/) {
534 0           print "Seems failed to set detail $mode\n";
535 0           return;
536             }
537 0           print "OK - $result{$mode} stats collection for detail dump.\n";
538 0           return 1;
539             }
540              
541             sub dump_all {
542 0     0 0   my $self = shift;
543 0           my %items;
544             my $total;
545              
546 0           my $response = $self->{ds}->query('stats items');
547 0           for my $line (@$response) {
548 0 0         if ($line =~ m/^STAT items:(\d*):number (\d*)/) {
549 0           $items{$1} = $2;
550 0           $total += $2;
551             }
552             }
553              
554 0           print STDERR "Dumping memcache contents\n";
555 0           printf STDERR " Number of buckets: %d\n", scalar(keys(%items));
556 0           print STDERR " Number of items : $total\n";
557              
558 0           for my $bucket (sort(keys %items)) {
559 0           print STDERR "Dumping bucket $bucket - " . $items{$bucket} . " total items\n";
560 0           $response = $self->{ds}->query("stats cachedump $bucket $items{$bucket}");
561              
562 0           my %expires;
563 0           for my $line (@$response) {
564             # Ex) ITEM foo [6 b; 1176415152 s]
565 0 0         if ($line =~ m/^ITEM (\S+) \[.* (\d+) s\]/) {
566 0           $expires{$1} = $2;
567             }
568             }
569              
570 0           my $now = time();
571 0           my @keys_bucket = keys %expires;
572 0           while (my @keys = splice(@keys_bucket, 0, 20)) {
573 0           my $list = $self->{ds}->get(\@keys);
574 0           for my $d (@$list) {
575 0 0         my $expire = ($expires{$d->{key}} < $now) ? 0 : $expires{$d->{key}};
576 0           print "add $d->{key} $d->{flags} $expire $d->{length}\r\n";
577 0           print "$d->{value}\r\n";
578             }
579 0           Time::HiRes::sleep(0.01);
580             }
581             }
582              
583 0           return 1;
584             }
585              
586             sub restore_dump {
587 0     0 0   my $self = shift;
588 0           my $file = shift;
589 0 0 0       unless ($file and -r $file) {
590 0           print "Dump FILE not found.\n";
591 0           return;
592             }
593              
594 0 0         open my $fh, '<', $file or die "Can't read $file!";
595 0           my $i = 0;
596 0           while (my $input = $fh->getline) {
597 0           $i++;
598 0           $self->{ds}->{socket}->write($input);
599 0 0         if ($i%200 == 0) {
600 0           printf "%s...loaded $i lines\n", q[ ] x 4;
601 0           $self->{ds}->{socket}->flush;
602 0           Time::HiRes::sleep(0.03);
603             }
604             }
605 0           print "Complete.\n";
606 0           return 1;
607             }
608              
609             sub flush_all {
610 0     0 0   my $self = shift;
611 0           my $delay = shift;
612 0           my $query = 'flush_all';
613 0 0         if ($delay) { $query .= " $delay"; }
  0            
614 0           my $response = $self->{ds}->query_one($query);
615 0           print "$response\n";
616 0           return 1;
617             }
618              
619             sub randomset {
620 0     0 0   my $self = shift;
621 0           my ($num, $max, $min, $namespace) = @_;
622 0   0       $num ||= 100;
623 0   0       $max ||= 1_000_000;
624 0   0       $min ||= 1;
625 0   0       $namespace ||= 'memcached-cli:sample';
626              
627 0           local $| = 1; # disable output buffering
628 0           print "Random Generate. [";
629 0           my $pos = 0;
630 0           for my $i (1..$num) {
631 0           my $key = $namespace . ":data$i";
632 0           my $length = int( rand() * ($max - $min) ) + $min;
633 0           my $value = '.' x $length;
634 0 0         unless ($self->{ds}->set($key, $value)) {
635 0           warn "Failed to set KEY $key, length: $length B";
636             }
637 0 0         if ( (my $_pos = int($i*20 / $num)) > $pos ) {
638 0           $pos = $_pos;
639 0           print '.';
640             }
641 0 0         Time::HiRes::sleep(0.1) if ($i % 100 == 0);
642             }
643 0           print "]\n";
644 0           print "Complete.\n";
645 0           return 1;
646             }
647              
648             1;
649             __END__