Group
Extension

Perinci-CmdLine-Server/lib/Perinci/CmdLine/Server.pm

package Perinci::CmdLine::Server;

our $DATE = '2015-09-03'; # DATE
our $VERSION = '0.06'; # VERSION

use 5.010001;
use strict;
use warnings;
#use Log::Any '$log';

use Perinci::CmdLine::Classic;

sub import {
    my $pkg = shift;

    my $caller = caller;

    while (@_) {
        my $arg = shift @_;
        if ($arg =~ /\A(create_cmdline_server)\z/) {
            no strict 'refs';
            *{"$caller\::$arg"} = \&{$arg};
        } elsif ($arg =~ /\A-(.+)\z/) {
            my $name = $1;
            die "Invalid app name $name" unless $name =~ /\A\w+\z/;
            die "$arg requires argument" unless @_;
            my $url = shift;
            create_cmdline_server(name => $name, cmdline_args => {url=>$url});
        } else {
            die "$arg is not imported by ".__PACKAGE__;
        }
    }
}

our %SPEC;

$SPEC{create_cmdline_server} = {
    v => 1.1,
    summary => 'Create Perinci::CmdLine::Classic object and '.
        'some functions to access it in a Perl package',
    description => <<'_',

Currently the functions created are:

    complete_cmdline

_
    args => {
        name => {
            summary => 'Application name',
            description => <<'_',

This function stores the created functions in a hash, keyed by name. If you
create an application with the same name as previously created, the previous
instance will be replaced.

_
            schema  => ['str*', match => '\A\w+\z'],
            req     => 1,
            pos     => 0,
        },
        cmdline => {
            summary => 'A Perinci::CmdLine::Classic object',
            description => <<'_',

If you set this argument, you're passing an existing object. Otherwise, a new
Perinci::CmdLine::Classic object will be created (with `cmdline_args` passed to
its constructor).

Note: it is desirable that your existing Perinci::CmdLine::Classic object has
its attribute exit => 0 :-) Otherwise, it will exit after the first run().

_
            schema => ['obj*'],
        },
        cmdline_args => {
            summary => 'Arguments to be fed to Perinci::CmdLine::Classic constructor',
            description => <<'_',

If you specify this argument and not `cmdline`, a new Perinci::CmdLine::Classic
object will be created.

_
            schema  => [hash => default => {}],
        },
        package => {
            summary => 'Where to put the functions to access the object',
            schema  => 'str*',
            description => <<'_',

The default is `Perinci::CmdLine::Server::app::` + `<name>`. But you can put it
somewhere else. The functions will be installed here.

_
        },
    },
    result_naked => 1,
};
sub create_cmdline_server {
    my %cargs = @_;

    # store created cli's by name
    state $clis = {};

    my $name    = $cargs{name} or die "Please specify CLI app name";
    $name =~ /\A\w+\z/ or die "Invalid name, please use alphanumeric only";
    my $package = $cargs{package} // 'Perinci::CmdLine::Server::app::' . $name;

    my $cli = $cargs{cmdline} // Perinci::CmdLine::Classic->new(
        %{ $cargs{cmdline_args} // {} },
        exit => 0,
    );

    # create the functions to access the CLI
    {
        no strict 'refs';
        ${"$package\::SPEC"}{complete_cmdline} = {
            v => 1.1,
            summary => 'Complete command-line application',
            description => <<'_',

Currently only supports bash.

_
            args => {
                cmdline => {
                    summary => 'Command-line string, usually from COMP_LINE',
                    schema  => 'str*',
                    req     => 1,
                    pos     => 0,
                },
                point => {
                    summary => 'Cursor position in command-line, usually from COMP_POINT',
                    schema  => 'int*',
                    req     => 1,
                    pos     => 1,
                },
            },
            result => {
                schema => 'str*',
            },
        };
        *{"$package\::complete_cmdline"} = sub {
            my %fargs = @_;
            local $ENV{COMP_LINE}  = $fargs{cmdline};
            local $ENV{COMP_POINT} = $fargs{point};
            $cli->run();
        };
    }

    $clis->{$name} = [$cli, $package];

    $cli;
}

1;
# ABSTRACT: Create Perinci::CmdLine::Classic object and some functions to access it in a Perl package

__END__

=pod

=encoding UTF-8

=head1 NAME

Perinci::CmdLine::Server - Create Perinci::CmdLine::Classic object and some functions to access it in a Perl package

=head1 VERSION

This document describes version 0.06 of Perinci::CmdLine::Server (from Perl distribution Perinci-CmdLine-Server), released on 2015-09-03.

=head1 SYNOPSIS

=head2 Running the server

From your L<Perinci::Access::HTTP::Server>-based PSGI application:

 use Perinci::CmdLine::Server qw(create_cmdline_server);
 create_cmdline_server(
     name         => 'app1',
     cmdline_args => {
         url         => '/Some/Module/some_func',
         log_any_app => 0,
     },
 );

Or, shortcut for simple cases:

 use Perinci::CmdLine::Server -app1 => '/Some/Module/some_func';

Or, for testing using L<peri-htserve>:

 % peri-htserve --gepok-unix-sockets /tmp/app1.sock \
     -MPerinci::CmdLine::Server=-app1,/Some/Module/some_func \
     Perinci::CmdLine::Server::app::app1,noload

=head2 Using the server for completion

 # foo-complete
 #!perl
 use HTTP::Tiny::UNIX;
 use JSON;

 my $hres = HTTP::Tiny::UNIX->new->post_form(
    'http:/tmp/app1.sock//api/Perinci/CmdLine/Server/app/app1/complete_cmdline',
    {
        cmdline     => $ENV{COMP_LINE},
        point       => $ENV{COMP_POINT},
        '-riap-fmt' => 'json',
    },
 );
 my $rres = decode_json($hres->{content});
 print $rres->[2];

Activate bash tab completion:

 % chmod +x foo-complete
 % complete -C foo-complete foo
 % foo <Tab>

Now foo will be tab-completed using Rinci specification from C<Some::Module>'s
C<some_func>.

=head1 DESCRIPTION

Currently, L<Perinci::CmdLine::Classic>-based CLI applications have a
perceptible startup overhead (between 0.15-0.35s or even more, depending on your
hardware, those numbers are for 2011-2013 PC/laptop hardware). Some of the cause
of the overhead is subroutine wrapping (see L<Perinci::Sub::Wrapper>) which also
involves compilation of L<Sah> schemas (see L<Data::Sah>), all of which are
necessary for the convenience of using L<Rinci> metadata to specify aspects of
your functions.

This level of overhead is a bit annoying when we are doing shell tab completion
(Perinci::CmdLine::Classic-based applications call themselves for doing tab
completion, e.g. through bash's C<complete -C progname progname> mechanism).
Ideally, tab completion should take no longer than 0.05-0.1s to feel
instantaneous.

One (temporary?) solution to this annoyance is to start a daemon that listens to
L<Riap> requests (either through Unix domain sockets or TCP/IP). This way, the
completion external command can just be a lightweight HTTP client which asks the
server for the completion and displays the result to STDOUT for bash (this only
requires, e.g. L<HTTP::Tiny::Unix> + L<Complete::Bash>).

In the future, other functionalities aside from completion can also be
"off-loaded" to the server side to make the CLI program lighter and quicker to
start. This might require a refactoring of Perinci::CmdLine::Classic codebase so
it's more "stateless" and reusable/safer for multiple requests (perhaps will be
made non-OO in the core so it's clear what states are being passed?)

In the future, Perinci::CmdLine::Classic might also be configured to
automatically start a daemon after the first run (and retire/kill the daemon
after being idle for, say, 30 minute or an hour).

=head2 How does it work?

In your L<Perinci::Access::HTTP::Server>-based PSGI application:

 use Perinci::CmdLine::Server qw(create_cmdline_server);
 create_cmdline_server(
     name         => 'app1',
     cmdline_args => {
         url         => '/Some/Module/some_func',
         log_any_app => 0,
     },
 );

This will create an instance of Perinci::CmdLine::Classic object (the
C<cmdline_args> argument will be fed to the constructor). It will also create a
Perl package dynamically (the default is C<Perinci::CmdLine::Server::app::> +
application name specified in C<name> argument). The package will contain
several functions along with their L<Rinci> metadata. The functions can then be
accessed over L<Riap> protocol. So far, the only function available is:
C<complete_cmdline>. You can use it to request command-line completion. The
Perinci::CmdLine::Classic object will persist as long as the process lives. You
can of course start several applications.

=head2 Caveats

Leaving daemons around could give rise to some security and resource-usage
issues. It is ideal in situations where you already have a daemon for other
purposes (for example, in Spanel there is already an API daemon service running;
the command-line client uses this daemon to request for tab completion).

Some code which normally runs on the client-side will now run on the
server-side. For example, the C<custom_completer> and C<custom_arg_completer>
code. You have to make sure that authentication and authorization issues are
handled.

=head1 FUNCTIONS


=head2 create_cmdline_server(%args) -> any

Create Perinci::CmdLine::Classic object and some functions to access it in a Perl package.

Currently the functions created are:

 complete_cmdline

Arguments ('*' denotes required arguments):

=over 4

=item * B<cmdline> => I<obj>

A Perinci::CmdLine::Classic object.

If you set this argument, you're passing an existing object. Otherwise, a new
Perinci::CmdLine::Classic object will be created (with C<cmdline_args> passed to
its constructor).

Note: it is desirable that your existing Perinci::CmdLine::Classic object has
its attribute exit => 0 :-) Otherwise, it will exit after the first run().

=item * B<cmdline_args> => I<hash> (default: {})

Arguments to be fed to Perinci::CmdLine::Classic constructor.

If you specify this argument and not C<cmdline>, a new Perinci::CmdLine::Classic
object will be created.

=item * B<name>* => I<str>

Application name.

This function stores the created functions in a hash, keyed by name. If you
create an application with the same name as previously created, the previous
instance will be replaced.

=item * B<package> => I<str>

Where to put the functions to access the object.

The default is C<Perinci::CmdLine::Server::app::> + C<< E<lt>nameE<gt> >>. But you can put it
somewhere else. The functions will be installed here.

=back

Return value:  (any)

=for Pod::Coverage ^(import)$

=head1 SEE ALSO

L<Perinci::CmdLine::Classic>

L<Perinci::Access::HTTP::Server>

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Perinci-CmdLine-Server>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-Perinci-CmdLine-Server>.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-CmdLine-Server>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2015 by perlancar@cpan.org.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.