File Coverage

blib/lib/MCP/Run/Bash.pm
Criterion Covered Total %
statement 63 66 95.4
branch 11 12 91.6
condition 3 6 50.0
subroutine 7 7 100.0
pod 1 1 100.0
total 85 92 92.3


line stmt bran cond sub pod time code
1             package MCP::Run::Bash;
2             our $VERSION = '0.100';
3 3     3   159782 use Mojo::Base 'MCP::Run', -signatures;
  3         16509  
  3         15  
4              
5             # ABSTRACT: MCP server that executes commands via bash
6              
7              
8 3     3   1866 use IPC::Open3;
  3         7031  
  3         142  
9 3     3   1201 use IO::Select;
  3         3800  
  3         155  
10 3     3   15 use Symbol 'gensym';
  3         8  
  3         117  
11 3     3   13 use POSIX ':sys_wait_h';
  3         5  
  3         23  
12              
13 9     9 1 31 sub execute ($self, $command, $working_directory, $timeout) {
  9         12  
  9         20  
  9         13  
  9         12  
  9         11  
14 9         25 my $full_command = $command;
15 9 100 66     37 if (defined $working_directory && length $working_directory) {
16 2         8 my $escaped = $working_directory;
17 2         32 $escaped =~ s/'/'\\''/g;
18 2         7 $full_command = "cd '$escaped' && $command";
19             }
20              
21 9         17 my ($stdout, $stderr) = ('', '');
22 9         13 my ($exit_code, $error);
23              
24 9         12 eval {
25 9         101 my $err = gensym;
26 9         216 my $pid = open3(my $in, my $out, $err, 'bash', '-c', $full_command);
27 9         56580 close $in;
28              
29 9         185 my $select = IO::Select->new($out, $err);
30 9         947 my $timed_out = 0;
31              
32 9     1   343 local $SIG{ALRM} = sub { $timed_out = 1; die "alarm\n" };
  1         1000230  
  1         20  
33 9         54 alarm($timeout);
34              
35 9         22 eval {
36 9         49 while (my @ready = $select->can_read) {
37 16         12550 for my $fh (@ready) {
38 23         92 my $buf;
39 23         184 my $bytes = sysread($fh, $buf, 4096);
40 23 100       62 if (!$bytes) {
41 16         54 $select->remove($fh);
42 16         552 next;
43             }
44 7 100       42 if ($fh == $out) { $stdout .= $buf }
  6         45  
45 1         17 else { $stderr .= $buf }
46             }
47             }
48             };
49              
50 9         90 alarm(0);
51              
52 9 100       27 if ($timed_out) {
53 1         39 kill 'TERM', $pid;
54 1         332 waitpid($pid, 0);
55 1         9 $exit_code = 124;
56 1         7 $error = "Command timed out after ${timeout}s";
57             }
58             else {
59 8         110 waitpid($pid, 0);
60 8         68 $exit_code = $? >> 8;
61             }
62              
63 9         129 close $out;
64 9         292 close $err;
65             };
66              
67 9 50 33     61 if ($@ && !defined $exit_code) {
68 0         0 $exit_code = 1;
69 0         0 $error = "$@";
70 0         0 chomp $error;
71             }
72              
73 9         23 chomp $stdout;
74 9         13 chomp $stderr;
75              
76 9 100       185 return { exit_code => $exit_code, stdout => $stdout, stderr => $stderr, (defined $error ? (error => $error) : ()) };
77             }
78              
79              
80              
81             1;
82              
83             __END__