File Coverage

blib/lib/App/Procapult.pm
Criterion Covered Total %
statement 18 63 28.5
branch 0 8 0.0
condition 0 2 0.0
subroutine 6 15 40.0
pod 0 7 0.0
total 24 95 25.2


line stmt bran cond sub pod time code
1             package App::Procapult;
2              
3 1     1   27177 use strictures 2;
  1         7  
  1         48  
4 1     1   1001 use IO::Socket::UNIX;
  1         28978  
  1         7  
5 1     1   594 use IO::Handle;
  1         11  
  1         36  
6 1     1   806 use String::ShellQuote qw(shell_quote);
  1         855  
  1         59  
7 1     1   970 use Moo;
  1         20483  
  1         7  
8 1     1   2705 use MooX::Options protect_argv => 0, flavour => [ qw(require_order) ];
  1         35737  
  1         7  
9              
10             our $VERSION = '0.009001'; # 0.9.1
11              
12             $VERSION = eval $VERSION;
13              
14             option socket => (
15             is => 'ro',
16             format => 's',
17             required => 1,
18             short => 's',
19             doc => 'unix socket path'
20             );
21              
22             sub run {
23 0     0 0   my ($self) = @_;
24 0 0         if (my $cmd = shift @ARGV) {
25 0           return $self->${\(
26 0   0       $self->can("run_${cmd}")
27             ||die "Invalid command ${cmd}: must be (start|stop|run|die|status|watch)\n"
28             )}(@ARGV);
29             }
30 0           require Proc::Apult;
31 0           return Proc::Apult->new(socket_path => $self->socket)->run;
32             }
33              
34             sub run_start {
35 0     0 0   my ($self, @args) = @_;
36 0           my $sock = $self->_connect_discard;
37 0           print $sock join(' ', start => shell_quote @args)."\n";
38 0           my $line = <$sock>;
39 0           print $line;
40             }
41              
42             sub run_stop {
43 0     0 0   my ($self) = @_;
44 0           my $sock = $self->_connect_discard;
45 0           print $sock "stop\n";
46 0           my $line = <$sock>;
47 0           print $line;
48             }
49              
50             sub run_die {
51 0     0 0   print { $_[0]->_connect_discard } "die\n";
  0            
52             }
53              
54             sub run_status {
55 0     0 0   my ($self) = @_;
56 0           my $sock = $self->_connect;
57 0           my $line = <$sock>;
58 0           print $line;
59             }
60              
61             sub run_watch {
62 0     0 0   my ($self) = @_;
63 0           my $sock = $self->_connect;
64 0           STDOUT->autoflush(1);
65 0           while (my $line = <$sock>) {
66 0           print $line;
67             }
68             }
69              
70             sub run_run {
71 0     0 0   my ($self, @args) = @_;
72 0           my $sock = $self->_connect_discard;
73 0           print $sock join(' ', start => shell_quote @args)."\n";
74 0           STDOUT->autoflush(1);
75 0           my $first = <$sock>;
76 0           print $first;
77 0 0         return unless $first =~ /^STATUS: started/;
78 0           while (my $line = <$sock>) {
79 0           print $line;
80 0 0         return if $line =~ /^STATUS: stopped/;
81             }
82             }
83              
84             sub _connect {
85 0     0     my ($self) = @_;
86 0 0         my $socket = IO::Socket::UNIX->new(
87             Peer => $self->socket,
88 0           ) or die "Couldn't create ${\$self->socket} - $!\n";
89 0           return $socket;
90             }
91              
92             sub _connect_discard {
93 0     0     my ($self) = @_;
94 0           my $socket = $self->_connect;
95 0           my $discard = <$socket>;
96 0           return $socket;
97             }
98              
99             1;
100              
101             =head1 NAME
102              
103             App::Procapult - Hand cranked process launcher
104              
105             =head1 SYNOPSIS
106              
107             $ procapult -s ./ctrl
108              
109             Then in another shell ...
110              
111             $ socat - ./ctrl
112             STATUS: stopped
113             start sleep 3
114             STATUS: started 31563 sleep 3
115             STATUS: stopped
116             start bash
117             STATUS: started 31585 bash
118              
119             And play with the bash in the first shell until you're bored then
120              
121             stop
122             STATUS: stopped
123             die
124             $
125              
126             and with that, your procapult will expire in a puff of logic.
127              
128             =head1 DESCRIPTION
129              
130             The idea for procapult is to have a process launcher that sits around
131             doing nothing, until you tell it to start something, at which point it
132             runs that until it exits or you tell it to stop it.
133              
134             A procapult can, by design, only run one process at once - it's expected
135             to be started in a screen/tmux/dtach window or an xterm, so the behaviour
136             is as simple as possible.
137              
138             To control your procapult, you make a unix socket connection to the
139             control socket passed when you started it. Multiple clients are permitted
140             at the same time, and if they step on each others' toes that's considered
141             operator error on your part.
142              
143             The protocol for the socket is so simple even I can understand it:
144              
145             =over 4
146              
147             =item * On connect, procapult sends its current status
148              
149             =item * When the status changes, procapult sends the new status
150              
151             =item * Status lines look like one of
152              
153             STATUS: started 12345 some shell process
154             STATUS: stopped
155              
156             where 12345 is the pid of the process procapult is currently running
157              
158             =item * Valid commands are 'start', 'stop' and 'die'
159              
160             =item * 'start some shell process' passes the string 'some shell process'
161             to perl's exec()
162              
163             =item * 'stop' causes procapult to send its process a SIGHUP
164              
165             =item * 'die' causes procapult itself to commit harakiri
166              
167             =item * If your command is malformed or makes no sense, procapult sends
168             an error line
169              
170             =item * Error lines look like
171              
172             ERROR: some description of what went wrong
173              
174             =item * A successful command returns nothing, on the assumption that a status
175             line will be along shortly to tell you what happened
176              
177             =item * That's all, folks.
178              
179             =back
180              
181             =head1 SIGNAL HANDLING
182              
183             procapult traps both INT and QUIT, because it's likely sat at the root of
184             a terminal. So Ctrl-C and Ctrl-\ won't blow it up. If you actually want your
185             procapult to fall down and go boom, you can either send it a SIGTERM, which
186             incidentally is what 'kill 12345' will do anyway, or send it a die -
187              
188             $ echo die | socat - /path/to/procapult/socket
189              
190             =head1 SCRIPTING CLIENT
191              
192             You can also avoid needing to use socat (or your own unix socket logic) by
193             using the built-in client:
194              
195             # sends start, reads one line, prints, exits
196             #
197             $ procapult -s foo start some process name
198             STATUS: started 12345 some process name
199             $
200              
201             # sends stop, reads one line, prints, exits
202             #
203             $ procapult -s foo stop
204             STATUS: stopped
205             $
206              
207             # sends start, reads one line, exits if not started, reads until stop, exits
208             #
209             $ procapult -s foo run sleep 3
210             STATUS: started 12345 sleep 3
211             STATUS: stopped
212             $
213              
214             # sends die to kill the procapult, exits
215             #
216             $ procapult -s foo die
217             $
218              
219             # reads status, prints, exits
220             #
221             $ procapult -s foo status
222             STATUS: stopped
223             $
224              
225             # reads status, prints, repeats until killed
226             #
227             $ procapult -s foo watch
228             STATUS: stopped
229             STATUS: started 12345 sleep 3
230             STATUS: stopped
231             ...
232              
233             =head1 USAGE EXAMPLE
234              
235             The purpose for which this code was originally written was that I tend to
236             run clusters of four xterms locally and connect them to matching server
237             sessions. Which gets boring when my connection's a bit patchy. So what I
238             can now do is -
239              
240             # on the server
241             #
242             $ for i in tl tr bl br; do dtach -c ~/dtach/0$i -z bash; done
243              
244             which starts four dtach sessions running bash (if you don't know dtach,
245             think "screen for grumpy minimalists" and you won't be far wrong). Then on
246             my machine I start my four xterms, and in each one start a procapult -
247              
248             # in different terminals -
249             #
250             $ procapult -s ~/clus0/tl
251             $ procapult -s ~/clus0/tr
252             $ procapult -s ~/clus0/bl
253             $ procapult -s ~/clus0/br
254              
255             and then with that done, I can cause a full (re)connect simply with -
256              
257             $ for i in tl tr bl br; do
258             procapult -s ~/clus0/$i start ssh -t servername dtach -a dtach/0$i;
259             done
260              
261             noting that the -t is required to get a tty allocated even though we're not
262             just letting ssh start a shell, and if any of the four haven't died then
263             you'll just get an error from those, which procapult will duly print out
264             and assume is now your problem. Obviously, if you care about noticing when
265             something falls over, you wanted either 'run' instead of 'start' or to
266             run 'status' or 'watch' as preferred.
267              
268             =head1 SUPPORT
269              
270             While you can, in theory, email me, and I will, in theory, reply at some
271             point, you're far better bugging me on #web-simple on irc.perl.org. I'm
272             'mst' on there, and my client is permanently connected, so while I might
273             not reply until tomorrow if I've already called pubtime I should reply
274             eventually.
275              
276             =head1 AUTHOR
277              
278             mst - Matt S. Trout (cpan:MSTROUT)
279              
280             =head1 CONTRIBUTORS
281              
282             None yet - maybe this software is perfect! (ahahahahahahahahaha)
283              
284             =head1 COPYRIGHT
285              
286             Copyright (c) 2015 the App::Procapult L and L
287             as listed above.
288              
289             =head1 LICENSE
290              
291             This library is free software and may be distributed under the same terms
292             as perl itself.