App::HistHub - Sync shell history between multiple PC.


App-HistHub documentation Contained in the App-HistHub distribution.

Index


Code Index:

NAME

Top

App::HistHub - Sync shell history between multiple PC.

SYNOPSIS

Top

    use App::HistHub;

    my $hh = App::HistHub->new(
        hist_file    => 'path to your history file',
        api_endpoint => 'http://localhost:3000/',
    );
    $hh->run;

DESCRIPTION

Top

App::HistHub is an application that enables you to sync shell history between multiple computers.

This application consists of two modules: client module (histhubd.pl) and server module (histhub_server.pl).

You need one histhub server. To bootup the server, type following command:

    histhub_server

This server receive updating history data from one client, and broadcast to others.

You also need one client daemon in each computer that you want to share history. To boot client, type following command:

    histhubd --histfile=/path/to/your_history_file --server=http://your_server_address

This client send updated history to server, and receive new history from other clients.

METHODS

Top

new

    my $hh = App::HistHub->new( %options );

Create HistHub object.

Available obtions are:

hist_file

History file path to watch update

api_endpoint

Update API URL.

spawn

Create POE session and return session object.

run

Spawn and start POE::Kernel

uri_for

    $hh->uri_for( $path )

Build api url

append_history

    $hh->append_history( $session, $api_response );

Update history file

POE METHODS

Top

poe__start

poe_init

poe_hist_line

poe_hist_rollover

poe_set_poll

poe_poll

AUTHOR

Top

Daisuke Murase <typester@cpan.org>

COPYRIGHT

Top


App-HistHub documentation Contained in the App-HistHub distribution.
package App::HistHub;
use Moose;

our $VERSION = '0.01';

use POE qw/
    Wheel::FollowTail
    Component::Client::HTTPDeferred
    /;

use JSON::XS ();
use HTTP::Request::Common;
use Fcntl ':flock';

has hist_file => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

has tailor => (
    is  => 'rw',
    isa => 'POE::Wheel::FollowTail',
);

has ua => (
    is      => 'rw',
    isa     => 'POE::Component::Client::HTTPDeferred',
    lazy    => 1,
    default => sub {
        POE::Component::Client::HTTPDeferred->new;
    },
);

has json_driver => (
    is      => 'rw',
    isa     => 'JSON::XS',
    lazy    => 1,
    default => sub {
        JSON::XS->new->latin1;
    },
);

has poll_delay => (
    is      => 'rw',
    isa     => 'Int',
    default => sub { 5 },
);

has update_queue => (
    is      => 'rw',
    isa     => 'ArrayRef',
    default => sub { [] },
);

has api_endpoint => (
    is       => 'rw',
    isa      => 'Str',
    required => 1,
);

has api_uid => (
    is  => 'rw',
    isa => 'Str',
);

sub spawn {
    my $self = shift;

    POE::Session->create(
        object_states => [
            $self => {
                map { $_ => "poe_$_" } qw/_start init poll set_poll hist_line hist_rollover/
            },
        ],
    );
}

sub run {
    my $self = shift;
    $self->spawn;
    POE::Kernel->run;
}

sub uri_for {
    my ($self, $path) = @_;

    (my $url = $self->api_endpoint) =~ s!/+$!!;
    $url . $path;
}

sub append_history {
    my ($self, $session, $data) = @_;

    my $json = $self->json_driver->decode($data);
    if ($json->{error}) {
        warn 'api poll error: '. $json->{error};
    }
    elsif ($json->{result}) {
        $self->{tailer} = undef;

        open my $fh, '>>', $self->hist_file;

        flock($fh, LOCK_EX);
        seek($fh, 0, 2);

        print $fh $json->{result};

        flock($fh, LOCK_UN);
        close $fh;

        $poe_kernel->post( $session->ID => 'init' );
    }
}

sub poe__start {
    my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];

    my $d = $self->ua->request( GET $self->api_endpoint . '/api/init' );
    $d->addCallback(sub {
        my $res = shift;
        my $json = $self->json_driver->decode($res->content);

        if ($json->{error}) {
            die 'api response error: ' . $json->{error};
        }
        else {
            $self->api_uid( $json->{result}{uid} );
            $kernel->post( $session->ID, 'init' );
        }
    });
    $d->addErrback(sub {
        my $res = shift;
        die 'api response error: ', $res->status_line;
    });
}

sub poe_init {
    my ($self, $kernel) = @_[OBJECT, KERNEL];

    my $tailor = POE::Wheel::FollowTail->new(
        Filename   => $self->hist_file,
        InputEvent => 'hist_line',
        ResetEvent => 'hist_rollover',
    );
    $self->tailor( $tailor );

    $kernel->yield('set_poll');
}

sub poe_hist_line {
    my ($self, $kernel, $line) = @_[OBJECT, KERNEL, ARG0];

    push @{ $self->update_queue }, $line;
    $kernel->yield('set_poll');
}

sub poe_hist_rollover {
    my ($self, $kernel) = @_[OBJECT, KERNEL];
    
}

sub poe_set_poll {
    my ($self, $kernel) = @_[OBJECT, KERNEL];
    $kernel->delay( poll => $self->poll_delay );
}

sub poe_poll {
    my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];

    $kernel->yield('set_poll');

    my $d = $self->ua->request(
        POST $self->uri_for('/api/poll'),
        [ uid => $self->api_uid, data => join '', @{ $self->update_queue } ]
    );
    $self->update_queue([]);

    $d->addCallback(sub { $self->append_history($session, shift->content) });
    $d->addErrback(sub { warn 'api poll error: ' . shift->status_line });
    $d->addBoth(sub { $kernel->post($session->ID => 'set_poll') });
}

1;