File Coverage

blib/lib/App/PersistentSSH.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             package App::PersistentSSH;
4              
5 1     1   32226 use MooseX::POE;
  0            
  0            
6             use POE::Wheel::Run;
7              
8             use namespace::clean -except => 'meta';
9              
10             our $VERSION = "0.04";
11              
12             with qw(
13             MooseX::Getopt
14             MooseX::LogDispatch
15             );
16              
17             has host => (
18             isa => "Str",
19             is => "rw",
20             required => 1,
21             );
22              
23             has ssh_verbose => (
24             isa => "Bool",
25             is => "rw",
26             default => 0,
27             );
28              
29             has ssh => (
30             isa => "Str",
31             is => "rw",
32             default => "ssh",
33             );
34              
35             has ssh_master_opts => (
36             isa => 'ArrayRef[Str]',
37             is => "rw",
38             default => sub { [qw(-o ControlMaster=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -N)] },
39             );
40              
41             has ssh_opts => (
42             isa => 'ArrayRef[Str]',
43             is => "rw",
44             default => sub { [] },
45             );
46              
47             has scutil => (
48             isa => "Str",
49             is => "rw",
50             default => "scutil",
51             );
52              
53             has ipconfig => (
54             isa => "Str",
55             is => "rw",
56             default => "ipconfig",
57             );
58              
59             has _stopping_ssh => (
60             isa => "Bool",
61             is => "rw",
62             );
63              
64             has _ssh_wheel => (
65             isa => "POE::Wheel::Run",
66             is => "rw",
67             predicate => "_has_ssh_wheel",
68             clearer => "_clear_ssh_wheel",
69             handles => {
70             _ssh_pid => "PID",
71             _kill_ssh => "kill",
72             },
73             );
74              
75             has _scutil_wheel => (
76             isa => "POE::Wheel::Run",
77             is => "rw",
78             predicate => "_has_scutil_wheel",
79             clearer => "_clear_scutil_wheel",
80             handles => { _scutil_pid => "PID" },
81             );
82              
83             sub START {
84             my ( $self, $kernel ) = @_[OBJECT, KERNEL];
85             $kernel->yield("start_scutil");
86             $kernel->yield("try_spawn");
87             }
88              
89             event network_changed => sub {
90             my ( $self, $kernel ) = @_[OBJECT, KERNEL];
91              
92             $self->logger->info("network state changed");
93              
94             $kernel->yield("try_spawn");
95             };
96              
97             event try_spawn => sub {
98             my ( $self, $kernel ) = @_[OBJECT, KERNEL];
99              
100             if ( $self->is_reachable ) {
101             $kernel->yield("start_ssh");
102             } else {
103             $kernel->yield("stop_ssh");
104             }
105             };
106              
107             sub is_reachable {
108             my ( $self, $host ) = @_;
109              
110             $host ||= $self->host;
111              
112             # wait for the network interfaces to be configured
113             $self->logger->debug("ipconfig wait all");
114             system( $self->ipconfig, "waitall" );
115             $self->logger->info("ipconfig waitall reports interface is configured");
116              
117             # check for reachability
118             my $scutil = $self->scutil;
119             my $out = `$scutil -r $host`;
120             chomp $out;
121              
122             $self->logger->debug("scutil -r $host: $out");
123              
124             if ( $out =~ /^Reachable/ and not $out =~ /Connection (?:Required|Automatic)/ ) {
125             $self->logger->debug("$host reachable");
126             return 1;
127             } else {
128             $self->logger->info("$host not reachable");
129             return 0;
130             }
131             }
132              
133             sub is_running {
134             my $self = shift;
135             return unless $self->_has_ssh_wheel;
136             kill 0 => $self->_ssh_pid;
137             }
138              
139             sub create_ssh_args {
140             my $self = shift;
141             return [ ( $self->ssh_verbose ? "-v" : () ), @{ $self->ssh_master_opts }, @{ $self->ssh_opts }, $self->host ];
142             }
143              
144             event spawn_command => sub {
145             my ( $self, $kernel, $command, @args ) = @_[OBJECT, KERNEL, ARG0, ARG1 .. $#_];
146              
147             my ( $program, $args ) = (
148             $self->$command,
149             $self->${\"create_${command}_args"},
150             );
151              
152             $self->logger->info("spawning", join(" ", $program, @$args));
153            
154             my $wheel = POE::Wheel::Run->new(
155             Program => $program,
156             ProgramArgs => $args,
157              
158             ( map { ucfirst() . 'Event' => "${command}_$_" } qw(
159             stdin
160             stdout
161             stderr
162             error
163             close
164             )),
165              
166             ( map { $_ . Filter => POE::Filter::Line->new } qw(Stdout Stderr Stdin) ),
167              
168             @args,
169             );
170              
171             $kernel->sig_child( $wheel->PID, "${command}_died" );
172              
173             $self->${\"_${command}_wheel"}($wheel);
174             };
175              
176             event start_ssh => sub {
177             my ( $self, $kernel ) = @_[OBJECT, KERNEL];
178              
179             unless ( $self->is_running ) {
180             $self->call(spawn_command => "ssh");
181             $kernel->sig( INT => "stop_ssh" );
182             }
183             };
184              
185             event ssh_stderr => sub {
186             $_[OBJECT]->logger->warning(@_[ARG0 .. $#_]);
187             };
188              
189             event ssh_died => sub {
190             my $self = $_[OBJECT];
191              
192             $self->_clear_ssh_wheel;
193              
194             if ( $self->_stopping_ssh ) {
195             $self->_stopping_ssh(0);
196             $self->logger->info("ssh stopped");
197             } else {
198             $self->logger->warning("ssh died")
199             }
200              
201             $self->yield("try_spawn");
202             };
203              
204             event stop_ssh => sub {
205             my $self = $_[OBJECT];
206              
207             if ( $self->_has_ssh_wheel ) {
208             $self->_stopping_ssh(1);
209             $self->logger->info("stopping ssh");
210             $self->_kill_ssh;
211             }
212             };
213              
214             sub create_scutil_args {
215             my $self = shift;
216             return [ ];
217             }
218              
219             event start_scutil => sub {
220             my $self = $_[OBJECT];
221              
222             $self->call( spawn_command => "scutil" );
223              
224             $self->_scutil_wheel->put(
225             "n.add State:/Network/Global/IPv4",
226             "n.watch"
227             );
228             };
229              
230             event scutil_died => sub {
231             my $self = $_[OBJECT];
232              
233             $self->logger->warning("scutil died");
234              
235             $self->_clear_scutil_wheel;
236              
237             $self->yield("start_scutil");
238             };
239              
240             event scutil_stderr => sub {
241             my ( $self, $kernel, $output ) = @_[OBJECT, KERNEL, ARG0];
242             $self->logger->debug("scutil err: $output");
243             };
244              
245             event scutil_stdout => sub {
246             my ( $self, $kernel, $output ) = @_[OBJECT, KERNEL, ARG0];
247              
248             if ( $output =~ m{^\s*changed key \[\d+\] = State:/Network/Global/IPv4} ) {
249             $kernel->yield("network_changed");
250             } elsif ( $output !~ m{^\s*notification callback} ) {
251             $self->logger->debug("scutil out: $output");
252             }
253             };
254              
255             sub run {
256             POE::Kernel->run;
257             }
258              
259             __PACKAGE__
260              
261             __END__
262              
263             =pod
264              
265             =head1 NAME
266              
267             App::PersistentSSH - Kick an F<ssh> control master around on OSX using
268             F<scutil>
269              
270             =head1 SYNOPSIS
271              
272             % persisshtent --host your.host.com
273              
274             =head1 DESCRIPTION
275              
276             This POE component will keep an SSH control master alive, depending on network status.
277              
278             It uses the OSX command line tool F<scutil> to get notification on changes to
279             the C<State:/Network/Global/IPv4> configuration key. Whenever this key is changed
280             C<scutil -r> will be used to check if the specified host is directly reachable
281             (without creating a connection using e.g. PPP), and if so spawn F<ssh>.
282              
283             If the host is not reachable, F<ssh> is stopped.
284              
285             =head1 CONFIGURATION
286              
287             Add something alongs the lines of
288              
289             Host *
290             ControlPath /tmp/%r@%h:%p
291              
292             to your F<ssh_config>, in order to configure the path that the F<ssh> control
293             master will bind on. C<ControlMaster auto> is not needed.
294              
295             The advantage over C<ControlMaster auto> is that if you close your initial ssh,
296             which is the control master under C<auto> all subsequently made connections
297             will also close. By keeping a daemonized, managed instance of C<ssh> this
298             problem is avoided.
299              
300              
301             Use C<ssh -v yourhost> to verify that the connection really is going through
302             the control master.
303              
304             You can create a F<launchd> service for this using
305             L<http://lingon.sourceforge.net/>. I use:
306              
307             <key>Disabled</key>
308             <false/>
309             <key>KeepAlive</key>
310             <true/>
311             <key>Label</key>
312             <string>pasta ssh</string>
313             <key>ProgramArguments</key>
314             <array>
315             <string>/usr/local/bin/perl</string>
316             <string>/Users/nothingmuch/Perl/App-PersistentSSH/bin/persisshtent</string>
317             <string>--verbose</string>
318             <string>--host</string>
319             <string>pasta.woobling.org</string>
320             </array>
321              
322             =head1 ATTRIBUTES
323              
324             =over 4
325              
326             =item host
327              
328             The host to connect to. Must be a valid ipaddress/hostname, not just an ssh
329             config host entry.
330              
331             =item ssh_verbose
332              
333             Pass C<-v> to ssh.
334              
335             =item ssh_opts
336              
337             Additional options for ssh, useful for tunnelling etc.
338              
339             =back
340              
341             =head1 METHODS
342              
343             =over 4
344              
345             =item new
346              
347             =item new_with_options
348              
349             Spawn the POE component.
350              
351             C<new_with_options> comes from L<MooseX::Getopt>.
352              
353             =item run
354              
355             Calls L<POE::Kernel/run>.
356              
357             =back
358              
359             =head1 VERSION CONTROL
360              
361             This module is maintained using Darcs. You can get the latest version from
362             L<http://nothingmuch.woobling.org/code>, and use C<darcs send> to commit
363             changes.
364              
365             =head1 AUTHOR
366              
367             Yuval Kogman E<lt>nothingmuch@woobling.orgE<gt>
368              
369             =head1 COPYRIGHT
370              
371             Copyright (c) 2008 Yuval Kogman. All rights reserved
372             This program is free software; you can redistribute
373             it and/or modify it under the same terms as Perl itself.
374              
375             =cut