File Coverage

blib/lib/CatalystX/Restarter/GTK.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package CatalystX::Restarter::GTK;
2 1     1   947 use v5.008;
  1         3  
  1         46  
3 1     1   546 use Moose;
  0            
  0            
4             use MooseX::Types::Moose qw(Int Str);
5             use Try::Tiny qw(try catch);
6             use POSIX qw(SIGUSR1 SIGUSR2 WNOHANG);
7             use IPC::Semaphore qw();
8             use IPC::SysV qw(S_IRWXU IPC_PRIVATE IPC_CREAT);
9             use Object::Destroyer qw();
10             use Carp qw(croak);
11             use Socket qw(AF_UNIX SOCK_STREAM);
12             use IO::Handle qw();
13             use namespace::autoclean;
14              
15             our $VERSION = '0.08';
16              
17             extends 'Catalyst::Restarter';
18              
19             sub pick_subclass {
20             die "Win32 not supported" if ($^O eq 'MSWin32');
21              
22             return __PACKAGE__;
23             }
24              
25             # stores forked catalyst server's PID
26             has _child => (
27             is => 'rw',
28             isa => Int
29             );
30              
31             # stores forked gtk window process' PID
32             has win_pid => (
33             is => 'rw',
34             isa => Int,
35             );
36              
37             # Port number of catalyst server
38             has port => (
39             is => 'rw',
40             isa => Int,
41             );
42              
43             # name of catalyst application.
44             has application_name => (
45             is => 'rw',
46             isa => Str,
47             );
48              
49             # Socket for communication with window process
50             has parent_sock => (
51             is => 'rw',
52             );
53              
54             # Pipe for retriving error messages from server process
55             has srv_reader => (
56             is => 'rw'
57             );
58              
59             has auto_restart => (
60             is => 'rw',
61             default => 1
62             );
63              
64             has server_watcher => (
65             is => 'rw'
66             );
67              
68             sub start_server_watcher {
69             my $self = shift;
70             my $pid = shift;
71            
72             $self->_child($pid);
73             # Detect server process termination.
74             my $server_watcher = AnyEvent->child(
75             pid => $self->_child,
76             cb => sub {
77             $self->notify_win('stopped');
78             $self->_child(0);
79             }
80             );
81             $self->server_watcher($server_watcher);
82             }
83              
84             sub run_and_watch {
85             my ($self) = @_;
86            
87            
88             my $sem = IPC::Semaphore->new(IPC_PRIVATE, 1, S_IRWXU | IPC_CREAT)
89             or croak "Can not create semaphore $!";
90              
91             my $sentry = Object::Destroyer->new($sem, 'remove');
92              
93             socketpair(my $parent_sock, my $win_sock, AF_UNIX, SOCK_STREAM, 0)
94             or croak "socketpair failed: $!";
95              
96             # Fork GUI process
97             my $pid = fork;
98             croak $! unless defined $pid;
99              
100             if ($pid) {
101             close $win_sock;
102             $parent_sock->autoflush(1);
103              
104             require AnyEvent;
105            
106             $self->win_pid($pid);
107             $self->parent_sock($parent_sock);
108              
109             # Detect window process termination
110             my $child_win = AnyEvent->child(
111             pid => $self->win_pid,
112             cb => sub {
113             $self->win_pid(0);
114             $self->_kill_child;
115             exit;
116             }
117             );
118              
119             # Handle USR1 (Restart signal) from window
120             my $restart_watcher = AnyEvent->signal(
121             signal => SIGUSR1,
122             cb => sub {
123             $self->_kill_child;
124             $self->_fork_and_start;
125             }
126             );
127              
128             if ($self->auto_restart) {
129             my $timer = AnyEvent->timer(
130             after => 1,
131             interval => 1,
132             cb => sub {
133             if (my @events = $self->_watcher->new_events) {
134             $self->_handle_events(@events);
135             }
136             }
137             );
138             }
139              
140             # wait until window process sets up watchers.
141             $sem->op(0, -1, 0);
142             $sentry = undef;
143              
144             $self->_fork_and_start;
145              
146             # Wait for events infinitely.
147             AnyEvent->condvar->recv;
148             }
149             else {
150             $sentry->dismiss;
151             close $parent_sock;
152             $win_sock->autoflush(1);
153              
154             # Use event loop of Gtk2 by loading it first.
155             require Gtk2;
156             Gtk2->init;
157             require AnyEvent::Socket;
158              
159             my $win = WinMonitor->new($self->application_name);
160              
161             $win->set_restart_handler(sub { kill SIGUSR1, getppid; });
162              
163             my ($watcher, $start_timer);
164              
165             # Creates event watcher for checking socket readiness of forked server.
166             $start_timer = sub {
167             $watcher = AnyEvent->timer(
168             after => 1,
169             cb => sub {
170             AnyEvent::Socket::tcp_connect('localhost', $self->port, sub {
171             if (shift) {
172             $watcher = undef;
173             $win->set_status('started');
174             }
175             else {
176             # Restart timer upon failure
177             $watcher = $start_timer->();
178             }
179             });
180             }
181             );
182             };
183              
184             # SIGUSR1 - starting server
185             my $usr1_watcher = AnyEvent->signal(
186             signal => SIGUSR1,
187             cb => sub {
188             $win->clear_msg;
189             $win->set_status('starting');
190             $win_sock->say('1');
191             $start_timer->();
192             }
193             );
194              
195             # SIGUSR2 - Server exited / killed
196             my $usr2_watcher = AnyEvent->signal(
197             signal => SIGUSR2,
198             cb => sub {
199             $win->set_status('stopped');
200             $watcher = undef;
201             $win_sock->say('1');
202             }
203             );
204              
205             my $winsock_watcher = AnyEvent->io(
206             fh => $win_sock,
207             poll => 'r',
208             cb => sub {
209             # Unbuffered read from socket
210             return unless sysread($win_sock, my $msg, 256, 0);
211             $win->append_msg($msg);
212             }
213             );
214             $sem->op(0, 1, 0);
215              
216             main Gtk2;
217             exit(0);
218             }
219             }
220              
221             # Sends server status signal to window process.
222             {
223             my %map = ('starting' => SIGUSR1, 'stopped' => SIGUSR2);
224              
225             sub notify_win {
226             my ($self, $msg) = @_;
227             return unless exists $map{$msg};
228              
229             if ($self->win_pid) {
230             kill $map{$msg}, $self->win_pid;
231             # Wait until signal is handled. This is for synchronizing signals.
232             $self->parent_sock->getline;
233             }
234             }
235             }
236              
237             sub _fork_and_start {
238             my $self = shift;
239              
240             pipe(my $reader, my $writer) or croak "$!";
241              
242             my $sem = IPC::Semaphore->new(IPC_PRIVATE, 1, S_IRWXU | IPC_CREAT)
243             or croak "failed to create semaphore $!";
244             my $sentry = Object::Destroyer->new($sem, 'remove');
245              
246             my $pid = fork;
247             return unless (defined $pid);
248              
249             if($pid) {
250             close $writer;
251              
252             $self->start_server_watcher($pid);
253            
254             # Read console output from forked server and send to win proc
255             $self->srv_reader(AnyEvent->io(
256             fh => $reader,
257             poll => 'r',
258             cb => sub {
259             if (my $bytes = sysread($reader, my $msg, 256, 0)) {
260             syswrite($self->parent_sock, $msg, $bytes);
261             }
262             }
263             ));
264            
265             $self->notify_win('starting');
266             $sentry->dismiss;
267             $sem->op(0, 1, 0);
268             }
269             else {
270             close $reader;
271              
272             $writer->autoflush(1);
273              
274             $sem->op(0, -1, 0);
275             $sentry = undef;
276              
277             open (STDERR, '>&', $writer) or croak "Failed to dup STDERR $!";
278             open (STDOUT, '>&', $writer) or croak "Failed to dup STDOUT $!";
279             STDOUT->autoflush(1);
280              
281             try {
282             $self->start_sub->();
283             }
284             catch {
285             STDERR->print($_);
286             exit 1;
287             };
288             }
289             }
290              
291             sub _kill_child {
292             my $self = shift;
293              
294             if ($self->_child) {
295             kill 'INT', $self->_child;
296             waitpid($self->_child, 0);
297             $self->_child(0);
298             $self->notify_win('stopped');
299             }
300             }
301              
302             __PACKAGE__->meta->make_immutable;
303              
304             no Moose;
305              
306             #--- Class WinMonitor for GUI ---
307              
308             package WinMonitor;
309             use strict;
310             use warnings;
311             use Gtk2;
312             use Glib qw(TRUE FALSE);
313             use Carp;
314              
315             my $path = __FILE__;
316             $path =~ s/[^\/]+$//;
317              
318             my %status_msg = (
319             starting => { msg => 'Starting', color => Gtk2::Gdk::Color->new(0, 0, 0x55 * 257) },
320             started => { msg => 'Started', color => Gtk2::Gdk::Color->new(0, 0x55 * 257, 0) },
321             stopped => { msg => 'Stopped', color => Gtk2::Gdk::Color->new(0x55 * 257, 0, 0) },
322             );
323              
324             $status_msg{$_}->{icon} = $path.$_.'.png' foreach (keys %status_msg);
325              
326             sub new {
327             my ($class, $app_name) = @_;
328              
329             my $obj = {};
330              
331             my $win = Gtk2::Window->new('toplevel');
332              
333             $win->set_title($app_name);
334             $win->set_keep_above(1);
335              
336             $win->set_position('center');
337              
338             my $status = Gtk2::Label->new;
339              
340             my $menu_bar = Gtk2::MenuBar->new;
341             my $view = Gtk2::MenuItem->new('_View');
342             my $mview = Gtk2::Menu->new;
343              
344             my $console = Gtk2::MenuItem->new('Console');
345             $console->signal_connect('activate', sub { $obj->show_msg; });
346              
347             $mview->append($console);
348             $view->set_submenu($mview);
349              
350             my $restart = Gtk2::MenuItem->new('Restart');
351             my $mrestart = Gtk2::Menu->new;
352             $mrestart->append($restart);
353              
354             my $tools = Gtk2::MenuItem->new('_Tools');
355             $tools->set_submenu($mrestart);
356              
357             $menu_bar->append($view);
358             $menu_bar->append($tools);
359             $menu_bar->set_size_request(-1, 22);
360              
361             my $vbox = Gtk2::VBox->new(FALSE, 0);
362             $vbox->pack_start($menu_bar, FALSE, FALSE, 0);
363              
364             my $hbox = Gtk2::HBox->new(TRUE, 0);
365             $hbox->pack_start(Gtk2::Label->new($app_name.' Server'), TRUE, TRUE, 3);
366              
367             $vbox->pack_start($hbox, TRUE, FALSE, 3);
368             $vbox->pack_start($status, TRUE, FALSE, 3);
369              
370             $win->add($vbox);
371              
372             $win->signal_connect(delete_event => sub { Gtk2->main_quit; });
373             $win->signal_connect('window-state-event' => sub {
374             if (shift(@{$_[1]->new_window_state}) eq 'iconified' && $obj->{trayicon}->is_embedded) {
375             $win->hide;
376             }
377             });
378            
379             $win->show_all;
380             my $buffer = Gtk2::TextBuffer->new;
381             #-- Create tray icon and menu
382             my $trayicon = Gtk2::StatusIcon->new_from_file($status_msg{stopped}->{icon});
383             $trayicon->set_visible(TRUE);
384            
385             my $traymenu = Gtk2::Menu->new;
386             my $tray_mconsole = Gtk2::MenuItem->new('View Console');
387             $tray_mconsole->signal_connect('activate' => sub { $console->activate; });
388            
389             my $tray_mrestart = Gtk2::MenuItem->new('Restart');
390             $tray_mrestart->signal_connect('activate' => sub { $restart->activate; });
391            
392             my $mexit = Gtk2::MenuItem->new('Exit');
393             $mexit->signal_connect('activate' => sub { Gtk2->main_quit; });
394            
395             $traymenu->append($tray_mconsole);
396             $traymenu->append($tray_mrestart);
397             $traymenu->append(Gtk2::SeparatorMenuItem->new);
398             $traymenu->append($mexit);
399            
400             $trayicon->signal_connect('popup-menu', sub {
401             my ($ticon, $button, $time) = @_;
402             my ($x, $y, $push) = Gtk2::StatusIcon::position_menu($traymenu, $ticon);
403             $traymenu->show_all;
404             $traymenu->popup(undef, undef, sub {($x, $y,$push)}, undef, $button, $time);
405             });
406            
407             $obj = { %$obj, win => $win, trayicon => $trayicon, msg_buffer => $buffer, app_name => $app_name, lbstatus => $status, bt_restart => $restart, bt_console => $console };
408              
409             bless $obj, $class;
410             }
411              
412             # Updates status message on window
413             sub set_status {
414              
415             my ($self, $st) = @_;
416              
417             my $msg = $status_msg{$st};
418              
419             $self->{lbstatus}->set_text($msg->{msg});
420             $self->{lbstatus}->modify_fg('normal', $msg->{color});
421              
422             $self->{win}->set_title($self->{app_name}.'-'.$msg->{msg});
423             $self->{trayicon}->set_from_file($msg->{icon});
424             $self->{trayicon}->set_tooltip($self->{app_name}.' ('.$msg->{msg}.')');
425              
426             }
427              
428             # Collects console output received into text buffer
429             sub append_msg {
430             my ($self, $msg) = @_;
431             my $buffer = $self->{msg_buffer};
432             $buffer->insert($buffer->get_end_iter, $msg);
433             }
434              
435             sub get_msg_window {
436             my ($self) = @_;
437              
438             my $win = Gtk2::Window->new;
439             $win->set_title($self->{app_name}.' - console output');
440              
441             $win->set_position('center');
442             $win->signal_connect('delete_event' => sub { $win->hide; 1; });
443              
444             my $textview = Gtk2::TextView->new_with_buffer($self->{msg_buffer});
445             $textview->set_editable(FALSE);
446             $textview->set_wrap_mode('word');
447              
448            
449             my $text_desc = Pango::FontDescription->new;
450             $text_desc->set_family('Monospace');
451             $textview->modify_font($text_desc);
452              
453             my $scrolled_win = Gtk2::ScrolledWindow->new;
454             $scrolled_win->add($textview);
455              
456             $win->add($scrolled_win);
457             $win->set_default_size(800, 400);
458             $win->set_size_request(100, 100);
459             return $win;
460             }
461              
462             # Shows collected messages in a new window
463             sub show_msg {
464             my ($self) = @_;
465              
466             unless ($self->{win_msg}) {
467              
468             $self->{win_msg} = $self->get_msg_window;
469             }
470             $self->{win_msg}->show_all;
471             }
472              
473             # Clears text buffer.
474              
475             sub clear_msg {
476              
477             $_[0]->{msg_buffer}->set_text(q{});
478              
479             }
480              
481             sub set_restart_handler {
482             $_[0]->{bt_restart}->signal_connect('activate', $_[1]);
483             }
484             1;
485              
486             =pod
487              
488             =head1 NAME
489              
490             CatalystX::Restarter::GTK - GTK based Catalyst server restarter.
491              
492             =head1 SYNOPSIS
493              
494             Set environment variable CATALYST_RESTARTER to CatalystX::Restarter::GTK. Then start server with -r (auto restart on file changes) option.
495              
496             export CATALYST_RESTARTER=CatalystX::Restarter::GTK
497             perl script/myapp_server -r
498              
499             You can also create a shell script and add a shortcut to panel. This avoids need of starting terminal.
500              
501             #!/bin/bash
502             cd /home/username/myapp/trunk/
503             export CATALYST_RESTARTER=CatalystX::Restarter::GTK
504             perl script/myapp_server.pl -r
505              
506             To use this restarter for specific application only, set appropirate envioronment variable.
507              
508             MYAPP_RESTARTER=CatalystX::Restarter::GTK
509            
510             =head1 DESCRIPTION
511              
512             This module provides GUI interface for controlling Catalyst server and viewing console output generated. It captures both STDOUT and STDERR.
513              
514             It provides tray icon in GNOME notification area and a GTK window on desktop. It is set always on top by default. You can drag window to any screen corner for convenience.
515              
516             Server can be controlled from window as well as tray icon. You can hide window by minimizing it. Tray icon changes according to server status.
517              
518             User can view console output and manually restart server from menu.
519              
520             Whenever any file of project is updated, developer can immediately check server status without switching to console.
521              
522             =head1 NOTES
523              
524             This module extends Catalyst::Restarter and depends on its _watcher and _handle_events.
525              
526             =head1 AUTHOR
527              
528             Dhaval Dhanani L<mailto:dhaval@cpan.org>
529              
530             =head1 LICENCE
531              
532             This library is free software. You can redistribute it and/or modify it under the same terms as Perl itself.
533              
534             =head1 COPYRIGHT
535              
536             This library is copyright (c) 2011 the above named AUTHOR and CONSTRIBUTOR(s).
537              
538             =cut