| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Net::SSH::Mechanize::Session; | 
| 2 | 1 |  |  | 1 |  | 2927 | use Moose; | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 3 |  |  |  |  |  |  | use MooseX::Params::Validate; | 
| 4 |  |  |  |  |  |  | use AnyEvent; | 
| 5 |  |  |  |  |  |  | use Carp qw(croak); | 
| 6 |  |  |  |  |  |  | our @CARP_NOT = qw(Net::SSH::Mechanize AnyEvent); | 
| 7 |  |  |  |  |  |  |  | 
| 8 |  |  |  |  |  |  | our $VERSION = '0.1.3'; # VERSION | 
| 9 |  |  |  |  |  |  |  | 
| 10 |  |  |  |  |  |  | extends 'AnyEvent::Subprocess::Running'; | 
| 11 |  |  |  |  |  |  |  | 
| 12 |  |  |  |  |  |  | my $passwd_prompt_re = qr/assword:\s*/; | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | my $initial_prompt_re = qr/^.*?\Q$ \E$/m; | 
| 15 |  |  |  |  |  |  | my $sudo_initial_prompt_re = qr/^.*?\Q$ \E$/m; | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | # Create a random text delimiter | 
| 18 |  |  |  |  |  |  | # We want chars A-Z, a-z, 0-9, _- => 26+26+10 = 64 different characters. | 
| 19 |  |  |  |  |  |  | # First we generate a random string of ASCII chars 1..65, | 
| 20 |  |  |  |  |  |  | my $delim = pack "C*", map { int(rand 64)+1 } 1..20; | 
| 21 |  |  |  |  |  |  |  | 
| 22 |  |  |  |  |  |  | # Then we map it to the characters we want. | 
| 23 |  |  |  |  |  |  | $delim =~ tr/\x01-\x40/A-Za-z0-9_-/; | 
| 24 |  |  |  |  |  |  |  | 
| 25 |  |  |  |  |  |  | my $prompt = "$delim"; | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  | my $sudo_passwd_prompt = "$delim-passwd"; | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | my $prompt_re = qr/\Q$prompt\E$/sm; | 
| 30 |  |  |  |  |  |  |  | 
| 31 |  |  |  |  |  |  | my $sudo_passwd_prompt_re = qr/^$sudo_passwd_prompt$/; | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  |  | 
| 34 |  |  |  |  |  |  | has 'connection_params' => ( | 
| 35 |  |  |  |  |  |  | isa => 'Net::SSH::Mechanize::ConnectParams', | 
| 36 |  |  |  |  |  |  | is => 'rw', | 
| 37 |  |  |  |  |  |  | # Note: this made rw and unrequired so that it can be supplied | 
| 38 |  |  |  |  |  |  | # after AnyEvent::Subprocess::Job constructs the instance | 
| 39 |  |  |  |  |  |  | ); | 
| 40 |  |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | has 'is_logged_in' => ( | 
| 42 |  |  |  |  |  |  | isa => 'Bool', | 
| 43 |  |  |  |  |  |  | is => 'ro', | 
| 44 |  |  |  |  |  |  | writer => '_set_logged_in', | 
| 45 |  |  |  |  |  |  | ); | 
| 46 |  |  |  |  |  |  |  | 
| 47 |  |  |  |  |  |  | has '_error_event' => ( | 
| 48 |  |  |  |  |  |  | is => 'rw', | 
| 49 |  |  |  |  |  |  | isa => 'AnyEvent::CondVar', | 
| 50 |  |  |  |  |  |  | default => sub { return AnyEvent->condvar }, | 
| 51 |  |  |  |  |  |  | ); | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  |  | 
| 54 |  |  |  |  |  |  | # The log-in timeout limit in seconds | 
| 55 |  |  |  |  |  |  | has 'login_timeout' => ( | 
| 56 |  |  |  |  |  |  | is => 'rw', | 
| 57 |  |  |  |  |  |  | isa => 'Int', | 
| 58 |  |  |  |  |  |  | default => 30, | 
| 59 |  |  |  |  |  |  | ); | 
| 60 |  |  |  |  |  |  |  | 
| 61 |  |  |  |  |  |  | # helper function | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | sub _croak_with { | 
| 64 |  |  |  |  |  |  | my ($msg, $cv) = @_; | 
| 65 |  |  |  |  |  |  | sub { | 
| 66 |  |  |  |  |  |  | my $h = shift; | 
| 67 |  |  |  |  |  |  | return unless my $text = $h->rbuf; | 
| 68 |  |  |  |  |  |  | $h->{rbuf} = ''; | 
| 69 |  |  |  |  |  |  | $cv->croak("$msg: $text"); | 
| 70 |  |  |  |  |  |  | } | 
| 71 |  |  |  |  |  |  | } | 
| 72 |  |  |  |  |  |  |  | 
| 73 |  |  |  |  |  |  | sub _warn_with { | 
| 74 |  |  |  |  |  |  | my ($msg) = @_; | 
| 75 |  |  |  |  |  |  | sub { | 
| 76 |  |  |  |  |  |  | my $h = shift; | 
| 77 |  |  |  |  |  |  | return unless my $text = $h->rbuf; | 
| 78 |  |  |  |  |  |  | $h->{rbuf} = ''; | 
| 79 |  |  |  |  |  |  | warn "$msg: $text"; | 
| 80 |  |  |  |  |  |  | } | 
| 81 |  |  |  |  |  |  | } | 
| 82 |  |  |  |  |  |  |  | 
| 83 |  |  |  |  |  |  | sub _push_write { | 
| 84 |  |  |  |  |  |  | my $handle = shift; | 
| 85 |  |  |  |  |  |  |  | 
| 86 |  |  |  |  |  |  | #    print qq(writing: "@_"\n); # DB | 
| 87 |  |  |  |  |  |  | $handle->push_write(@_); | 
| 88 |  |  |  |  |  |  | } | 
| 89 |  |  |  |  |  |  |  | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  | sub _match { | 
| 92 |  |  |  |  |  |  | my $handle = shift; | 
| 93 |  |  |  |  |  |  | my $re = shift; | 
| 94 |  |  |  |  |  |  | return unless $handle->{rbuf}; | 
| 95 |  |  |  |  |  |  | my @captures = $handle->{rbuf} =~ /$re/; | 
| 96 |  |  |  |  |  |  | if (!@captures) { | 
| 97 |  |  |  |  |  |  | #        print qq(not matching $re: "$handle->{rbuf}"\n); # DB | 
| 98 |  |  |  |  |  |  | return; | 
| 99 |  |  |  |  |  |  | } | 
| 100 |  |  |  |  |  |  |  | 
| 101 |  |  |  |  |  |  | #    printf qq(matching $re with: "%s"\n), substr $handle->{rbuf}, 0, $+[0]; # DB | 
| 102 |  |  |  |  |  |  |  | 
| 103 |  |  |  |  |  |  | substr $handle->{rbuf}, 0, $+[0], ""; | 
| 104 |  |  |  |  |  |  | return @captures; | 
| 105 |  |  |  |  |  |  | } | 
| 106 |  |  |  |  |  |  |  | 
| 107 |  |  |  |  |  |  | sub _define_automation { | 
| 108 |  |  |  |  |  |  | my $self = shift; | 
| 109 |  |  |  |  |  |  | my $states = {@_}; | 
| 110 |  |  |  |  |  |  | my $function = (caller 1)[3]; | 
| 111 |  |  |  |  |  |  |  | 
| 112 |  |  |  |  |  |  | my ($stdin, $stderr) = map { $self->delegate($_)->handle } qw(pty stderr); | 
| 113 |  |  |  |  |  |  |  | 
| 114 |  |  |  |  |  |  | my $state = 'start'; | 
| 115 |  |  |  |  |  |  | my $cb; | 
| 116 |  |  |  |  |  |  | $cb = sub { | 
| 117 |  |  |  |  |  |  | #        printf "before: state is %s %s\n", $function, $state; # DB | 
| 118 |  |  |  |  |  |  | $state = $states->{$state}->(@_); | 
| 119 |  |  |  |  |  |  | exists $states->{$state} | 
| 120 |  |  |  |  |  |  | or die "something is wrong, next state returned is an unknown name: '$state'"; | 
| 121 |  |  |  |  |  |  |  | 
| 122 |  |  |  |  |  |  | #        printf "after: state is %s %s\n", $function, $state; # DB | 
| 123 |  |  |  |  |  |  | if (!$states->{$state}) { # terminal state, stop reading | 
| 124 |  |  |  |  |  |  | #            $stderr->on_read(undef); # cancel errors on stderr | 
| 125 |  |  |  |  |  |  | $stdin->{rbuf} = ''; | 
| 126 |  |  |  |  |  |  | return 1; | 
| 127 |  |  |  |  |  |  | } | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | #        $stdin->push_read($cb); | 
| 130 |  |  |  |  |  |  | return; | 
| 131 |  |  |  |  |  |  | }; | 
| 132 |  |  |  |  |  |  | $stdin->push_read($cb); | 
| 133 |  |  |  |  |  |  |  | 
| 134 |  |  |  |  |  |  | #    printf "$Coro::current exiting _define_automation\n"; # DB | 
| 135 |  |  |  |  |  |  | return $state; | 
| 136 |  |  |  |  |  |  | }; | 
| 137 |  |  |  |  |  |  |  | 
| 138 |  |  |  |  |  |  | # FIXME check code for possible self-ref closures which may cause mem leaks | 
| 139 |  |  |  |  |  |  |  | 
| 140 |  |  |  |  |  |  |  | 
| 141 |  |  |  |  |  |  | sub login_async { | 
| 142 |  |  |  |  |  |  | my $self = shift; | 
| 143 |  |  |  |  |  |  | my $done = AnyEvent->condvar; | 
| 144 |  |  |  |  |  |  |  | 
| 145 |  |  |  |  |  |  | my $stdin = $self->delegate('pty')->handle; | 
| 146 |  |  |  |  |  |  | my $stderr = $self->delegate('stderr')->handle; | 
| 147 |  |  |  |  |  |  |  | 
| 148 |  |  |  |  |  |  | # Make this a no-op if we've already logged in | 
| 149 |  |  |  |  |  |  | if ($self->is_logged_in) { | 
| 150 |  |  |  |  |  |  | $done->send($stdin, $self); | 
| 151 |  |  |  |  |  |  | return $done; | 
| 152 |  |  |  |  |  |  | } | 
| 153 |  |  |  |  |  |  |  | 
| 154 |  |  |  |  |  |  | $self->_error_event->cb(sub { | 
| 155 |  |  |  |  |  |  | #        print "_error_event sent\n"; # DB | 
| 156 |  |  |  |  |  |  | $done->croak(shift->recv); | 
| 157 |  |  |  |  |  |  | }); | 
| 158 |  |  |  |  |  |  |  | 
| 159 |  |  |  |  |  |  | my $timeout; | 
| 160 |  |  |  |  |  |  | my $delay = $self->login_timeout; | 
| 161 |  |  |  |  |  |  | $timeout = AnyEvent->timer( | 
| 162 |  |  |  |  |  |  | after => $delay, | 
| 163 |  |  |  |  |  |  | cb    => sub { | 
| 164 |  |  |  |  |  |  | undef $timeout; | 
| 165 |  |  |  |  |  |  | #            print "timing out login\n"; # DB | 
| 166 |  |  |  |  |  |  | $done->croak("login timed out after $delay seconds"); | 
| 167 |  |  |  |  |  |  | }, | 
| 168 |  |  |  |  |  |  | ); | 
| 169 |  |  |  |  |  |  |  | 
| 170 |  |  |  |  |  |  | # capture stderr output, interpret as an error | 
| 171 |  |  |  |  |  |  | $stderr->on_read(_croak_with "error" => $done); | 
| 172 |  |  |  |  |  |  |  | 
| 173 |  |  |  |  |  |  | $self->_define_automation( | 
| 174 |  |  |  |  |  |  | start => sub { | 
| 175 |  |  |  |  |  |  | if (_match($stdin => $passwd_prompt_re)) { | 
| 176 |  |  |  |  |  |  | if (!$self->connection_params->has_password) { | 
| 177 |  |  |  |  |  |  | $done->croak('password requested but none provided'); | 
| 178 |  |  |  |  |  |  | return 'auth_failure'; | 
| 179 |  |  |  |  |  |  | } | 
| 180 |  |  |  |  |  |  | my $passwd = $self->connection_params->password; | 
| 181 |  |  |  |  |  |  | _push_write($stdin => "$passwd\n"); | 
| 182 |  |  |  |  |  |  | return 'sent_passwd'; | 
| 183 |  |  |  |  |  |  | } | 
| 184 |  |  |  |  |  |  |  | 
| 185 |  |  |  |  |  |  | if (_match($stdin => $initial_prompt_re)) { | 
| 186 |  |  |  |  |  |  | _push_write($stdin => qq(PS1=$prompt; export PS1\n)); | 
| 187 |  |  |  |  |  |  | return 'expect_prompt'; | 
| 188 |  |  |  |  |  |  | } | 
| 189 |  |  |  |  |  |  | # FIXME limit buffer size and time | 
| 190 |  |  |  |  |  |  | return 'start'; | 
| 191 |  |  |  |  |  |  | }, | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | sent_passwd => sub { | 
| 194 |  |  |  |  |  |  | if (_match($stdin => $passwd_prompt_re)) { | 
| 195 |  |  |  |  |  |  | my $msg = $stderr->{rbuf} || ''; | 
| 196 |  |  |  |  |  |  | $done->croak("auth failure: $msg"); | 
| 197 |  |  |  |  |  |  | return 'auth_failure'; | 
| 198 |  |  |  |  |  |  | } | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | if (_match($stdin => $initial_prompt_re)) { | 
| 201 |  |  |  |  |  |  | _push_write($stdin => qq(PS1=$prompt; export PS1\n)); | 
| 202 |  |  |  |  |  |  | return 'expect_prompt'; | 
| 203 |  |  |  |  |  |  | } | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | return 'sent_passwd'; | 
| 206 |  |  |  |  |  |  | }, | 
| 207 |  |  |  |  |  |  |  | 
| 208 |  |  |  |  |  |  | expect_prompt => sub { | 
| 209 |  |  |  |  |  |  | if (_match($stdin => $prompt_re)) { | 
| 210 |  |  |  |  |  |  | # Cancel stderr monitor | 
| 211 |  |  |  |  |  |  | $stderr->on_read(undef); | 
| 212 |  |  |  |  |  |  |  | 
| 213 |  |  |  |  |  |  | $self->_set_logged_in(1); | 
| 214 |  |  |  |  |  |  | $done->send($stdin, $self); # done | 
| 215 |  |  |  |  |  |  | return 'finished'; | 
| 216 |  |  |  |  |  |  | } | 
| 217 |  |  |  |  |  |  |  | 
| 218 |  |  |  |  |  |  | return 'expect_prompt'; | 
| 219 |  |  |  |  |  |  | }, | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | auth_failure => 0, | 
| 222 |  |  |  |  |  |  | finished => 0, | 
| 223 |  |  |  |  |  |  | ); | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  | return $done; | 
| 226 |  |  |  |  |  |  | } | 
| 227 |  |  |  |  |  |  |  | 
| 228 |  |  |  |  |  |  |  | 
| 229 |  |  |  |  |  |  | sub login { | 
| 230 |  |  |  |  |  |  | #    return (shift->login_async(@_)->recv)[1]; | 
| 231 |  |  |  |  |  |  | my ($cv) = shift->login_async(@_); | 
| 232 |  |  |  |  |  |  | #        printf "$Coro::current about to call recv\n"; # DB | 
| 233 |  |  |  |  |  |  | my $v = ($cv->recv)[1]; | 
| 234 |  |  |  |  |  |  | #        printf "$Coro::current about to called recv\n"; # DB | 
| 235 |  |  |  |  |  |  | return $v; | 
| 236 |  |  |  |  |  |  | } | 
| 237 |  |  |  |  |  |  |  | 
| 238 |  |  |  |  |  |  | sub logout { | 
| 239 |  |  |  |  |  |  | my $self = shift; | 
| 240 |  |  |  |  |  |  | croak "cannot use session yet, as it is not logged in" | 
| 241 |  |  |  |  |  |  | if !$self->is_logged_in; | 
| 242 |  |  |  |  |  |  |  | 
| 243 |  |  |  |  |  |  | _push_write($self->delegate('pty')->handle => "exit\n"); | 
| 244 |  |  |  |  |  |  | return $self; | 
| 245 |  |  |  |  |  |  | } | 
| 246 |  |  |  |  |  |  |  | 
| 247 |  |  |  |  |  |  | sub capture_async { | 
| 248 |  |  |  |  |  |  | my $self = shift; | 
| 249 |  |  |  |  |  |  | my ($cmd) = pos_validated_list( | 
| 250 |  |  |  |  |  |  | \@_, | 
| 251 |  |  |  |  |  |  | { isa => 'Str' }, | 
| 252 |  |  |  |  |  |  | ); | 
| 253 |  |  |  |  |  |  |  | 
| 254 |  |  |  |  |  |  | croak "cannot use session yet, as it is not logged in" | 
| 255 |  |  |  |  |  |  | if !$self->is_logged_in; | 
| 256 |  |  |  |  |  |  |  | 
| 257 |  |  |  |  |  |  | my $stdin = $self->delegate('pty')->handle; | 
| 258 |  |  |  |  |  |  | my $stderr = $self->delegate('stderr')->handle; | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | $cmd =~ s/\s*\z/\n/ms; | 
| 261 |  |  |  |  |  |  |  | 
| 262 |  |  |  |  |  |  | # send command | 
| 263 |  |  |  |  |  |  | _push_write($stdin => $cmd); | 
| 264 |  |  |  |  |  |  |  | 
| 265 |  |  |  |  |  |  | # read result | 
| 266 |  |  |  |  |  |  | my $cumdata = ''; | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  | # we want the _error_event condvar to trigger a croak sent to $done. | 
| 269 |  |  |  |  |  |  | my $done = AnyEvent->condvar; | 
| 270 |  |  |  |  |  |  | # FIXME check _error_event for expiry? | 
| 271 |  |  |  |  |  |  | $self->_error_event->cb(sub { | 
| 272 |  |  |  |  |  |  | #        print "xxxx _error_event\n"; # DB | 
| 273 |  |  |  |  |  |  | $done->croak(shift->recv); | 
| 274 |  |  |  |  |  |  | }); | 
| 275 |  |  |  |  |  |  |  | 
| 276 |  |  |  |  |  |  | # capture stderr output, interpret as a warning | 
| 277 |  |  |  |  |  |  | $stderr->on_read(_warn_with "unexpected stderr from command"); | 
| 278 |  |  |  |  |  |  |  | 
| 279 |  |  |  |  |  |  | my $read_output_cb = sub { | 
| 280 |  |  |  |  |  |  | my ($handle) = @_; | 
| 281 |  |  |  |  |  |  | return unless defined $handle->{rbuf}; | 
| 282 |  |  |  |  |  |  |  | 
| 283 |  |  |  |  |  |  | #        print "got: $handle->{rbuf}\n"; # DB | 
| 284 |  |  |  |  |  |  |  | 
| 285 |  |  |  |  |  |  | $cumdata .= $handle->{rbuf}; | 
| 286 |  |  |  |  |  |  | $handle->{rbuf} = ''; | 
| 287 |  |  |  |  |  |  |  | 
| 288 |  |  |  |  |  |  | $cumdata =~ /(.*?)$prompt_re/ms | 
| 289 |  |  |  |  |  |  | or return; | 
| 290 |  |  |  |  |  |  |  | 
| 291 |  |  |  |  |  |  | # cancel stderr monitor | 
| 292 |  |  |  |  |  |  | $stderr->on_read(undef); | 
| 293 |  |  |  |  |  |  |  | 
| 294 |  |  |  |  |  |  | $done->send($handle, $1); | 
| 295 |  |  |  |  |  |  | return 1; | 
| 296 |  |  |  |  |  |  | }; | 
| 297 |  |  |  |  |  |  |  | 
| 298 |  |  |  |  |  |  | $stdin->push_read($read_output_cb); | 
| 299 |  |  |  |  |  |  |  | 
| 300 |  |  |  |  |  |  | return $done; | 
| 301 |  |  |  |  |  |  | } | 
| 302 |  |  |  |  |  |  |  | 
| 303 |  |  |  |  |  |  |  | 
| 304 |  |  |  |  |  |  | sub capture { | 
| 305 |  |  |  |  |  |  | return (shift->capture_async(@_)->recv)[1]; | 
| 306 |  |  |  |  |  |  | } | 
| 307 |  |  |  |  |  |  |  | 
| 308 |  |  |  |  |  |  |  | 
| 309 |  |  |  |  |  |  | sub sudo_capture_async { | 
| 310 |  |  |  |  |  |  | my $self = shift; | 
| 311 |  |  |  |  |  |  | my ($cmd) = pos_validated_list( | 
| 312 |  |  |  |  |  |  | \@_, | 
| 313 |  |  |  |  |  |  | { isa => 'Str' }, | 
| 314 |  |  |  |  |  |  | ); | 
| 315 |  |  |  |  |  |  |  | 
| 316 |  |  |  |  |  |  | croak "cannot use session yet, as it is not logged in" | 
| 317 |  |  |  |  |  |  | if !$self->is_logged_in; | 
| 318 |  |  |  |  |  |  |  | 
| 319 |  |  |  |  |  |  | my $done = AnyEvent->condvar; | 
| 320 |  |  |  |  |  |  | $self->_error_event->cb(sub { | 
| 321 |  |  |  |  |  |  | #        print "_error_event sent\n"; DB | 
| 322 |  |  |  |  |  |  | $done->croak(shift->recv); | 
| 323 |  |  |  |  |  |  | }); | 
| 324 |  |  |  |  |  |  |  | 
| 325 |  |  |  |  |  |  | # we know we'll need the password, so check this up-front | 
| 326 |  |  |  |  |  |  | if (!$self->connection_params->has_password) { | 
| 327 |  |  |  |  |  |  | croak 'password requested but none provided'; | 
| 328 |  |  |  |  |  |  | } | 
| 329 |  |  |  |  |  |  |  | 
| 330 |  |  |  |  |  |  | my $stdin = $self->delegate('pty')->handle; | 
| 331 |  |  |  |  |  |  | my $stderr = $self->delegate('stderr')->handle; | 
| 332 |  |  |  |  |  |  |  | 
| 333 |  |  |  |  |  |  | my $timeout; | 
| 334 |  |  |  |  |  |  | my $delay = $self->login_timeout; | 
| 335 |  |  |  |  |  |  | $timeout = AnyEvent->timer( | 
| 336 |  |  |  |  |  |  | after => $delay, | 
| 337 |  |  |  |  |  |  | cb    => sub { | 
| 338 |  |  |  |  |  |  | undef $timeout; | 
| 339 |  |  |  |  |  |  | #            print "timing out login\n"; # DB | 
| 340 |  |  |  |  |  |  | $done->croak("sudo_capture timed out after $delay seconds"); | 
| 341 |  |  |  |  |  |  | }, | 
| 342 |  |  |  |  |  |  | ); | 
| 343 |  |  |  |  |  |  |  | 
| 344 |  |  |  |  |  |  | # capture stderr output, interpret as an error | 
| 345 |  |  |  |  |  |  | $stderr->on_read(_croak_with "error" => $done); | 
| 346 |  |  |  |  |  |  |  | 
| 347 |  |  |  |  |  |  | # ensure command has a trailing newline | 
| 348 |  |  |  |  |  |  | $cmd =~ s/\s*\z/\n/ms; | 
| 349 |  |  |  |  |  |  |  | 
| 350 |  |  |  |  |  |  | # get captured result here | 
| 351 |  |  |  |  |  |  | my $cumdata = ''; | 
| 352 |  |  |  |  |  |  |  | 
| 353 |  |  |  |  |  |  | # FIXME escape/untaint $passwd_prompt_re | 
| 354 |  |  |  |  |  |  | # use full path names | 
| 355 |  |  |  |  |  |  |  | 
| 356 |  |  |  |  |  |  | # Authenticate. Erase any cached sudo authentication first - we | 
| 357 |  |  |  |  |  |  | # want to guarantee that we will get a password prompt.  Then | 
| 358 |  |  |  |  |  |  | # start a new shell with sudo. | 
| 359 |  |  |  |  |  |  | _push_write($stdin => "sudo -K; sudo -p '$sudo_passwd_prompt' sh\n"); | 
| 360 |  |  |  |  |  |  |  | 
| 361 |  |  |  |  |  |  | $self->_define_automation( | 
| 362 |  |  |  |  |  |  | start => sub { | 
| 363 |  |  |  |  |  |  | if (_match($stdin => $sudo_passwd_prompt_re)) { | 
| 364 |  |  |  |  |  |  | my $passwd = $self->connection_params->password; | 
| 365 |  |  |  |  |  |  | #                print "sending password\n"; # DB | 
| 366 |  |  |  |  |  |  | _push_write($stdin => "$passwd\n"); | 
| 367 |  |  |  |  |  |  | return 'sent_passwd'; | 
| 368 |  |  |  |  |  |  | } | 
| 369 |  |  |  |  |  |  |  | 
| 370 |  |  |  |  |  |  | # FIXME limit buffer size and time | 
| 371 |  |  |  |  |  |  | return 'start'; | 
| 372 |  |  |  |  |  |  | }, | 
| 373 |  |  |  |  |  |  |  | 
| 374 |  |  |  |  |  |  | sent_passwd => sub { | 
| 375 |  |  |  |  |  |  | if (_match($stdin => $sudo_passwd_prompt_re)) { | 
| 376 |  |  |  |  |  |  | my $msg = $stderr->{rbuf} || ''; | 
| 377 |  |  |  |  |  |  | $done->croak("auth failure: $msg"); | 
| 378 |  |  |  |  |  |  | return 'auth_failure'; | 
| 379 |  |  |  |  |  |  | } | 
| 380 |  |  |  |  |  |  |  | 
| 381 |  |  |  |  |  |  | if (_match($stdin => $prompt_re)) { | 
| 382 |  |  |  |  |  |  | # Cancel stderr monitor | 
| 383 |  |  |  |  |  |  | $stderr->on_read(undef); | 
| 384 |  |  |  |  |  |  |  | 
| 385 |  |  |  |  |  |  | _push_write($stdin => $cmd); | 
| 386 |  |  |  |  |  |  | return 'sent_cmd'; | 
| 387 |  |  |  |  |  |  | } | 
| 388 |  |  |  |  |  |  |  | 
| 389 |  |  |  |  |  |  | return 'sent_passwd'; | 
| 390 |  |  |  |  |  |  | }, | 
| 391 |  |  |  |  |  |  |  | 
| 392 |  |  |  |  |  |  | sent_cmd => sub { | 
| 393 |  |  |  |  |  |  | if (my ($data) = _match($stdin => qr/(.*?)$prompt_re/sm)) { | 
| 394 |  |  |  |  |  |  | $cumdata .= $data; | 
| 395 |  |  |  |  |  |  | #                print "got data: $data\n<$stdin->{rbuf}>\n"; # DB | 
| 396 |  |  |  |  |  |  |  | 
| 397 |  |  |  |  |  |  | $stdin->{rbuf} = ''; | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | # capture stderr output, interpret as a warning | 
| 400 |  |  |  |  |  |  | $stderr->on_read(_warn_with "unexpected stderr from sudo command"); | 
| 401 |  |  |  |  |  |  |  | 
| 402 |  |  |  |  |  |  | # exit sudo shell | 
| 403 |  |  |  |  |  |  | _push_write($stdin => "exit\n"); | 
| 404 |  |  |  |  |  |  |  | 
| 405 |  |  |  |  |  |  | return 'exited_shell'; | 
| 406 |  |  |  |  |  |  | } | 
| 407 |  |  |  |  |  |  |  | 
| 408 |  |  |  |  |  |  | $cumdata .= $stdin->{rbuf}; | 
| 409 |  |  |  |  |  |  | $stdin->{rbuf} = ''; | 
| 410 |  |  |  |  |  |  | return 'sent_cmd'; | 
| 411 |  |  |  |  |  |  | }, | 
| 412 |  |  |  |  |  |  |  | 
| 413 |  |  |  |  |  |  | exited_shell => sub { | 
| 414 |  |  |  |  |  |  | if (_match($stdin => $prompt_re)) { | 
| 415 |  |  |  |  |  |  | # Cancel stderr monitor | 
| 416 |  |  |  |  |  |  | $stderr->on_read(undef); | 
| 417 |  |  |  |  |  |  |  | 
| 418 |  |  |  |  |  |  | # remove any output from the exit | 
| 419 |  |  |  |  |  |  | # FIXME should this check that everything has been consumed? | 
| 420 |  |  |  |  |  |  | $stdin->{rbuf} = ''; | 
| 421 |  |  |  |  |  |  |  | 
| 422 |  |  |  |  |  |  | $done->send($stdin, $cumdata); # done, send data collected | 
| 423 |  |  |  |  |  |  | return 'finished'; | 
| 424 |  |  |  |  |  |  | } | 
| 425 |  |  |  |  |  |  |  | 
| 426 |  |  |  |  |  |  | return 'exited_shell'; | 
| 427 |  |  |  |  |  |  | }, | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | auth_failure => 0, | 
| 430 |  |  |  |  |  |  | finished => 0, | 
| 431 |  |  |  |  |  |  | ); | 
| 432 |  |  |  |  |  |  |  | 
| 433 |  |  |  |  |  |  | return $done; | 
| 434 |  |  |  |  |  |  | } | 
| 435 |  |  |  |  |  |  |  | 
| 436 |  |  |  |  |  |  | sub sudo_capture { | 
| 437 |  |  |  |  |  |  | return (shift->sudo_capture_async(@_)->recv)[1]; | 
| 438 |  |  |  |  |  |  | } | 
| 439 |  |  |  |  |  |  |  | 
| 440 |  |  |  |  |  |  |  | 
| 441 |  |  |  |  |  |  | __PACKAGE__->meta->make_immutable; | 
| 442 |  |  |  |  |  |  | 1; | 
| 443 |  |  |  |  |  |  |  | 
| 444 |  |  |  |  |  |  | __END__ | 
| 445 |  |  |  |  |  |  |  | 
| 446 |  |  |  |  |  |  | =head1 NAME | 
| 447 |  |  |  |  |  |  |  | 
| 448 |  |  |  |  |  |  | Net::SSH::Mechanize::Session - manage a running ssh process. | 
| 449 |  |  |  |  |  |  |  | 
| 450 |  |  |  |  |  |  | =head1 VERSION | 
| 451 |  |  |  |  |  |  |  | 
| 452 |  |  |  |  |  |  | version 0.1.3 | 
| 453 |  |  |  |  |  |  |  | 
| 454 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 455 |  |  |  |  |  |  |  | 
| 456 |  |  |  |  |  |  | This class represents a sunning C<ssh> process. It is a subclass of | 
| 457 |  |  |  |  |  |  | C<AnyEvent::Subprocess::Running>, with methods to manage the | 
| 458 |  |  |  |  |  |  | authentication and other interaction with the sub-process. | 
| 459 |  |  |  |  |  |  |  | 
| 460 |  |  |  |  |  |  | Typically you will not create one directly, but obtain one via | 
| 461 |  |  |  |  |  |  | C<< Net::SSH::Mechanize::Session->login >>, | 
| 462 |  |  |  |  |  |  | or C<< Net::SSH::Mechanize->session >> | 
| 463 |  |  |  |  |  |  |  | 
| 464 |  |  |  |  |  |  | You might invoke methods directly, or via C<Net::SSH::Mechanize> | 
| 465 |  |  |  |  |  |  | instance's methods which delegate to the instance's C<session> | 
| 466 |  |  |  |  |  |  | attribute (which is an instance of this class). | 
| 467 |  |  |  |  |  |  |  | 
| 468 |  |  |  |  |  |  | use Net::SSH::Mechanize; | 
| 469 |  |  |  |  |  |  |  | 
| 470 |  |  |  |  |  |  | my $mech = Net::SSH::Mechanize->new(hostname => 'somewhere'); | 
| 471 |  |  |  |  |  |  |  | 
| 472 |  |  |  |  |  |  | my $session = $mech->session; | 
| 473 |  |  |  |  |  |  | # ... | 
| 474 |  |  |  |  |  |  |  | 
| 475 |  |  |  |  |  |  | =head1 CLASS METHODS | 
| 476 |  |  |  |  |  |  |  | 
| 477 |  |  |  |  |  |  | =head2 C<< $obj = $class->new(%params) >> | 
| 478 |  |  |  |  |  |  |  | 
| 479 |  |  |  |  |  |  | Creates a new instance.  Not intended for public use.  Use | 
| 480 |  |  |  |  |  |  | C<< Net::SSH::Mechanize->session >> instead. | 
| 481 |  |  |  |  |  |  |  | 
| 482 |  |  |  |  |  |  | =head1 INSTANCE ATTRIBUTES | 
| 483 |  |  |  |  |  |  |  | 
| 484 |  |  |  |  |  |  | =head2 C<< $params = $obj->connection_params >> | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | This is a read-only accessor for the C<connection_params> instance | 
| 487 |  |  |  |  |  |  | passed to the constructor by C<Net::SSH::Mechanize>. | 
| 488 |  |  |  |  |  |  |  | 
| 489 |  |  |  |  |  |  | =head2 C<< $obj->login_timeout($integer) >> | 
| 490 |  |  |  |  |  |  | =head2 C<< $integer = $obj->login_timeout >> | 
| 491 |  |  |  |  |  |  |  | 
| 492 |  |  |  |  |  |  | This is a read-write accessor to the log-in timeout parameter passed | 
| 493 |  |  |  |  |  |  | to the constructor. | 
| 494 |  |  |  |  |  |  |  | 
| 495 |  |  |  |  |  |  | If you plan to modify it, do so before C<< ->login >> or | 
| 496 |  |  |  |  |  |  | C<< ->login_async >> has been invoked or it will not have any effect | 
| 497 |  |  |  |  |  |  | on anything. | 
| 498 |  |  |  |  |  |  |  | 
| 499 |  |  |  |  |  |  | =head1 INSTANCE METHODS | 
| 500 |  |  |  |  |  |  |  | 
| 501 |  |  |  |  |  |  | Note, all of these will throw an exception if used before C<< ->login >> | 
| 502 |  |  |  |  |  |  | or before C<< ->login_async >> has successfully completed, except | 
| 503 |  |  |  |  |  |  | of course C<< ->login >> and C<< ->login_async >> themselves. | 
| 504 |  |  |  |  |  |  | These latter methods do nothing after the first invocation. | 
| 505 |  |  |  |  |  |  |  | 
| 506 |  |  |  |  |  |  | =head2 C<< $session = $obj->login >> | 
| 507 |  |  |  |  |  |  |  | 
| 508 |  |  |  |  |  |  | This method logs into the remote host using the defined connection | 
| 509 |  |  |  |  |  |  | parameters, and returns a C<Net::SSH::Mechanize::Session> instance on | 
| 510 |  |  |  |  |  |  | success, or throws an exception on failure. | 
| 511 |  |  |  |  |  |  |  | 
| 512 |  |  |  |  |  |  | It is safe to use in C<AnyEvent> applications or C<Coro> co-routines, | 
| 513 |  |  |  |  |  |  | because the implementation is asynchronous and will not block the | 
| 514 |  |  |  |  |  |  | whole process. | 
| 515 |  |  |  |  |  |  |  | 
| 516 |  |  |  |  |  |  | =head2 C<< $condvar = $obj->login_async >> | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | This is an asynchronous method used to implement the synchronous | 
| 519 |  |  |  |  |  |  | C<< ->login >> method.  It returns an AnyEvent::CondVar instance | 
| 520 |  |  |  |  |  |  | immediately, which can be used to wait for completion, or register a | 
| 521 |  |  |  |  |  |  | callback to be notified when the log-in has completed. | 
| 522 |  |  |  |  |  |  |  | 
| 523 |  |  |  |  |  |  | =head2 C<< $obj->logout >> | 
| 524 |  |  |  |  |  |  |  | 
| 525 |  |  |  |  |  |  | Logs out of the remote host by issuing an "exit" command. | 
| 526 |  |  |  |  |  |  |  | 
| 527 |  |  |  |  |  |  | =head2 C<< $condvar = $obj->capture_async($command) >> | 
| 528 |  |  |  |  |  |  |  | 
| 529 |  |  |  |  |  |  | The returns a condvar immediately, which can be used to wait for | 
| 530 |  |  |  |  |  |  | successful completion (or otherwise) of the command(s) defined by | 
| 531 |  |  |  |  |  |  | C<$command>. | 
| 532 |  |  |  |  |  |  |  | 
| 533 |  |  |  |  |  |  | =head2 C<< $result = $obj->capture($command) >> | 
| 534 |  |  |  |  |  |  |  | 
| 535 |  |  |  |  |  |  | This invokes the command(s) defined by C<$command> on the remote host, | 
| 536 |  |  |  |  |  |  | and returns the result. | 
| 537 |  |  |  |  |  |  |  | 
| 538 |  |  |  |  |  |  | =head2 C<< $condvar = $obj->sudo_capture_async($command) >> | 
| 539 |  |  |  |  |  |  |  | 
| 540 |  |  |  |  |  |  | The returns a condvar immediately, which can be used to wait for | 
| 541 |  |  |  |  |  |  | successful completion (or otherwise) in a sudo'ed sub-shell of the | 
| 542 |  |  |  |  |  |  | command(s) defined by C<$command>. | 
| 543 |  |  |  |  |  |  |  | 
| 544 |  |  |  |  |  |  | A password is required in C<connection_params> for this to | 
| 545 |  |  |  |  |  |  | authenticate with sudo. | 
| 546 |  |  |  |  |  |  |  | 
| 547 |  |  |  |  |  |  | =head2 C<< $result = $obj->sudo_capture($command) >> | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | This invokes the command(s) defined by C<$command> in a sudo'ed sub-shell | 
| 550 |  |  |  |  |  |  | on the remote host, and returns the result. | 
| 551 |  |  |  |  |  |  |  | 
| 552 |  |  |  |  |  |  |  | 
| 553 |  |  |  |  |  |  | =head1 AUTHOR | 
| 554 |  |  |  |  |  |  |  | 
| 555 |  |  |  |  |  |  | Nick Stokoe  C<< <wulee@cpan.org> >> | 
| 556 |  |  |  |  |  |  |  | 
| 557 |  |  |  |  |  |  |  | 
| 558 |  |  |  |  |  |  | =head1 LICENCE AND COPYRIGHT | 
| 559 |  |  |  |  |  |  |  | 
| 560 |  |  |  |  |  |  | Copyright (c) 2011, Nick Stokoe C<< <wulee@cpan.org> >>. All rights reserved. | 
| 561 |  |  |  |  |  |  |  | 
| 562 |  |  |  |  |  |  | This module is free software; you can redistribute it and/or | 
| 563 |  |  |  |  |  |  | modify it under the same terms as Perl itself. See L<perlartistic>. |