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.004';
3 3     3   167977 use Mojo::Base 'MCP::Run', -signatures;
  3         18007  
  3         14  
4              
5             # ABSTRACT: MCP server that executes commands via bash
6              
7              
8 3     3   1898 use IPC::Open3;
  3         7439  
  3         154  
9 3     3   1176 use IO::Select;
  3         5121  
  3         156  
10 3     3   21 use Symbol 'gensym';
  3         7  
  3         121  
11 3     3   37 use POSIX ':sys_wait_h';
  3         3  
  3         24  
12              
13 9     9 1 26 sub execute ($self, $command, $working_directory, $timeout) {
  9         12  
  9         14  
  9         10  
  9         12  
  9         8  
14 9         13 my $full_command = $command;
15 9 100 66     28 if (defined $working_directory && length $working_directory) {
16 2         8 my $escaped = $working_directory;
17 2         16 $escaped =~ s/'/'\\''/g;
18 2         27 $full_command = "cd '$escaped' && $command";
19             }
20              
21 9         20 my ($stdout, $stderr) = ('', '');
22 9         13 my ($exit_code, $error);
23              
24 9         10 eval {
25 9         79 my $err = gensym;
26 9         202 my $pid = open3(my $in, my $out, $err, 'bash', '-c', $full_command);
27 9         63478 close $in;
28              
29 9         186 my $select = IO::Select->new($out, $err);
30 9         955 my $timed_out = 0;
31              
32 9     1   372 local $SIG{ALRM} = sub { $timed_out = 1; die "alarm\n" };
  1         1000196  
  1         19  
33 9         57 alarm($timeout);
34              
35 9         31 eval {
36 9         96 while (my @ready = $select->can_read) {
37 16         13782 for my $fh (@ready) {
38 23         65 my $buf;
39 23         191 my $bytes = sysread($fh, $buf, 4096);
40 23 100       71 if (!$bytes) {
41 16         71 $select->remove($fh);
42 16         503 next;
43             }
44 7 100       48 if ($fh == $out) { $stdout .= $buf }
  6         52  
45 1         12 else { $stderr .= $buf }
46             }
47             }
48             };
49              
50 9         92 alarm(0);
51              
52 9 100       33 if ($timed_out) {
53 1         42 kill 'TERM', $pid;
54 1         401 waitpid($pid, 0);
55 1         10 $exit_code = 124;
56 1         10 $error = "Command timed out after ${timeout}s";
57             }
58             else {
59 8         121 waitpid($pid, 0);
60 8         56 $exit_code = $? >> 8;
61             }
62              
63 9         143 close $out;
64 9         329 close $err;
65             };
66              
67 9 50 33     69 if ($@ && !defined $exit_code) {
68 0         0 $exit_code = 1;
69 0         0 $error = "$@";
70 0         0 chomp $error;
71             }
72              
73 9         24 chomp $stdout;
74 9         20 chomp $stderr;
75              
76 9 100       181 return { exit_code => $exit_code, stdout => $stdout, stderr => $stderr, (defined $error ? (error => $error) : ()) };
77             }
78              
79              
80              
81             1;
82              
83             __END__