File Coverage

blib/lib/App/HistHub.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             package App::HistHub;
2 1     1   684 use Moose;
  0            
  0            
3              
4             our $VERSION = '0.01';
5              
6             use POE qw/
7             Wheel::FollowTail
8             Component::Client::HTTPDeferred
9             /;
10              
11             use JSON::XS ();
12             use HTTP::Request::Common;
13             use Fcntl ':flock';
14              
15             has hist_file => (
16             is => 'rw',
17             isa => 'Str',
18             required => 1,
19             );
20              
21             has tailor => (
22             is => 'rw',
23             isa => 'POE::Wheel::FollowTail',
24             );
25              
26             has ua => (
27             is => 'rw',
28             isa => 'POE::Component::Client::HTTPDeferred',
29             lazy => 1,
30             default => sub {
31             POE::Component::Client::HTTPDeferred->new;
32             },
33             );
34              
35             has json_driver => (
36             is => 'rw',
37             isa => 'JSON::XS',
38             lazy => 1,
39             default => sub {
40             JSON::XS->new->latin1;
41             },
42             );
43              
44             has poll_delay => (
45             is => 'rw',
46             isa => 'Int',
47             default => sub { 5 },
48             );
49              
50             has update_queue => (
51             is => 'rw',
52             isa => 'ArrayRef',
53             default => sub { [] },
54             );
55              
56             has api_endpoint => (
57             is => 'rw',
58             isa => 'Str',
59             required => 1,
60             );
61              
62             has api_uid => (
63             is => 'rw',
64             isa => 'Str',
65             );
66              
67             =head1 NAME
68              
69             App::HistHub - Sync shell history between multiple PC.
70              
71             =head1 SYNOPSIS
72              
73             use App::HistHub;
74            
75             my $hh = App::HistHub->new(
76             hist_file => 'path to your history file',
77             api_endpoint => 'http://localhost:3000/',
78             );
79             $hh->run;
80              
81             =head1 DESCRIPTION
82              
83             App::HistHub is an application that enables you to sync shell history between multiple computers.
84              
85             This application consists of two modules: client module (histhubd.pl) and server module (histhub_server.pl).
86              
87             You need one histhub server. To bootup the server, type following command:
88              
89             histhub_server
90              
91             This server receive updating history data from one client, and broadcast to others.
92              
93             You also need one client daemon in each computer that you want to share history. To boot client, type following command:
94              
95             histhubd --histfile=/path/to/your_history_file --server=http://your_server_address
96              
97             This client send updated history to server, and receive new history from other clients.
98              
99             =head1 METHODS
100              
101             =head2 new
102              
103             my $hh = App::HistHub->new( %options );
104              
105             Create HistHub object.
106              
107             Available obtions are:
108              
109             =over 4
110              
111             =item hist_file
112              
113             History file path to watch update
114              
115             =item api_endpoint
116              
117             Update API URL.
118              
119             =back
120              
121             =head2 spawn
122              
123             Create POE session and return session object.
124              
125             =cut
126              
127             sub spawn {
128             my $self = shift;
129              
130             POE::Session->create(
131             object_states => [
132             $self => {
133             map { $_ => "poe_$_" } qw/_start init poll set_poll hist_line hist_rollover/
134             },
135             ],
136             );
137             }
138              
139             =head2 run
140              
141             Spawn and start POE::Kernel
142              
143             =cut
144              
145             sub run {
146             my $self = shift;
147             $self->spawn;
148             POE::Kernel->run;
149             }
150              
151             =head2 uri_for
152              
153             $hh->uri_for( $path )
154              
155             Build api url
156              
157             =cut
158              
159             sub uri_for {
160             my ($self, $path) = @_;
161              
162             (my $url = $self->api_endpoint) =~ s!/+$!!;
163             $url . $path;
164             }
165              
166             =head2 append_history
167              
168             $hh->append_history( $session, $api_response );
169              
170             Update history file
171              
172             =cut
173              
174             sub append_history {
175             my ($self, $session, $data) = @_;
176              
177             my $json = $self->json_driver->decode($data);
178             if ($json->{error}) {
179             warn 'api poll error: '. $json->{error};
180             }
181             elsif ($json->{result}) {
182             $self->{tailer} = undef;
183              
184             open my $fh, '>>', $self->hist_file;
185              
186             flock($fh, LOCK_EX);
187             seek($fh, 0, 2);
188              
189             print $fh $json->{result};
190              
191             flock($fh, LOCK_UN);
192             close $fh;
193              
194             $poe_kernel->post( $session->ID => 'init' );
195             }
196             }
197              
198             =head1 POE METHODS
199              
200             =head2 poe__start
201              
202             =cut
203              
204             sub poe__start {
205             my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];
206              
207             my $d = $self->ua->request( GET $self->api_endpoint . '/api/init' );
208             $d->addCallback(sub {
209             my $res = shift;
210             my $json = $self->json_driver->decode($res->content);
211              
212             if ($json->{error}) {
213             die 'api response error: ' . $json->{error};
214             }
215             else {
216             $self->api_uid( $json->{result}{uid} );
217             $kernel->post( $session->ID, 'init' );
218             }
219             });
220             $d->addErrback(sub {
221             my $res = shift;
222             die 'api response error: ', $res->status_line;
223             });
224             }
225              
226             =head2 poe_init
227              
228             =cut
229              
230             sub poe_init {
231             my ($self, $kernel) = @_[OBJECT, KERNEL];
232              
233             my $tailor = POE::Wheel::FollowTail->new(
234             Filename => $self->hist_file,
235             InputEvent => 'hist_line',
236             ResetEvent => 'hist_rollover',
237             );
238             $self->tailor( $tailor );
239              
240             $kernel->yield('set_poll');
241             }
242              
243             =head2 poe_hist_line
244              
245             =cut
246              
247             sub poe_hist_line {
248             my ($self, $kernel, $line) = @_[OBJECT, KERNEL, ARG0];
249              
250             push @{ $self->update_queue }, $line;
251             $kernel->yield('set_poll');
252             }
253              
254             =head2 poe_hist_rollover
255              
256             =cut
257              
258             sub poe_hist_rollover {
259             my ($self, $kernel) = @_[OBJECT, KERNEL];
260            
261             }
262              
263             =head2 poe_set_poll
264              
265             =cut
266              
267             sub poe_set_poll {
268             my ($self, $kernel) = @_[OBJECT, KERNEL];
269             $kernel->delay( poll => $self->poll_delay );
270             }
271              
272             =head2 poe_poll
273              
274             =cut
275              
276             sub poe_poll {
277             my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];
278              
279             $kernel->yield('set_poll');
280              
281             my $d = $self->ua->request(
282             POST $self->uri_for('/api/poll'),
283             [ uid => $self->api_uid, data => join '', @{ $self->update_queue } ]
284             );
285             $self->update_queue([]);
286              
287             $d->addCallback(sub { $self->append_history($session, shift->content) });
288             $d->addErrback(sub { warn 'api poll error: ' . shift->status_line });
289             $d->addBoth(sub { $kernel->post($session->ID => 'set_poll') });
290             }
291              
292             =head1 AUTHOR
293              
294             Daisuke Murase <typester@cpan.org>
295              
296             =head1 COPYRIGHT
297              
298             This program is free software; you can redistribute
299             it and/or modify it under the same terms as Perl itself.
300              
301             The full text of the license can be found in the
302             LICENSE file included with this module.
303              
304             =cut
305              
306             1;