| File: | blib/lib/App/FileSync/Transport/Local.pm |
| Coverage: | 89.8% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::FileSync::Transport::Local; | |||||
| 2 | ||||||
| 3 | # Runs filesync-server as a child process and speaks the wire protocol over | |||||
| 4 | # its pipes. Used by the test suite, and for a server directory that is | |||||
| 5 | # local to the client (a NAS mount, say). | |||||
| 6 | ||||||
| 7 | 313 313 313 | 841 342 4525 | use App::FileSync::Platform; | |||
| 8 | ||||||
| 9 | 313 313 313 | 591 316 2649 | use strict; | |||
| 10 | 313 313 313 | 481 238 6443 | use warnings; | |||
| 11 | ||||||
| 12 | # Not :all - this class has a close method, and autodie replaces a package's | |||||
| 13 | # subs named after the builtins it wraps. | |||||
| 14 | 313 313 313 | 514 253 700 | use autodie qw(open); | |||
| 15 | ||||||
| 16 | 313 313 313 | 38219 348 7397 | use File::Basename qw(dirname); | |||
| 17 | 313 313 313 | 703 321 3243 | use File::Spec; | |||
| 18 | 313 313 313 | 62340 386997 3538 | use IPC::Open3 (); | |||
| 19 | 313 313 313 | 858 288 7528 | use Readonly; | |||
| 20 | 313 313 313 | 596 268 1019 | use Sub::Private { mode => 'enforce' }; | |||
| 21 | ||||||
| 22 | 313 313 313 | 31490 275 5793 | use App::FileSync::Args qw(named); | |||
| 23 | 313 313 313 | 555 258 13819 | use App::FileSync::Constants qw($ROLE_CLIENT $SERVER_PROGRAM); | |||
| 24 | 313 313 313 | 645 267 4533 | use App::FileSync::I18N qw(throw); | |||
| 25 | ||||||
| 26 | 313 313 313 | 529 305 782 | use version; our $VERSION = version->declare('0.001.1'); | |||
| 27 | ||||||
| 28 | # How far up from this file to look for a checkout's bin/filesync-server: | |||||
| 29 | # lib/App/FileSync/Transport is four levels, blib/lib/... five. | |||||
| 30 | Readonly my $SEARCH_DEPTH => 6; | |||||
| 31 | ||||||
| 32 | =encoding utf8 | |||||
| 33 | ||||||
| 34 - 98 | =head1 NAME
App::FileSync::Transport::Local - run the server as a local child process
=head1 SYNOPSIS
my $transport = App::FileSync::Transport::Local->new(
root => '/srv/filesync', repo => 'docs', client => 'laptop',
);
my $connection = App::FileSync::Protocol->new(in => $transport->reader, out => $transport->writer);
...
$transport->close;
=head1 METHODS
=head2 new
Purpose: start a server for one session.
Args: C<root> (the server root), C<repo>, C<client>, optional C<role>
(default C<client>) and C<server_bin> (default C<FILESYNC_SERVER_BIN>, then a
F<bin/filesync-server> found above this module, then C<filesync-server> on
the PATH).
Returns: the transport.
Side Effects: starts C<$^X server_bin --root ... --repo ... --client ...
--role ...>. The child's standard error is the parent's, never the protocol
pipe: sharing it would put diagnostics into the protocol stream.
Usage:
my $transport = App::FileSync::Transport::Local->new(%args);
=head3 EXAMPLE
my $t = App::FileSync::Transport::Local->new(root => $root, repo => 'docs', client => 'test-1');
print {$t->writer} ...;
=head3 API SPECIFICATION
=head4 INPUT
{
root => { type => 'string', min => 1 },
repo => { type => 'string', min => 1 },
client => { type => 'string', min => 1 },
role => { type => 'string', optional => 1 },
server_bin => { type => 'string', optional => 1 },
}
=head4 OUTPUT
{ type => 'object', isa => 'App::FileSync::Transport::Local' }
=head3 MESSAGES
Key Meaning Resolution
transport.missing root, repo or client absent pass it
=head3 FORMAL SPECIFICATION
new(a) â â child ⢠argv(child) = â¨perl, bin, --root, a.root, --repo, a.repo, --client, a.client, --role, roleâ©
=cut | |||||
| 99 | ||||||
| 100 | sub new { | |||||
| 101 | 164 | 2596 | my $class = shift; | |||
| 102 | my $args = named(\@_, { | |||||
| 103 | 164 820 | 363 1483 | map({ $_ => { type => 'string', optional => 1 } } qw(root repo client role server_bin)), | |||
| 104 | }, 'Transport::Local->new'); | |||||
| 105 | ||||||
| 106 | 164 | 482 | for my $required (qw(root repo client)) { | |||
| 107 | throw('transport.missing', { transport => 'Local', what => $required }) | |||||
| 108 | 492 | 918 | unless defined $args->{$required} && length $args->{$required}; | |||
| 109 | } | |||||
| 110 | ||||||
| 111 | my @command = ( | |||||
| 112 | $^X, $args->{server_bin} || _default_server_bin(), | |||||
| 113 | '--root', $args->{root}, | |||||
| 114 | '--repo', $args->{repo}, | |||||
| 115 | '--client', $args->{client}, | |||||
| 116 | 164 | 552 | '--role', $args->{role} || $ROLE_CLIENT, | |||
| 117 | ); | |||||
| 118 | ||||||
| 119 | # '>&STDERR' dups the parent's stderr. Passing a filehandle here would not | |||||
| 120 | # redirect the child at all: it would write into an unread pipe, and the | |||||
| 121 | # first log line would kill it with SIGPIPE. | |||||
| 122 | 164 | 245 | my ($writer, $reader); | |||
| 123 | 164 | 583 | my $pid = IPC::Open3::open3($writer, $reader, '>&STDERR', @command); | |||
| 124 | ||||||
| 125 | 164 | 474935 | return bless { pid => $pid, writer => $writer, reader => $reader }, $class; | |||
| 126 | } | |||||
| 127 | ||||||
| 128 - 168 | =head2 reader
Documented with L</writer> below.
=head2 writer
Purpose: the pipes to and from the server.
Args: none.
Returns: the read handle (server's stdout) or write handle (its stdin).
Side Effects: none.
Usage:
my $in = $transport->reader;
=head3 EXAMPLE
App::FileSync::Protocol->new(in => $t->reader, out => $t->writer);
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'self', type => 'object' } ]
=head4 OUTPUT
{ type => 'object' }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
reader = self.reader ; writer = self.writer
=cut | |||||
| 169 | ||||||
| 170 | sub reader { | |||||
| 171 | 164 | 490 | my ($self) = @_; | |||
| 172 | ||||||
| 173 | 164 | 760 | return $self->{reader}; | |||
| 174 | } | |||||
| 175 | ||||||
| 176 | sub writer { | |||||
| 177 | 164 | 294 | my ($self) = @_; | |||
| 178 | ||||||
| 179 | 164 | 1626 | return $self->{writer}; | |||
| 180 | } | |||||
| 181 | ||||||
| 182 - 219 | =head2 close
Purpose: end the session's process.
Args: none.
Returns: 1. Repeated calls do nothing.
Side Effects: closes both pipes (the server sees end of file) and reaps the
child.
Usage:
$transport->close;
=head3 EXAMPLE
$transport->close; # safe to call twice
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'self', type => 'object' } ]
=head4 OUTPUT
{ type => 'integer', memberof => [ 1 ] }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
closed' = 1 â§ child â processes'
=cut | |||||
| 220 | ||||||
| 221 | sub close { ## no critic (Subroutines::ProhibitBuiltinHomonyms, Subroutines::ProhibitAmbiguousNames) -- transport interface | |||||
| 222 | 164 | 9770 | my ($self) = @_; | |||
| 223 | ||||||
| 224 | 164 | 428 | return 1 if $self->{closed}++; | |||
| 225 | ||||||
| 226 | # Pipe close failures mean the child already went; reaping still matters. | |||||
| 227 | 164 328 164 | 256 645 300 | for my $handle (grep { defined } @{$self}{qw(writer reader)}) { | |||
| 228 | 328 | 1460 | CORE::close($handle); | |||
| 229 | } | |||||
| 230 | 164 | 219827065 | waitpid($self->{pid}, 0) if $self->{pid}; | |||
| 231 | ||||||
| 232 | 164 | 2174 | return 1; | |||
| 233 | } | |||||
| 234 | ||||||
| 235 - 272 | =head2 error_text
Purpose: what the transport itself said, for when the protocol failed.
Args: none.
Returns: an empty string: the local server writes its diagnostics to this
process's own standard error, which the user already sees.
Side Effects: none.
Usage:
my $text = $transport->error_text;
=head3 EXAMPLE
print $transport->error_text, "\n";
=head3 API SPECIFICATION
=head4 INPUT
[ { name => 'self', type => 'object' } ]
=head4 OUTPUT
{ type => 'string' }
=head3 MESSAGES
None.
=head3 FORMAL SPECIFICATION
error_text = ""
=cut | |||||
| 273 | ||||||
| 274 | sub error_text { | |||||
| 275 | 0 | return ''; | ||||
| 276 | } | |||||
| 277 | ||||||
| 278 | # _default_server_bin | |||||
| 279 | # Purpose: find the server program when none is configured. | |||||
| 280 | # Entry: none. | |||||
| 281 | # Exit: returns FILESYNC_SERVER_BIN, a bin/filesync-server found by | |||||
| 282 | # walking up from this file (a checkout, or blib under | |||||
| 283 | # "make test"), or the bare name (installed, on the PATH). | |||||
| 284 | # Side effects: none. | |||||
| 285 | sub _default_server_bin :Private { | |||||
| 286 | my $configured = $ENV{FILESYNC_SERVER_BIN}; | |||||
| 287 | return $configured if defined $configured && length $configured; | |||||
| 288 | ||||||
| 289 | my $directory = dirname(__FILE__); | |||||
| 290 | for (1 .. $SEARCH_DEPTH) { | |||||
| 291 | my $candidate = File::Spec->catfile($directory, 'bin', $SERVER_PROGRAM); | |||||
| 292 | return $candidate if -f $candidate; | |||||
| 293 | $directory = dirname($directory); | |||||
| 294 | } | |||||
| 295 | ||||||
| 296 | return $SERVER_PROGRAM; | |||||
| 297 | 313 313 313 | 91471 309 4196 | } | |||
| 298 | ||||||
| 299 | 1; | |||||
| 300 | ||||||