File Coverage

blib/lib/Hg/Lib/Server.pm
Criterion Covered Total %
statement 24 79 30.3
branch 0 40 0.0
condition 0 9 0.0
subroutine 8 18 44.4
pod n/a
total 32 146 21.9


line stmt bran cond sub pod time code
1             package Hg::Lib::Server;
2              
3 3     3   81552 use 5.10.1;
  3         10  
  3         123  
4              
5 3     3   15 use Carp;
  3         4  
  3         299  
6              
7 3     3   1663 use System::Command;
  3         59979  
  3         23  
8 3     3   2373 use Params::Validate ':all';
  3         31740  
  3         783  
9              
10 3     3   6453 use Moo;
  3         35880  
  3         19  
11 3     3   5816 use MooX::Types::MooseLike::Base qw[ :all ];
  3         25749  
  3         1223  
12              
13 3     3   31 use Try::Tiny;
  3         13  
  3         289  
14              
15 3 0   3   16 use constant forceArray => sub { 'ARRAY' eq ref $_[0] ? $_[0] : [ $_[0] ] };
  3         6  
  3         8876  
  0            
16              
17             with 'MooX::Attributes::Shadow::Role';
18              
19             shadowable_attrs( qw[ hg args path configs encoding env ] );
20              
21             # path to hg executable; allow multiple components
22             has hg => (
23             is => 'ro',
24             default => 'hg',
25             coerce => forceArray,
26             isa => ArrayRef [Str],
27             );
28              
29             # arguments to hg
30             has args => (
31             is => 'ro',
32             coerce => forceArray,
33             isa => ArrayRef [Str],
34             default => sub { [] },
35             );
36              
37             has path => (
38             is => 'ro',
39             predicate => 1,
40             );
41              
42             has configs => (
43             is => 'ro',
44             coerce => forceArray,
45             isa => ArrayRef [Str],
46             default => sub { [] },
47             );
48              
49             # default encoding; set to that returned by the hg hello response
50             has encoding => (
51             is => 'rwp',
52             predicate => 1,
53             clearer => '_clear_encoding',
54             );
55              
56             has env => (
57             is => 'ro',
58             isa => HashRef,
59             default => sub { {} },
60             );
61              
62             # the actual pipe object. the pipe should be lazily created when any
63             # handled method is used. $server->_get_hello *must* be called after
64             # pipe creation; trigger will do that. however, trigger is not called
65             # when default is used, only when attribute is set. so, have
66             # default call $server->open which calls setter.
67              
68             has _pipe => (
69              
70             is => 'rw',
71             init_arg => undef,
72             lazy => 1,
73             predicate => 1,
74             trigger => sub { $_[0]->_get_hello },
75             handles => [qw[ stdin stdout stderr pid close is_terminated ]],
76             default => sub { $_[0]->open },
77             );
78              
79              
80             has connect => (
81              
82             is => 'ro',
83             default => 0,
84              
85             );
86              
87             # constructed command line; does not include environment variables
88             has _cmdline => (
89             is => 'lazy',
90             init_arg => undef,
91             builder => sub {
92              
93             my $self = shift;
94              
95             my @cmd = (
96             @{ $self->hg },
97             qw[ --config ui.interactive=True
98             serve
99             --cmdserver pipe
100             ],
101             );
102              
103             push @cmd, '-R', $self->path if $self->has_path;
104              
105             push @cmd, map { ( '--config' => $_ ) } @{ $self->configs };
106              
107             push @cmd, @{ $self->args };
108              
109             return \@cmd;
110             },
111              
112             );
113              
114              
115             has capabilities => (
116             is => 'rwp',
117             predicate => 1,
118             init_arg => undef,
119             );
120              
121             sub BUILD {
122              
123 0     0     my $self = shift;
124              
125 0 0         $self->open if $self->connect;
126              
127             }
128              
129             sub DEMOLISH {
130              
131 0     0     my $self = shift;
132              
133 0 0         $self->close if $self->_has_pipe;
134              
135             }
136              
137             sub open {
138              
139 0     0     my $self = shift;
140              
141 0           my $env = $self->env;
142              
143 0           $env->{HGPLAIN} = 1;
144 0 0         $env->{HGENCODING} = $self->encoding
145             if $self->has_encoding;
146              
147 0           my $pipe
148 0           = System::Command->new( @{ $self->_cmdline }, { env => $self->env } );
149 0           $self->_pipe( $pipe );
150              
151 0           return $pipe;
152             }
153              
154             sub read {
155              
156 0     0     my $self = shift;
157              
158             # use aliased data in @_ to prevent copying
159 0           return $self->stdout->sysread( @_ );
160             }
161              
162             # always use aliased $_[0] as buffer to prevent copying
163             # call as get_chunk( $buf )
164             sub get_chunk {
165              
166 0     0     my $self = shift;
167              
168             # catch pipe errors from child
169 0     0     local $SIG{'PIPE'} = sub { croak( "SIGPIPE on read from server\n" ) };
  0            
170              
171 0           my $nr = $self->read( $_[0], 5 );
172 0 0         croak( "error reading chunk header from server: $!\n" )
173             unless defined $nr;
174              
175 0 0         $nr > 0
176             or croak( "unexpected end-of-file getting chunk header from server\n" );
177              
178 0           my ( $ch, $len ) = unpack( 'A[1] l>', $_[0] );
179              
180 0 0         if ( $ch =~ /IL/ ) {
181              
182 0 0         croak(
183             "get_chunk called incorrectly called in scalar context for channel $ch\n"
184             ) unless wantarray();
185              
186 0           return $ch, $len;
187             }
188              
189             else {
190              
191 0 0         $self->read( $_[0], $len ) == $len
192             or croak(
193             "unexpected end-of-file reading $len bytes from server channel $ch\n"
194             );
195              
196 0           return $ch;
197             }
198              
199             }
200              
201             # call as $self->write( $buf, [ $len ] )
202             sub write {
203              
204 0     0     my $self = shift;
205 0 0         my $len = @_ > 1 ? $_[1] : length( $_[0] );
206 0 0         $self->stdin->syswrite( $_[0], $len ) == $len
207             or croak( "error writing $len bytes to server\n" );
208             }
209              
210             sub writeblock {
211              
212 0     0     my $self = shift;
213              
214 0           $self->write( pack( "N/a*", $_[0] ) );
215             }
216              
217             sub _get_hello {
218              
219 0     0     my $self = shift;
220              
221 0           my $buf;
222 0           my $ch = $self->get_chunk( $buf );
223              
224 0 0 0       croak( "corrupt or incomplete hello message from server\n" )
225             unless $ch eq 'o' && length $buf;
226              
227 0 0         my $requested_encoding = $self->has_encoding ? $self->encoding : undef;
228 0           $self->_clear_encoding;
229              
230 0           for my $item ( split( "\n", $buf ) ) {
231              
232 0           my ( $field, $value ) = $item =~ /([a-z0-9]+):\s*(.*)/;
233              
234 0 0         if ( $field eq 'capabilities' ) {
    0          
235              
236 0           $self->_set_capabilities(
237 0           { map { $_ => 1 } split( ' ', $value ) } );
238             }
239              
240             elsif ( $field eq 'encoding' ) {
241              
242 0 0 0       croak( sprintf "requested encoding of %s; got %s",
243             $requested_encoding, $value )
244             if defined $requested_encoding && $requested_encoding ne $value;
245              
246 0           $self->_set_encoding( $value );
247              
248             }
249              
250             # ignore anything else 'cause we don't know what it means
251              
252             }
253              
254             # make sure hello message meets minimum standards
255 0 0         croak( "server did not provide capabilities?\n" )
256             unless $self->has_capabilities;
257              
258 0 0         croak( "server is missing runcommand capability\n" )
259             unless exists $self->capabilities->{runcommand};
260              
261 0 0         croak( "server did not provide encoding?\n" )
262             unless $self->has_encoding;
263              
264 0           return;
265             }
266              
267             sub getencoding {
268              
269 0     0     my $self = shift;
270              
271 0           $self->write( "getencoding\n" );
272              
273 0           my $buffer;
274 0           my ( $ch, $len ) = $self->get_chunk( $buffer );
275              
276 0 0 0       croak( "unexpected return message for getencoding on channel $ch\n" )
277             unless $ch eq 'r' && length( $buffer );
278              
279 0           return $buffer;
280              
281             }
282              
283             # $server->runcommand( args => [ $command, @args ],
284             # inchannels => \%callbacks,
285             # outchannels => \%callbacks )
286             sub runcommand {
287              
288             my $self = shift;
289              
290             my $opts = validate(
291             @_,
292             {
293             inchannels => {
294             type => HASHREF,
295             default => {}
296             },
297             outchannels => {
298             type => HASHREF,
299             default => {}
300             },
301             args => {
302             type => ARRAYREF,
303             default => {}
304             },
305             } );
306              
307             $self->write( "runcommand\n" );
308             $self->writeblock( join( "\0", @{ $opts->{args} } ) );
309              
310             # read from server until a return channel is specified
311             my $buffer;
312             while ( 1 ) {
313              
314             my ( $ch, $len ) = $self->get_chunk( $buffer );
315              
316             for ( $ch ) {
317              
318             when ( $opts->{inchannels} ) {
319              
320             $self->writeblock( $opts->{inchannels}{$ch}->( $buffer ) );
321             }
322              
323             when ( $opts->{outchannels} ) {
324              
325             $opts->{outchannels}{$ch}->( $buffer );
326             }
327              
328              
329             when ( 'r' ) {
330              
331             state $length_exp = length( pack( 'l>', 0 ) );
332             croak( sprintf "incorrect message length (got %d, expected %d)",
333             length( $buffer ), $length_exp )
334             if length( $buffer ) != $length_exp;
335              
336             return unpack( 'l>', $buffer );
337             }
338              
339             when ( /[[:upper:]]/ ) {
340              
341             croak( "unexpected data on required channel $ch\n" );
342             }
343              
344             }
345              
346              
347             }
348              
349             }
350              
351             1;
352              
353             __END__