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