package PPM::InstallerClient; use strict; use Socket; use Cwd qw(getcwd); use File::Basename qw(dirname basename); use File::Path qw(mkpath rmtree); use constant FIELD_SEP => "\001"; use constant FIELD_UNDEF => "\002"; use Data::Dumper; $PPM::InstallerClient::VERSION = '3.0'; #============================================================================= # API: #============================================================================= sub init { my ($ppm_port, $inst) = @_; my ($paddr, $proto, $msg); # Set up a temporary socket server and waits for the frontend to connect # to it. # TODO: put this in a big while(1) loop, and keep a list of connected # frontends. That way, we can service multiple front-ends at once, which # prevents multiple instances of the same target from clobbering each # other's changes. $proto = getprotobyname('tcp'); socket(SERVER, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, pack('l', 1)) || die "setsockopt: $!"; bind(SERVER, sockaddr_in($ppm_port, INADDR_ANY)) || die "bind: $!"; listen(SERVER, SOMAXCONN); $paddr = accept(CLIENT, SERVER); my ($port, $iaddr) = sockaddr_in($paddr); my $name = gethostbyaddr($iaddr, AF_INET); select((select(CLIENT), $| = 1)[0]); my $fd = \*CLIENT; close(SERVER); my %tmpdirs; my $cwd = getcwd; # Read commands from the socket: while ($msg = recvmsg($fd)) { # To do: # 1. Decide what action is being requested; # 2. Parse the "packet" into the required arguments; # 3. Invoke the action callback on the $inst interface; and # 4. Respond over the socket. my ($cmd, @args) = decode_record($msg); # Package Operations if ($cmd eq 'QUERY') { my @ppds = $inst->query(@args); my @records = map { encode_record($_) } @ppds; local $" = "\n"; sendmsg($fd, "@records"); } elsif ($cmd eq 'PROPERTIES') { my @fields = $inst->properties(@args); if (@fields) { sendmsg($fd, encode_record(@fields)); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'REMOVE') { my $ret = $inst->remove(@args); if ($ret) { sendmsg($fd, "OK"); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'PRECIOUS') { my @ret = $inst->precious(); sendmsg($fd, encode_record(@ret)); } elsif ($cmd eq 'BUNDLED') { my @ret = $inst->bundled(); sendmsg($fd, encode_record(@ret)); } elsif ($cmd eq 'DEPENDENTS') { my @ret = $inst->dependents(@args); if (@ret == 1 and not defined $ret[0]) { sendmsg($fd, 'NOK'); } elsif (@ret == 0) { sendmsg($fd, encode_record(undef)); } else { sendmsg($fd, encode_record(@ret)); } } # Configuration Operations elsif ($cmd eq 'CONFIG_INFO') { my @ret = $inst->config_info; if (@ret) { my @records = map { encode_record(@$_) } @ret; local $" = "\n"; sendmsg($fd, "@records"); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'CONFIG_KEYS') { my @ret = $inst->config_keys; if (@ret) { my @records = map { encode_record(@$_) } @ret; local $" = "\n"; sendmsg($fd, "@records"); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'CONFIG_GET') { my $ret = $inst->config_get(@args); if ($ret) { sendmsg($fd, $ret); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'CONFIG_SET') { if ($inst->config_set(@args)) { sendmsg($fd, "OK"); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'ERROR_STR') { sendmsg($fd, $inst->error_str); } # Install and remove: the installerlib must substitute its own notion # of the tempdir if it knows it exists: elsif ($cmd eq 'INSTALL') { # The following line is for reference: # my ($pkg, $ppmpath, $ppd, $repos, $ppmpath) = @args; $args[1] = $tmpdirs{$args[0]} if exists $tmpdirs{$args[0]}; my $ret = $inst->install(@args); if ($ret) { sendmsg($fd, "OK"); } else { sendmsg($fd, "NOK"); } } elsif ($cmd eq 'UPGRADE') { # The following line is for reference: # my ($pkg, $ppmpath, $ppd, $repos, $ppmpath) = @args; $args[1] = $tmpdirs{$args[0]} if exists $tmpdirs{$args[0]}; my $ret = $inst->upgrade(@args); if ($ret) { sendmsg($fd, "OK"); } else { sendmsg($fd, "NOK"); } } # Transmission of files via the network elsif ($cmd eq 'PKGINIT') { my $pkg = shift @args; my $tmpdir = $inst->config_get("tempdir"); unless ($tmpdir and -w $tmpdir) { sendmsg($fd, encode_record('NOK', "Backend tempdir '$tmpdir' not writeable")); next; } $tmpdir .= "/$pkg-$$"; mkpath($tmpdir); $tmpdirs{$pkg} = $tmpdir; sendmsg($fd, 'OK'); } elsif ($cmd eq 'PKGFINI') { my $pkg = shift @args; my $path = $tmpdirs{$pkg} or do { sendmsg($fd, encode_record('NOK', 'pkgfini() without pkginit()')); next; }; rmtree($path); delete $tmpdirs{$pkg}; sendmsg($fd, 'OK'); } elsif ($cmd eq 'TRANSMIT') { my $pkg = shift @args; my $tmpdir = $tmpdirs{$pkg}; my $file = shift @args; my $dir = dirname($file); chdir($tmpdir); mkpath($dir); eval { open(FILE, "> $file") || die "can't write $file: $!"; binmode(FILE) || die "can't binmode $file: $!"; }; if ($@) { sendmsg($fd, encode_record('NOK', "$@")); next; } sendmsg($fd, 'OK'); my $msg; while ($msg = recvmsg($fd)) { my ($flag, $data) = decode_record($msg); last if $flag eq 'EOT'; print FILE $data; } eval { close(FILE) || die "can't close $file: $!"; }; if ($@) { sendmsg($fd, encode_record('NOK', "$@")); next; } sendmsg($fd, 'OK'); chdir($cwd); } elsif ($cmd eq 'STOP') { close(CLIENT); last; } else { die "Unrecognized command: $cmd"; } } } #============================================================================= # Private functions! #============================================================================= my $EOL = "\015\012"; sub sendmsg { my $fd = shift; my $msg = shift; local $\ = "$EOL.$EOL"; print $fd $msg; } sub recvmsg { my $fd = shift; local $/ = "$EOL.$EOL"; my $msg = <$fd>; chomp $msg if $msg; return $msg; } sub qmeta { local $_ = shift || $_; s{([^A-Za-z0-9])}{sprintf('\x%.2X',ord($1))}eg; $_; } sub uqmeta { local $_ = shift || $_; eval qq{qq{$_}}; } sub encode_record { my @fields = map { my $a = defined $_ ? $_ : FIELD_UNDEF; qmeta($a) } @_; join FIELD_SEP, @fields; } sub decode_record { my $t = shift || $_; return map { $_ = &uqmeta; $_ = undef if $_ eq FIELD_UNDEF; $_ } split(FIELD_SEP, $t, -1); }