ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/AnyEvent-MCP/lib/AnyEvent/MCP/Helper.pm
Revision: 1.2
Committed: Wed Oct 21 17:28:16 2026 UTC (12 days, 23 hours ago) by root
Branch: MAIN
CVS Tags: rel-0_62, rel-0_61, rel-0_7, rel-0_65, rel-0_6, rel-0_71, HEAD
Changes since 1.1: +6 -0 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     AnyEvent::MCP::Helper - asynchronous JSON exchange with an external helper
4    
5     =head1 SYNOPSIS
6    
7 root 1.2 use AnyEvent::MCP::Helper;
8 root 1.1 my $job = AnyEvent::MCP::Helper->new (
9     helper => ["my-helper", "--option"], timeout => 300,
10     );
11     $job->ask ($input, sub {
12     my ($success, $result) = @_;
13     ...
14     });
15    
16     =head1 DESCRIPTION
17    
18     Runs an executable (or an argument array, never a shell command), writes one
19     UTF-8 JSON value to its stdin and closes that input. A successful helper
20     writes one JSON value to stdout and exits zero. Diagnostics use stderr.
21    
22     C<ask ($input, $done)> calls C<$done-E<gt>($success, $result)> once. On failure,
23     C<$result> is an error string. One exchange may be active per object.
24     C<timeout> defaults to 300 seconds and C<max_size> to 1 MiB of output.
25     C<cancel ($reason)> terminates the helper and completes with failure.
26     The independent child watcher reaps the helper after cancellation too.
27    
28     This module handles processes and bytes, not the meaning of the JSON.
29 root 1.2 It is the transport for local form and permission helpers: an MCP server
30     or multibroker can ask a human without blocking its AnyEvent loop.
31     Ordinary server authors should normally use
32     L<AnyEvent::MCP::Server::Permissions> or the form facilities in
33     L<AnyEvent::MCP::Permissions::Ask>, not launch helpers directly.
34 root 1.1 L<AnyEvent::MCP::Permissions::Ask> provides the permission interface;
35     C<anyevent-mcp-form-elicitation> implements the standard form request/response contract.
36    
37     =cut
38    
39     package AnyEvent::MCP::Helper;
40    
41     use common::sense;
42     use AnyEvent::MCP ();
43     use AnyEvent ();
44     use AnyEvent::Util ();
45     use AnyEvent::Handle ();
46     use JSON::XS ();
47     use Scalar::Util qw(weaken);
48    
49     our @ISA = qw(AnyEvent::MCP::Base);
50    
51     sub ask {
52     my ($self, $prepared, $done) = @_;
53    
54     die "Helper is already active\n" if $self->{on_done};
55     $self->{finished} = 0;
56     $self->{on_done} = $done;
57     delete @$self{qw(status eof)};
58     my $helper = $self->{helper} or die "No helper configured\n";
59     my $input = JSON::XS::encode_json $prepared;
60     my $output = "";
61     my $ok = eval {
62     my ($parent, $child) = AnyEvent::Util::portable_socketpair;
63     die "Cannot create helper socket: $!\n" unless defined $parent && defined $child;
64     $self->{cv} = AnyEvent::Util::run_cmd (
65     ref $helper eq "ARRAY" ? $helper : [$helper],
66     "<" => $child, ">" => $child, '$$' => \$self->{pid}, close_all => 1,
67     );
68     close $child;
69     weaken $self;
70     $self->{handle} = AnyEvent::Handle->new (
71     fh => $parent,
72     on_error => sub { $self->cancel ("Helper socket: $_[2]") if $self },
73     on_read => sub {
74     return unless $self && !$self->{finished};
75     $output .= $_[0]{rbuf};
76     $_[0]{rbuf} = "";
77     $self->cancel ("Helper output exceeds size limit")
78     if length $output > ($self->{max_size} || 1024 * 1024);
79     },
80     on_eof => sub {
81     return unless $self && !$self->{finished};
82     $self->{eof} = 1;
83     $self->_complete ($output);
84     },
85     );
86     $self->{handle}->push_write ($input);
87     $self->{handle}->push_shutdown;
88     $self->{cv}->cb (sub {
89     my $status = $_[0]->recv;
90     return unless $self;
91     delete $self->{pid}; # The child is reaped even if descendants keep stdout open.
92     return if $self->{finished};
93     $self->{status} = $status;
94     $self->_complete ($output);
95     });
96     $self->{timer} = AnyEvent->timer (after => $self->{timeout} || 300, cb => sub {
97     $self->cancel ("Helper timed out") if $self;
98     });
99     1
100     };
101     $self->_finish (0, "$@") unless $ok;
102     }
103    
104     sub _complete {
105     my ($self, $output) = @_;
106    
107     return unless defined $self->{status} && $self->{eof};
108     return $self->_finish (0, "Helper exited with status $self->{status}") if $self->{status};
109     my $decision = eval { JSON::XS::decode_json $output };
110     return $self->_finish (0, "Invalid helper JSON: $@") if $@;
111     $self->_finish (1, $decision);
112     }
113    
114     sub _finish {
115     my ($self, $success, $result) = @_;
116    
117     return if $self->{finished};
118     $self->{finished} = 1;
119     delete @$self{qw(cv timer)};
120     (delete $self->{handle})->destroy if $self->{handle};
121     my $done = delete $self->{on_done};
122     $done->($success, $result) if $done;
123     }
124    
125     sub cancel {
126     my ($self, $reason) = @_;
127    
128     return if $self->{finished};
129     kill TERM => delete $self->{pid} if $self->{pid};
130     $self->_finish (0, defined $reason ? $reason : "Helper cancelled");
131     }
132    
133     sub DESTROY {
134     $_[0]->cancel
135     }
136    
137     1