File Coverage

lib/Remote/Perl/Transport.pm
Criterion Covered Total %
statement 114 118 96.6
branch 23 38 60.5
condition 1 2 50.0
subroutine 15 15 100.0
pod 0 9 0.0
total 153 182 84.0


line stmt bran cond sub pod time code
1 132     132   2040 use v5.36;
  132         709  
2             package Remote::Perl::Transport;
3             our $VERSION = '0.004';
4              
5 132     132   838 use autodie qw(open close);
  132         219  
  132         939  
6 132     132   68310 use IO::Select;
  132         1028  
  132         8982  
7 132     132   82112 use POSIX qw(WNOHANG);
  132         927010  
  132         894  
8              
9             # Wraps a bidirectional pipe to a child process.
10             #
11             # The child's stdin <- we write protocol bytes
12             # The child's stdout -> we read protocol bytes
13             # The child's stderr -> we read for debug/error logging (separate channel)
14             #
15             # All handles are set to binmode (raw bytes, no encoding layers).
16              
17 1522     1522 0 3552 sub new($class, %args) {
  1522         4808  
  1522         3664  
  1522         2875  
18             # cmd => arrayref of command + arguments (no shell expansion)
19             return bless {
20             cmd => $args{cmd},
21 1522         23050 pid => undef,
22             in_fh => undef, # write end -> child stdin
23             out_fh => undef, # read end <- child stdout
24             err_fh => undef, # read end <- child stderr
25             }, $class;
26             }
27              
28 1522     1522 0 3038 sub connect($self) {
  1522         3479  
  1522         3648  
29 1522         36981 $SIG{PIPE} = 'IGNORE';
30              
31             # We fork+exec manually instead of using open3 so we can call setpgrp()
32             # in the child. This puts the transport process (e.g. ssh) in its own
33             # process group, preventing terminal-generated signals (Ctrl-C -> SIGINT)
34             # from reaching it directly. Signals are instead forwarded through the
35             # protocol by the parent's signal handlers.
36 1522 50       102304 pipe(my $child_in_r, my $child_in_w) or die "pipe: $!\n";
37 1522 50       48183 pipe(my $child_out_r, my $child_out_w) or die "pipe: $!\n";
38 1522 50       47719 pipe(my $child_err_r, my $child_err_w) or die "pipe: $!\n";
39              
40 1522   50     4701388 my $pid = fork() // die "fork: $!\n";
41              
42 1522 100       23830 if ($pid == 0) {
43             # Child: new process group so parent's signals don't reach us.
44 99         8980 setpgrp(0, 0);
45              
46 99         10764 close($child_in_w);
47 99         120017 close($child_out_r);
48 99         9614 close($child_err_r);
49              
50 99 50       9682 open(STDIN, '<&', $child_in_r) or die "dup stdin: $!\n";
51 99 50       423524 open(STDOUT, '>&', $child_out_w) or die "dup stdout: $!\n";
52 99 50       13813 open(STDERR, '>&', $child_err_w) or die "dup stderr: $!\n";
53 99         15273 close($_) for $child_in_r, $child_out_w, $child_err_w;
54              
55 99 0       15218 exec(@{ $self->{cmd} }) or die "exec: $!\n";
  99         0  
56             }
57              
58 1423         201012 close($child_in_r);
59 1423         1492570 close($child_out_w);
60 1423         199330 close($child_err_w);
61              
62 1423         85538 binmode($child_in_w);
63 1423         8191 binmode($child_out_r);
64 1423         4399 binmode($child_err_r);
65              
66 1423         10386 $self->{pid} = $pid;
67 1423         12277 $self->{in_fh} = $child_in_w;
68 1423         9183 $self->{out_fh} = $child_out_r;
69 1423         362829 $self->{err_fh} = $child_err_r;
70             }
71              
72             # Write raw bytes to child stdin. Returns number of bytes written.
73 8965     8965 0 20680 sub write_bytes($self, $data) {
  8965         17641  
  8965         25652  
  8965         13522  
74 8965         20249 my $total = length($data);
75 8965         16617 my $written = 0;
76 8965         27881 while ($written < $total) {
77 8965         1068205 my $n = syswrite($self->{in_fh}, $data, $total - $written, $written);
78 8965 50       42461 die "syswrite: $!\n" unless defined $n;
79 8965         29944 $written += $n;
80             }
81 8965         50678 return $written;
82             }
83              
84             # Read up to $len bytes from child stdout.
85             # Returns undef on EOF, dies on error.
86 8187     8187 0 18189 sub read_bytes($self, $len) {
  8187         19567  
  8187         14732  
  8187         12799  
87 8187         11422 my $data;
88 8187         107085 my $n = sysread($self->{out_fh}, $data, $len);
89 8187 50       32180 die "sysread: $!\n" unless defined $n;
90 8187 100       23482 return if $n == 0; # EOF
91 8186         39713 return $data;
92             }
93              
94             # Read up to $len bytes from child stderr (non-blocking).
95             # Returns '' if nothing available, undef on EOF.
96 1     1 0 3 sub read_stderr($self, $len = 4096) {
  1         2  
  1         3  
  1         5  
97 1 50       11 return '' unless $self->stderr_ready(0);
98 1         116 my $data;
99 1         21 my $n = sysread($self->{err_fh}, $data, $len);
100 1 50       7 die "sysread stderr: $!\n" unless defined $n;
101 1 50       4 return if $n == 0;
102 1         6 return $data;
103             }
104              
105             # Return true if child stderr has data ready within $timeout seconds.
106 3     3 0 11 sub stderr_ready($self, $timeout = 0) {
  3         12  
  3         10  
  3         27  
107 3         18 return IO::Select->new($self->{err_fh})->can_read($timeout);
108             }
109              
110             # Expose raw filehandle and pid for callers building their own IO::Select sets.
111 13939     13939 0 22171 sub out_fh($self) { $self->{out_fh} }
  13939         20296  
  13939         18959  
  13939         155286  
112 4     4 0 16 sub pid($self) { $self->{pid} }
  4         8  
  4         8  
  4         32  
113              
114             # Close stdin (signals EOF to child) and wait for it to exit.
115 1422     1422 0 2895 sub disconnect($self) {
  1422         2533  
  1422         2161  
116 1422 50       4227 if ($self->{in_fh}) {
117 1422         11652 close($self->{in_fh});
118 1422         219097 $self->{in_fh} = undef;
119             }
120 1422         16849 $self->_wait_for_child;
121             }
122              
123 1423     1423   2628 sub _wait_for_child($self) {
  1423         2631  
  1423         2220  
124 1423 50       8021 return unless $self->{pid};
125 1423         3076 my $pid = $self->{pid};
126              
127             # Poll for up to 5 seconds, then escalate.
128 1423         4066 my $deadline = time + 5;
129 1423         4674 while (time < $deadline) {
130 5748         279341 my $result = waitpid($pid, WNOHANG);
131 5748 100       22221 if ($result == $pid) {
132 1423         9488 $self->{pid} = undef;
133 1423         21591 return $?;
134             }
135 4325         219145081 select(undef, undef, undef, 0.05); # 50 ms sleep
136             }
137 0         0 kill('TERM', $pid);
138 0         0 sleep 1;
139 0         0 waitpid($pid, WNOHANG);
140 0         0 $self->{pid} = undef;
141             }
142              
143 1400     1400   8570 sub DESTROY($self) {
  1400         6091  
  1400         5766  
144 1400 100       10951 if ($self->{in_fh}) {
145 1         17 close($self->{in_fh});
146 1         157 $self->{in_fh} = undef;
147             }
148 1400 100       155343 $self->_wait_for_child if $self->{pid};
149             }
150              
151             1;
152              
153             __END__