parallelise connection setup
[scpubgit/Object-Remote.git] / lib / Object / Remote / Connector / LocalSudo.pm
index c23f2e9..304b3e9 100644 (file)
@@ -1,19 +1,88 @@
 package Object::Remote::Connector::LocalSudo;
 
+use Symbol qw(gensym);
+use Module::Runtime qw(use_module);
+use IPC::Open3;
 use Moo;
 
 extends 'Object::Remote::Connector::Local';
 
-around _perl_command => sub {
-  my ($orig, $self, $target_user) = @_;
-  return 'sudo', '-u', $target_user, $self->$orig($target_user);
+has target_user => (is => 'ro', required => 1);
+
+has password_callback => (is => 'lazy');
+
+sub _build_password_callback {
+  my ($self) = @_;
+  my $pw_prompt = use_module('Object::Remote::Prompt')->can('prompt_pw');
+  my $user = $self->target_user;
+  return sub {
+    $pw_prompt->("sudo password for ${user}", undef, { cache => 1 })
+  }
+}
+
+sub _sudo_perl_command {
+  my ($self) = @_;
+  return
+    'sudo', '-S', '-u', $self->target_user, '-p', "[sudo] password please\n",
+    'perl', '-MPOSIX=dup2',
+            '-e', 'print STDERR "GO\n"; exec(@ARGV);',
+    $self->_perl_command($self->target_user);
+}
+
+sub _start_perl {
+  my $self = shift;
+  my $sudo_stderr = gensym;
+  my $pid = open3(
+    my $foreign_stdin,
+    my $foreign_stdout,
+    $sudo_stderr,
+    $self->_sudo_perl_command(@_)
+  ) or die "open3 failed: $!";
+  chomp(my $line = <$sudo_stderr>);
+  if ($line eq "GO") {
+    # started already, we're good
+  } elsif ($line =~ /\[sudo\]/) {
+    my $cb = $self->password_callback;
+    die "sudo sent ${line} but we have no password callback"
+      unless $cb;
+    print $foreign_stdin $cb->($line, @_), "\n";
+    chomp($line = <$sudo_stderr>);
+    if ($line and $line ne 'GO') {
+      die "sent password and expected newline from sudo, got ${line}";
+    }
+    elsif (not $line) {
+      chomp($line = <$sudo_stderr>);
+      die "sent password but next line was ${line}"
+        unless $line eq "GO";
+    }
+  } else {
+    die "Got inexplicable line ${line} trying to sudo";
+  };
+  Object::Remote->current_loop
+                ->watch_io(
+                    handle => $sudo_stderr,
+                    on_read_ready => sub {
+                      if (sysread($sudo_stderr, my $buf, 1024) > 0) {
+                        print STDERR $buf;
+                      } else {
+                        Object::Remote->current_loop
+                                      ->unwatch_io(
+                                          handle => $sudo_stderr,
+                                          on_read_ready => 1
+                                        );
+                      }
+                    }
+                  );
+  return ($foreign_stdin, $foreign_stdout, $pid);
 };
 
+no warnings 'once';
+
 push @Object::Remote::Connection::Guess, sub {
   for ($_[0]) {
     # username followed by @
     if (defined and !ref and /^ ([^\@]*?) \@ $/x) {
-      return __PACKAGE__->new->connect($1);
+      return __PACKAGE__->new(target_user => $1);
     }
   }
   return;