Bladeren bron

Yay persistent shell working without bullshit

Andy Baugh 3 maanden geleden
bovenliggende
commit
ca31569e96
2 gewijzigde bestanden met toevoegingen van 134 en 103 verwijderingen
  1. 126 95
      lib/Net/OpenSSH/More.pm
  2. 8 8
      t/Net-OpenSSH-More.t

+ 126 - 95
lib/Net/OpenSSH/More.pm

@@ -18,6 +18,7 @@ use List::Util qw{first};
 use Net::DNS::Resolver ();
 use Net::DNS::Resolver ();
 use Net::IP            ();
 use Net::IP            ();
 use Time::HiRes        ();
 use Time::HiRes        ();
+use Term::ANSIColor    ();
 
 
 =head1 NAME
 =head1 NAME
 
 
@@ -53,53 +54,6 @@ Highlights:
 Net::OpenSSH
 Net::OpenSSH
 Net::OpenSSH::More::Linux
 Net::OpenSSH::More::Linux
 
 
-=head1 METHODS
-
-=head2 new
-
-Instantiate the object, establish the connection. Note here that I'm not allowing
-a connection string like the parent module, and instead exploding these out into
-opts to pass to the constructor. This is because we want to index certain things
-under the hood by user, etc. and I *do not* want to use a regexp to pick out
-your username, host, port, etc. when this problem is solved much more easily
-by forcing that separation on the caller's end.
-
-ACCEPTS:
-* %opts - <HASH> A hash of key value pairs corresponding to the what you would normally pass in to Net::OpenSSH,
-  along with the following keys:
-  * use_persistent_shell - Whether or not to setup Expect to watch a persistent TTY. Less stable, but faster.
-  * no_agent - Pass in a truthy value to disable the SSH agent. By default the agent is enabled.
-  * die_on_drop - If, for some reason, the connection drops, just die instead of attempting reconnection.
-  * output_prefix - If given, is what we will tack onto the beginning of any output via diag method.
-    useful for streaming output to say, a TAP consumer (test) via passing in '# ' as prefix.
-  * debug - Pass in a truthy value to enable certain diag statements I've added in the module and pass -v to ssh.
-  * home - STRING corresponding to an absolute path to something that "looks like" a homedir. Defaults to the user's homedir.
-    useful in cases where you say, want to load SSH keys from a different path without changing assumptions about where
-    keys exist in a homedir on your average OpenSSH using system.
-  * no_cache - Pass in a truthy value to disable caching the connection and object, indexed by host string.
-    useful if for some reason you need many separate connections to test something. Make sure your MAX_SESSIONS is set sanely
-    in sshd_config if you use this extensively.
-  * retry_interval - In the case that sshd is not up on the remote host, how long to wait while before reattempting connection.
-    defaults to 6s. We retry $RETRY_MAX times, so this means waiting a little over a minute for SSH to come up by default.
-	If your situation requires longer intervals, pass in something longer.
-  * retry_max - Number of times to retry when a connection fails. Defaults to 10.
-
-RETURNS a Net::OpenSSH::More object.
-
-=head3 A note on Authentication order
-
-We attempt to authenticate using the following details, and in this order:
-1) Use supplied key_path.
-2) Use supplied password.
-3) Use existing SSH agent (SSH_AUTH_SOCK environment variable)
-4) Use keys that may exist in $HOME/.ssh - id_rsa, id_dsa and id_ecdsa (in that order).
-
-If all methods therein fail, we will die, as nothing will likely work at that point.
-It is important to be aware of this if your remove host has something like fail2ban or cPHulkd
-enabled which monitors and blocks access based on failed login attempts. If this is you,
-ensure that you have not configured things in a way as to accidentally lock yourself out
-of the remote host just because you fatfingered a connection detail in the constructor.
-
 =cut
 =cut
 
 
 my %defaults = (
 my %defaults = (
@@ -221,6 +175,7 @@ my $init_ssh = sub {
 	# Leave no trace!
 	# Leave no trace!
 	$opts->{'_tmp_obj'} = File::Temp->newdir() if !$opts->{'_tmp_obj'};
 	$opts->{'_tmp_obj'} = File::Temp->newdir() if !$opts->{'_tmp_obj'};
     my $tmp_dir = $opts->{'_tmp_obj'}->dirname();
     my $tmp_dir = $opts->{'_tmp_obj'}->dirname();
+    diag( { '_opts' => $opts }, "Temp dir: $tmp_dir" ) if $opts->{'debug'};
     my $temp_fh;
     my $temp_fh;
 
 
     # Use an existing connection if possible, otherwise make one
     # Use an existing connection if possible, otherwise make one
@@ -276,7 +231,7 @@ my $init_ssh = sub {
 		my $error = $self->error;
 		my $error = $self->error;
         next unless ref $self eq 'Net::OpenSSH::More' && !$error;
         next unless ref $self eq 'Net::OpenSSH::More' && !$error;
 
 
-        if ( -s $temp_fh ) {
+        if ( $temp_fh && -s $temp_fh ) {
             seek( $temp_fh, 0, Fcntl::SEEK_SET );
             seek( $temp_fh, 0, Fcntl::SEEK_SET );
             local $/;
             local $/;
             $error .= " " . readline($temp_fh);
             $error .= " " . readline($temp_fh);
@@ -316,6 +271,7 @@ my $connection_check = sub {
     my ( $self ) = @_;
     my ( $self ) = @_;
     return 1 if $self->check_master;
     return 1 if $self->check_master;
 	local $@;
 	local $@;
+    local $disable_destructor = 1;
     eval { $self = $init_ssh->( __PACKAGE__, $self->{'_opts'}) };
     eval { $self = $init_ssh->( __PACKAGE__, $self->{'_opts'}) };
     return $@ ? 0 : 1;
     return $@ ? 0 : 1;
 };
 };
@@ -337,7 +293,7 @@ my $call_ssh_reinit_if_check_fails = sub {
     local $@;
     local $@;
     my @ret       = eval { $self->$func(@args) };
     my @ret       = eval { $self->$func(@args) };
     my $ssh_error = $@ || $self->error;
     my $ssh_error = $@ || $self->error;
-    $self->diag("[WARN] $ssh_error") if $ssh_error;
+    warn "[WARN] $ssh_error" if $ssh_error;
     return @ret if !$ssh_error;
     return @ret if !$ssh_error;
 
 
     $connection_check->($self);
     $connection_check->($self);
@@ -362,8 +318,11 @@ my $trim = sub {
 };
 };
 
 
 my $send = sub {
 my $send = sub {
-    my ( $self, $line_reader, @cmd ) = @_;
-    my ( $pty, $err, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open3pty', @cmd );
+    my ( $self, $line_reader, @command ) = @_;
+
+    $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC " . join( " ", @command ) ) if $self->{'_opts'}{'debug'};
+
+    my ( $pty, $err, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open3pty', @command );
     $die_no_trace->("Net::OpenSSH::open3pty failed: $err") if( !defined $pid || $self->error() );
     $die_no_trace->("Net::OpenSSH::open3pty failed: $err") if( !defined $pid || $self->error() );
 
 
     $self->{'_out'} = "";
     $self->{'_out'} = "";
@@ -385,45 +344,47 @@ my $send = sub {
     $line_reader->( $self, $out, '_err' ) while $out = $err->getline;
     $line_reader->( $self, $out, '_err' ) while $out = $err->getline;
     $err->close;
     $err->close;
 
 
-    $self->{'_pid'} = $pid;
     waitpid( $pid, 0 );
     waitpid( $pid, 0 );
     return $? >> 8;
     return $? >> 8;
 };
 };
 
 
-my $TERMINATOR = "\r\r";
+my $TERMINATOR = "\r\n";
 my $send_persistent_cmd = sub {
 my $send_persistent_cmd = sub {
-    my ( $self, $cmd, $uuid ) = @_;
+    my ( $self, $command, $uuid ) = @_;
 
 
     $uuid //= Data::UUID->new()->create_str();
     $uuid //= Data::UUID->new()->create_str();
+    $command = join( ' ', @$command );
+    my $actual_cmd = "UUID='$uuid'; echo \"BEGIN \$UUID\"; $command; echo \"___\$?___\"; echo; echo \"EOF \$UUID\"";
+    $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC $actual_cmd" ) if $self->{'_opts'}{'debug'};
 
 
     #Use command on bash to ignore stuff like aliases so that we have a minimum level of PEBKAC errors due to aliasing cp to cp -i, etc.
     #Use command on bash to ignore stuff like aliases so that we have a minimum level of PEBKAC errors due to aliasing cp to cp -i, etc.
-    $self->{'expect'}->print("UUID='$uuid'; echo \"BEGIN \$UUID\"; command $cmd ; echo \"___\$?___\"; echo; echo \"EOF \$UUID\" $TERMINATOR");
+    $self->{'expect'}->print("${actual_cmd}${TERMINATOR}");
 
 
-    #Rather than take the approach of Cpanel::Expect::cmd_then_poll, it seemed more straightforward to echo unique strings before and after the command.
-    #This made getting the return code somewhat more complicated, as you can see below.  That said, Cpanel::Expect appears to not concern itself with such things.
+    # Rather than take the approach of cPanel, which commands then polls async,
+    # it is more straightforward to echo unique strings before and after the command.
+    # This made getting the return code somewhat more complicated, as you can see below.
+    # That said, it also makes you not have to worry about doing things asynchronously.
+    $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/BEGIN $uuid/m );
+    $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/EOF $uuid/m ); # If nothing is printed in timeout, give up
 
 
-    $self->{'expect'}->expect( 30,                        '-re', qr/BEGIN $uuid/m );
-    $self->{'expect'}->expect( $self->{'expect_timeout'}, '-re', qr/EOF $uuid/m );     #If nothing is printed in 2mins, give up
-
-    #Get the actual output, remove terminal grunk
+    # Get the actual output, remove terminal grunk
     my $message = $trim->( $self->{'expect'}->before() );
     my $message = $trim->( $self->{'expect'}->before() );
-    $message =~ s/[\r\n]{1,2}$//;                                                      #Remove 'secret newline' control chars
-    $message =~ s/\x{d}//g;                                                            #More control chars
-    $message = Term::ANSIColor::colorstrip($message);                                  #Strip colors
+    $message =~ s/[\r\n]{1,2}$//;                                                      # Remove 'secret newline' control chars
+    $message =~ s/\x{d}//g;                                                            # More control chars
+    $message = Term::ANSIColor::colorstrip($message);                                  # Strip colors
 
 
-    #Find the exit code
+    # Find the exit code
     my ($code) = $message =~ m/___(\d*)___$/;
     my ($code) = $message =~ m/___(\d*)___$/;
     unless ( defined $code ) {
     unless ( defined $code ) {
 
 
-        #Tell the user if they've made a boo-boo
+        # Tell the user if they've made a boo-boo
         my $possible_err = $trim->( $self->{'expect'}->before() );
         my $possible_err = $trim->( $self->{'expect'}->before() );
         $possible_err =~ s/\s//g;
         $possible_err =~ s/\s//g;
         $die_no_trace->("Runaway multi-line string detected.  Please adjust the command passed.") if $possible_err =~ m/\>/;
         $die_no_trace->("Runaway multi-line string detected.  Please adjust the command passed.") if $possible_err =~ m/\>/;
 
 
         $die_no_trace->("Could not determine exit code!
         $die_no_trace->("Could not determine exit code!
-            It timed out (went 30s without printing anything).
-            Run command outside of the persistent terminal please.
-            (pass use_persistent_shell => 0 as opt to cmd)"
+            It timed out (went $self->{'_opts'}{'expect_timeout'}s without printing anything).
+            Run command outside of the persistent terminal please."
         );
         );
     }
     }
     $message =~ s/___(\d*)___$//g;
     $message =~ s/___(\d*)___$//g;
@@ -431,30 +392,30 @@ my $send_persistent_cmd = sub {
     return ( $message, $code );
     return ( $message, $code );
 };
 };
 
 
-# XXX TODO ALLOW ARRAY YOU BAG BITER
 my $do_persistent_command = sub {
 my $do_persistent_command = sub {
-    my ( $self, $cmd, $no_stderr ) = @_;
-
-    #XXX casting runes, but SSHControl & Cpanel::Expect do it...
-    #local $| = 1;
-    #local $ENV{'TERM'} = 'dumb';
+    my ( $self, $command, $no_stderr ) = @_;
 
 
     if ( !$self->{'persistent_shell'} ) {
     if ( !$self->{'persistent_shell'} ) {
-        my ( $pty, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open2pty', 'bash' );
+        my ( $pty, $pid ) = $call_ssh_reinit_if_check_fails->( $self, 'open2pty', $self->{'_remote_shell'} );
         die "Got no pty back from open2pty: " . $self->error if !$pty;
         die "Got no pty back from open2pty: " . $self->error if !$pty;
 
 
-        #XXX this all seems to be waving a dead chicken, but SSHControl and Cpanel::Expect do it, so...
+        # You might think that the below settings are important.
+        # In most cases, they are not.
         $pty->set_raw();
         $pty->set_raw();
         $pty->stty( 'raw', 'icrnl', '-echo' );
         $pty->stty( 'raw', 'icrnl', '-echo' );
         $pty->slave->stty( 'raw', 'icrnl', '-echo' );
         $pty->slave->stty( 'raw', 'icrnl', '-echo' );
 
 
         #Hook in expect
         #Hook in expect
+        $self->diag("[DEBUG][$self->{'_opts'}{'host'}] INIT expect on for PTY with pid $pid") if $self->{'_opts'}{'debug'};
         $self->{'expect'} = Expect->init($pty);
         $self->{'expect'} = Expect->init($pty);
         $self->{'expect'}->restart_timeout_upon_receive(1);    #Logabandon by default
         $self->{'expect'}->restart_timeout_upon_receive(1);    #Logabandon by default
-        $self->{'expect'}->print("export PS1=''; unset HISTFILE; stty raw icrnl -echo; echo 'EOF' $TERMINATOR");
-        $self->{'expect'}->expect( 10, 'EOF' );
+
+        # XXX WARNING bashisms. That said, I'm not sure how to better do this yet portably.
+        my $expect_env_cmd = "export PS1=''; export TERM='dumb'; unset HISTFILE; export FOE='configured'; stty raw icrnl -echo; unalias -a; echo \"EOF=\$FOE\"";
+        $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC $expect_env_cmd") if $self->{'_opts'}{'debug'};
+        $self->{'expect'}->print("${expect_env_cmd}${TERMINATOR}");
+        $self->{'expect'}->expect( $self->{'_opts'}{'expect_timeout'}, '-re', qr/EOF=configured/ );
         $self->{'expect'}->clear_accum();
         $self->{'expect'}->clear_accum();
-        $self->{'expect_timeout'} //= 30;
 
 
         #cache
         #cache
         $self->{'persistent_shell'} = $pty;
         $self->{'persistent_shell'} = $pty;
@@ -463,17 +424,17 @@ my $do_persistent_command = sub {
 
 
     #execute the command
     #execute the command
     my $uuid = Data::UUID->new()->create_str();
     my $uuid = Data::UUID->new()->create_str();
-    $cmd .= " 2> /tmp/stderr_$uuid.out" unless $no_stderr;
-    my ( $oot, $code ) = $send_persistent_cmd->( $self, $cmd, $uuid );
+    push @$command, '2>', "/tmp/stderr_$uuid.out" unless $no_stderr;
+    my ( $oot, $code ) = $send_persistent_cmd->( $self, $command, $uuid );
     $self->{'_out'} = $oot;
     $self->{'_out'} = $oot;
 
 
     unless ($no_stderr) {
     unless ($no_stderr) {
 
 
         #Grab stderr
         #Grab stderr
-        ( $self->{'_err'} ) = $send_persistent_cmd->( $self, "cat /tmp/stderr_$uuid.out" );
+        ( $self->{'_err'} ) = $send_persistent_cmd->( $self, [ '/usr/bin/cat', "/tmp/stderr_$uuid.out" ] );
 
 
         #Clean up
         #Clean up
-        $send_persistent_cmd->( $self, "rm -f /tmp/stderr_$uuid.out" );
+        $send_persistent_cmd->( $self, [ '/usr/bin/rm', '-f', "/tmp/stderr_$uuid.out" ] );
     }
     }
 
 
     return int($code);
     return int($code);
@@ -483,9 +444,62 @@ my $do_persistent_command = sub {
 # END PRIVATE METHODS #
 # END PRIVATE METHODS #
 #######################
 #######################
 
 
+=head1 METHODS
+
+=head2 new
+
+Instantiate the object, establish the connection. Note here that I'm not allowing
+a connection string like the parent module, and instead exploding these out into
+opts to pass to the constructor. This is because we want to index certain things
+under the hood by user, etc. and I *do not* want to use a regexp to pick out
+your username, host, port, etc. when this problem is solved much more easily
+by forcing that separation on the caller's end.
+
+ACCEPTS:
+* %opts - <HASH> A hash of key value pairs corresponding to the what you would normally pass in to Net::OpenSSH,
+  along with the following keys:
+  * use_persistent_shell - Whether or not to setup Expect to watch a persistent TTY. Less stable, but faster.
+  * expect_timeout - When the above is active, how long should we wait before your program prints something
+    before bailing out?
+  * no_agent - Pass in a truthy value to disable the SSH agent. By default the agent is enabled.
+  * die_on_drop - If, for some reason, the connection drops, just die instead of attempting reconnection.
+  * output_prefix - If given, is what we will tack onto the beginning of any output via diag method.
+    useful for streaming output to say, a TAP consumer (test) via passing in '# ' as prefix.
+  * debug - Pass in a truthy value to enable certain diag statements I've added in the module and pass -v to ssh.
+  * home - STRING corresponding to an absolute path to something that "looks like" a homedir. Defaults to the user's homedir.
+    useful in cases where you say, want to load SSH keys from a different path without changing assumptions about where
+    keys exist in a homedir on your average OpenSSH using system.
+  * no_cache - Pass in a truthy value to disable caching the connection and object, indexed by host string.
+    useful if for some reason you need many separate connections to test something. Make sure your MAX_SESSIONS is set sanely
+    in sshd_config if you use this extensively.
+  * retry_interval - In the case that sshd is not up on the remote host, how long to wait while before reattempting connection.
+    defaults to 6s. We retry $RETRY_MAX times, so this means waiting a little over a minute for SSH to come up by default.
+	If your situation requires longer intervals, pass in something longer.
+  * retry_max - Number of times to retry when a connection fails. Defaults to 10.
+
+RETURNS a Net::OpenSSH::More object.
+
+=head3 A note on Authentication order
+
+We attempt to authenticate using the following details, and in this order:
+1) Use supplied key_path.
+2) Use supplied password.
+3) Use existing SSH agent (SSH_AUTH_SOCK environment variable)
+4) Use keys that may exist in $HOME/.ssh - id_rsa, id_dsa and id_ecdsa (in that order).
+
+If all methods therein fail, we will die, as nothing will likely work at that point.
+It is important to be aware of this if your remove host has something like fail2ban or cPHulkd
+enabled which monitors and blocks access based on failed login attempts. If this is you,
+ensure that you have not configured things in a way as to accidentally lock yourself out
+of the remote host just because you fatfingered a connection detail in the constructor.
+
+=cut
+
 sub new {
 sub new {
     my ( $class, %opts ) = @_;
     my ( $class, %opts ) = @_;
     $opts{'host'} = '127.0.0.1' if !$opts{'host'} || $opts{'host'} eq 'localhost';
     $opts{'host'} = '127.0.0.1' if !$opts{'host'} || $opts{'host'} eq 'localhost';
+    $opts{'remote_shell'} ||= 'bash'; # prevent stupid defaults
+    $opts{'expect_timeout'} //= 30; # If your program goes over 30s without printing...
 
 
     # Set defaults, check if we can return early
     # Set defaults, check if we can return early
     %opts = ( %defaults, %opts );
     %opts = ( %defaults, %opts );
@@ -500,11 +514,8 @@ sub new {
     $check_local_perms->( "$opts{'home'}/.ssh/config", 0600 )    if -e "$opts{'home'}/.ssh/config";
     $check_local_perms->( "$opts{'home'}/.ssh/config", 0600 )    if -e "$opts{'home'}/.ssh/config";
 
 
     # Make the connection
     # Make the connection
-    my $self = $cache{$opts{'_cache_index'}} = $init_ssh->( $class, \%opts );
-
-    # Stash the originating pid, as running the destructor when
-    # you have forked past instantiation means you have a bad time
-    $self->{'ppid'} = $$;
+    my $self = $init_ssh->( $class, \%opts );
+    $cache{$opts{'_cache_index'}} = $self unless $opts{'no_cache'};
 
 
     # Stash opts for later
     # Stash opts for later
     $self->{'_opts'} = \%opts;
     $self->{'_opts'} = \%opts;
@@ -515,6 +526,19 @@ sub new {
     return $self;
     return $self;
 };
 };
 
 
+=head2 use_persistent_shell
+
+Pass "defined but falsy/truthy" to this to enable using the persistent shell or deactivate its' use.
+Returns either the value you just set or the value it last had (if arg is not defined).
+
+=cut
+
+sub use_persistent_shell {
+    my ($self, $use_shell) = @_;
+    return $self->{'_opts'}{'use_persistent_shell'} if !defined($use_shell);
+    return $self->{'_opts'}{'use_persistent_shell'} = $use_shell;
+}
+
 =head2 DESTROY
 =head2 DESTROY
 
 
 Noted in POD only because of some behavior differences between the
 Noted in POD only because of some behavior differences between the
@@ -526,7 +550,7 @@ the parent's destructor kicks in:
 
 
 sub DESTROY {
 sub DESTROY {
     my ($self) = @_;
     my ($self) = @_;
-    return if $$ != $self->{'ppid'} || $disable_destructor;
+    return if !$self->{'_perl_pid'} || $$ != $self->{'_perl_pid'} || $disable_destructor;
 	$ENV{SSH_AUTH_SOCK} = $self->{'_opts'}{'_restore_auth_sock'} if $self->{'_opts'}{'_restore_auth_sock'};
 	$ENV{SSH_AUTH_SOCK} = $self->{'_opts'}{'_restore_auth_sock'} if $self->{'_opts'}{'_restore_auth_sock'};
     $self->{'persistent_shell'}->close() if $self->{'persistent_shell'};
     $self->{'persistent_shell'}->close() if $self->{'persistent_shell'};
 
 
@@ -577,7 +601,9 @@ Also, be careful with changing directory;
 this can cause unexpected side-effects in your code.
 this can cause unexpected side-effects in your code.
 Changing shell with chsh will also be ignored;
 Changing shell with chsh will also be ignored;
 the persistent shell is what you started with no matter what.
 the persistent shell is what you started with no matter what.
-In those cases, you should pass no_persist as a true value to fork and execute the command by itself.
+In those cases, use_persistent_shell should be called to disable that before calling this.
+Also note that persistent mode basically *requires* you to use bash.
+I am not yet aware of how to make this better yet.
 
 
 If the 'debug' opt to the constructor is set, every command executed hereby will be printed.
 If the 'debug' opt to the constructor is set, every command executed hereby will be printed.
 
 
@@ -592,12 +618,17 @@ In no_persist mode, stderr and stdout are merged, making the $err parameter retu
 sub cmd {
 sub cmd {
     my ( $self ) = shift;
     my ( $self ) = shift;
 	my $opts = ref $_[0] eq 'HASH' ? shift : {};
 	my $opts = ref $_[0] eq 'HASH' ? shift : {};
-	my @cmd = @_;
+	my @command = @_;
 
 
-    $die_no_trace->( 'No command specified', 'PEBCAK' ) if !@cmd;
-    $self->diag("[DEBUG][$self->{'_opts'}{'host'}] EXEC " . join( " ", @cmd ) ) if $self->{'_opts'}{'debug'};
+    $die_no_trace->( 'No command specified', 'PEBCAK' ) if !@command;
 
 
-    my $ret = $opts->{'no_persist'} ? $send->( $self, undef, @cmd ) : $do_persistent_command->( $self, \@cmd, $opts->{'no_stderr'} );
+    my $ret;
+    if( $self->{'_opts'}{'use_persistent_shell'} ) {
+         $ret = $do_persistent_command->( $self, \@command, $opts->{'no_stderr'} )
+    }
+    else {
+        $ret = $send->( $self, undef, @command );
+    }
     chomp( my $out = $self->{'_out'} );
     chomp( my $out = $self->{'_out'} );
     my $err = $self->error || '';
     my $err = $self->error || '';
 
 

+ 8 - 8
t/Net-OpenSSH-More.t

@@ -6,7 +6,6 @@ use Test2::Tools::Explain;
 use Test2::Tools::Subtest qw{subtest_streamed};
 use Test2::Tools::Subtest qw{subtest_streamed};
 use Test2::Plugin::NoWarnings;
 use Test2::Plugin::NoWarnings;
 use Test::MockModule qw{strict};
 use Test::MockModule qw{strict};
-use Carp::Always;
 
 
 use FindBin;
 use FindBin;
 
 
@@ -17,16 +16,17 @@ use Net::OpenSSH::More;
 subtest_streamed "Live tests versus localhost" => sub {
 subtest_streamed "Live tests versus localhost" => sub {
     plan 'skip_all' => 'AUTHOR_TESTS not set in shell environment, skipping...' if !$ENV{'AUTHOR_TESTS'};
     plan 'skip_all' => 'AUTHOR_TESTS not set in shell environment, skipping...' if !$ENV{'AUTHOR_TESTS'};
     local %Net::OpenSSH::More::cache;
     local %Net::OpenSSH::More::cache;
-    my $obj = Net::OpenSSH::More->new( 'host' => '127.0.0.1' );
+    my $obj = Net::OpenSSH::More->new( 'host' => '127.0.0.1', 'no_cache' => 1 );
     is( ref $obj, 'Net::OpenSSH::More', "Got right ref type for object upon instantiation (using IP)" );
     is( ref $obj, 'Net::OpenSSH::More', "Got right ref type for object upon instantiation (using IP)" );
-    undef $obj;
-    %Net::OpenSSH::More::cache = ();
-    $obj = Net::OpenSSH::More->new( 'host' => 'localhost', 'output_prefix' => '# ' );
+    $obj = Net::OpenSSH::More->new(
+        'host' => 'localhost', 'output_prefix' => '# ', 'use_persistent_shell' => 0, 'expect_timeout' => 1,
+    );
     is( ref $obj, 'Net::OpenSSH::More', "Got right ref type for object upon instantiation (using localhost)" );
     is( ref $obj, 'Net::OpenSSH::More', "Got right ref type for object upon instantiation (using localhost)" );
-    my @cmd_ret = $obj->cmd( { 'no_persist' => 1 }, 'echo "whee"' );
+    my @cmd_ret = $obj->cmd( qw{echo whee} );
     is( \@cmd_ret, [ "whee", '', 0 ], "Got expected return (non-persistent shell)" );
     is( \@cmd_ret, [ "whee", '', 0 ], "Got expected return (non-persistent shell)" );
-    @cmd_ret = $obj->cmd( '/bin/true' );
-    is( \@cmd_ret, [ '', '', 0 ], "Got expected return (persistent shell)" );
+    $obj->use_persistent_shell(1);
+    @cmd_ret = $obj->cmd( qw{echo widdly} );
+    is( \@cmd_ret, [ 'widdly', '', 0 ], "Got expected return (persistent shell)" );
 };
 };
 
 
 # Mock based testing
 # Mock based testing