App-mirai/lib/App/mirai.pm
package App::mirai;
# ABSTRACT: Monitor and debug Future objects
use strict;
use warnings;
our $VERSION = '0.003';
=encoding UTF-8
=head1 NAME
App::mirai - debugging for L<Future>-based code
=head1 VERSION
version 0.003
=head1 SYNOPSIS
# Just trace a single script to STDOUT/STDERR:
mirai-trace script.pl
# Run the Tickit interface, and have it load the script as a separate process, directing
# STDOUT/STDERR to windows in the UI and communicating via pipepair
mirai script.pl
# eventually, the following options may also be added:
# start an IO::Async::Listener on the given port/socket file. Means the event loop needs to
# be running, but should be able to hook into an existing application without too much trouble.
# Some complications around Future nesting (Futures are created by the debugger itself) but
# that's easy enough to work around
perl -d:Mirai=localhost:1234 script.pl
perl -d:Mirai=/tmp/mirai.sock script.pl
# Run Tickit interface directly, presuming that the code itself is silent - everything is
# in-process, so no need for debugging to go via pipes
perl -Mirai script.pl
=head1 DESCRIPTION
Provides a basic debugging interface for tracing and interacting with L<Future>s. This should
allow you to see the L<Future> instances currently in use in a piece of code, and what their
current status is.
The UI is currently L<Tickit>-based.
=begin HTML
<p>Early preview screenshot:</p>
<p><img src="http://tickit.perlsite.co.uk/cpan-screenshot/mirai.png" alt="Mirai Tickit user interface" width="1024" height="550"></p>
=end HTML
There's a web interface in the works as well.
The name "mirai" (未来) was chosen because it's short and somewhat related to the concept
of the code, plus it seemed like a better option than chigiri (契り) at the time.
=cut
=head2 SERIALISATION
Defines the serialisation format to use.
Prefers L<Sereal> if available, will fall back to JSON via L<JSON::MaybeXS>. Set
C< MIRAI_SERIALISATION > in the environment to override:
=over 4
=item * Sereal
=item * JSON
=back
=cut
use constant SERIALISATION => $ENV{MIRAI_SERIALISATION} || (eval { require Sereal } ? 'Sereal' : 'JSON');
use Socket qw(AF_UNIX SOCK_STREAM PF_UNSPEC);
use IO::Handle;
# Everything after this point should happen at
# runtime only. That includes use/BEGIN/CHECK/INIT.
# use XYZ; will cause the module to be loaded in
# the child process, and it'd be nice to avoid that
# to keep the code-under-test as untainted as possible.
my ($child_pid);
# These are named for the entity doing the action, i.e.
# parent_write means parent will be doing the writing,
# child_read => child will read from this var.
my ($child_read, $parent_write);
my ($child_write, $parent_read);
my ($script);
=head1 METHODS
=cut
=head2 fork_child
Starts the child process for running the code-under-test.
=cut
sub fork_child {
my ($self) = @_;
# see perlipc
socketpair $child_read, $parent_write, AF_UNIX, SOCK_STREAM, PF_UNSPEC or die $!;
socketpair $child_write, $parent_read, AF_UNIX, SOCK_STREAM, PF_UNSPEC or die $!;
$child_write->autoflush(1);
$parent_write->autoflush(1);
unless($child_pid = fork // die) {
require App::mirai::Subprocess;
# Wait for permission to start
my $line = <$child_read>;
# $child_read->close or die $!;
my $encoder = SERIALISATION eq 'JSON' ? JSON::MaybeXS->new(utf8 => 1) : Sereal::Encoder->new;
App::mirai::Subprocess->setup(sub {
eval {
$child_write->print(pack 'N/a*', $encoder->encode(\@_));
} or warn $@;
# Single-step mode... not very efficient at all.
my $line = <$child_read>;
});
if(!defined(do $script) && $@) {
$child_write->print(
pack 'N/a*', $encoder->encode([
error => {
location => $script,
exception => $@
}
])
);
}
$child_write->close or die $!;
exit 0;
}
} # End of child process section.
sub new_from_argv {
my ($class, @args) = @_;
# see how we don't actually do anything with @ARGV here? that's maybe why we're
# not documented.
bless {}, $class
}
sub run {
my $self = shift;
die "No script provided" unless defined($script = shift @ARGV);
$self->fork_child;
# Don't pollute the child process with any of this. Normally I'm not a fan of
# late-loading like this, and if I was writing this properly I'd separate most of
# this out to a separate module. Yeah, that's an idea - let's do that next time.
require File::HomeDir;
require File::ShareDir;
require Mixin::Event::Dispatch::Bus;
require App::mirai::FutureProxy;
require App::mirai::Tickit;
my $tickit = App::mirai::Tickit->new(
bus => $self->bus,
script => $script,
);
my $loop = App::mirai::Tickit::loop();
$loop->add(
my $ps = IO::Async::Stream->new(
write_handle => $parent_write,
on_read => sub {
my ($stream, $buff, $eof) = @_;
warn "read from parent, that's backwards...";
warn "eof on parent" if $eof;
0
}
)
);
$loop->add(
my $cs = IO::Async::Stream->new(
read_handle => $parent_read,
on_read => sub {
my ($stream, $buff, $eof) = @_;
if(length $$buff >= 4) {
my $size = unpack 'N', substr $$buff, 0, 4, '';
# just in case someone tries to use a single socketpair
# for all communications and gets back our starter message
# instead of the encoded data we were expecting >_>
die "Unexpected size is fucked" unless $size < 10485760;
return sub {
my ($stream, $buff, $eof) = @_;
return 0 unless length($$buff) >= $size;
$self->incoming_frame(substr $$buff, 0, $size, '');
$ps->write("ok\n");
undef
}
}
0
}
)
);
$tickit->prepare;
$tickit->watcher_future->on_done(sub {
$ps->write("go\n");
});
$tickit->run;
$parent_write->close or die $!;
waitpid $child_pid, 0;
}
sub decoder { shift->{decoder} ||= SERIALISATION eq 'JSON' ? JSON::MaybeXS->new(utf8 => 1) : Sereal::Decoder->new; }
sub incoming_frame {
my ($self, $frame) = @_;
# Always load this for display anyway
require JSON::MaybeXS;
JSON::MaybeXS->import;
my $data = $self->decoder->decode($frame);
my ($cmd, $args) = @$data;
# warn "Had $cmd => $args\n";
my $f;
if($cmd eq 'create') {
$f = App::mirai::FutureProxy->new(%$args);
App::mirai::FutureProxy->_create($f);
} elsif($cmd eq 'label') {
$f = App::mirai::FutureProxy->_lookup($args->{id}) or die "we have no " . $args->{id};
$f->{$_} = $args->{$_} for keys %$args;
} elsif($cmd eq 'ready') {
$f = App::mirai::FutureProxy->_lookup($args->{id});
$f->{$_} = $args->{$_} for keys %$args;
} elsif($cmd eq 'destroy') {
$f = App::mirai::FutureProxy->_lookup($args->{id});
require Time::HiRes;
$f->{deleted} = Time::HiRes::time();
App::mirai::FutureProxy->_delete($f);
} else {
warn "unknown: $cmd => $args\n"
}
$self->bus->invoke_event($cmd => $f);
}
sub bus { shift->{bus} ||= Mixin::Event::Dispatch::Bus->new }
sub user_path {
shift->{user_path} //= File::HomeDir->my_dist_data(
'App-mirai',
{ create => 1 }
);
}
sub share_path {
shift->{user_path} //= File::ShareDir->my_dist_data(
'App-mirai',
{ create => 1 }
);
}
1;
__END__
=head1 SEE ALSO
=over 4
=item * L<Future>
=back
=head1 AUTHOR
Tom Molesworth <cpan@perlsite.co.uk>
=head1 LICENSE
Copyright Tom Molesworth 2014-2015. Licensed under the same terms as Perl itself.