File Coverage

blib/lib/Plack/Middleware/DBGp.pm
Criterion Covered Total %
statement 12 94 12.7
branch 0 50 0.0
condition 0 24 0.0
subroutine 4 10 40.0
pod 0 3 0.0
total 16 181 8.8


line stmt bran cond sub pod time code
1             package Plack::Middleware::DBGp;
2              
3             =head1 NAME
4              
5             Plack::Middleware::DBGp - interactive debugging for Plack applications
6              
7             =head1 SYNOPSIS
8              
9             # should be the first/one of the first modules to be loaded
10             use Plack::Middleware::DBGp (
11             remote_host => "localhost:9000",
12             );
13             use Plack::Builder;
14              
15             builder {
16             enable "DBGp";
17             $app;
18             };
19              
20             =head1 DESCRIPTION
21              
22             Add interactive debugging support via the
23             L to L applications.
24              
25             When debugging, the debugger running inside the application
26             establishes a connection to an external DBGp client (typically a GUI
27             program) that allows to inspect the program state from the outside.
28              
29             C has been tested with
30             This implementation has been tested with L,
31             L
32             and L.
33              
34             =head1 LOADING
35              
36             The Perl debugger needs to be enabled early during compilation,
37             therefore this middleware needs to be loaded explicitly near the top
38             of the main F<.psgi> file of the application. All files loaded before
39             the debugger won't be debuggable (unless L is present and
40             enabled).
41              
42             Supported parameters
43              
44             =over 4
45              
46             =item remote_host
47              
48             use Plack::Middleware::DBGp (
49             remote_host => "host:port",
50             );
51              
52             Hostname/port the debugger should connect to.
53              
54             =item user, client_dir, client_socket
55              
56             use Plack::Middleware::DBGp (
57             user => 'Unix login',
58             client_dir => '/path/to/dir',
59             client_socket => '/path/to/dir/and_socket',
60             );
61              
62             Unix-domain socket the debugger should connect to. The directory must
63             be present, must be owned by the specified user and the group under
64             which the web server is running, and it must not be
65             world-readable/writable.
66              
67             The C and C parameters are optional, and used for
68             extra sanity checks.
69              
70             =item autostart
71              
72             use Plack::Middleware::DBGp (
73             autostart => [0|1],
74             );
75              
76             Whether the debugger should try connect to the debugger client on
77             every request; see also L.
78              
79             =item ide_key
80              
81             use Plack::Middleware::DBGp (
82             ide_key => "DBGp ide key",
83             );
84              
85             The IDE key, as defined by the DBGp protocol. Only used when
86             C is in effect.
87              
88             =item cookie_expiration
89              
90             use Plack::Middleware::DBGp (
91             cookie_expiration => ,
92             );
93              
94             C cookie expiration time, in seconds. See L.
95              
96             =item debug_startup
97              
98             use Plack::Middleware::DBGp (
99             debug_startup => [0|1],
100             );
101              
102             Whether the debugger should try to connect to the debugger client as
103             soon as it is loaded, during application startup.
104              
105             =item log_path
106              
107             use Plack::Middleware::DBGp (
108             log_path => '/path/to/debugger.log',
109             );
110              
111             When set, will write debugging information from the debugger to the
112             sepcified path.
113              
114             =item enbugger
115              
116             use Plack::Middleware::DBGp (
117             enbugger => [0|1],
118             );
119              
120             Use L. At the moment it only enables debugging all files,
121             even the ones loaded before C.
122              
123             =item debug_client_path
124              
125             use Plack::Middleware::DBGp (
126             debug_client_path => '/path/to/dbgp-enabled/debugger',
127             );
128              
129             Use a L installed outside the default module
130             search path.
131              
132             =back
133              
134             =head1 HTTP INTERFACE
135              
136             When C is disabled, C emulates the
137             L
138             session|http://xdebug.org/docs/remote#browser_session> interface.
139              
140             The C GET/POST parameter starts a
141             debugging session and sets the C cookie.
142              
143             When the C cookie is set, the debugger tries to
144             connect to the debugger client passing the sepcified IDE key.
145              
146             The C GET/POST parameter clears the
147             C cookie.
148              
149             =cut
150              
151 1     1   531 use strict;
  1         1  
  1         23  
152 1     1   3 use warnings;
  1         1  
  1         45  
153              
154             our $VERSION = '0.12';
155              
156             use constant {
157 1         92 DEBUG_SINGLE_STEP_ON => 0x20,
158             DEBUG_USE_SUB_ADDRESS => 0x40,
159             DEBUG_REPORT_GOTO => 0x80,
160             DEBUG_ALL => 0x7ff,
161 1     1   9 };
  1         1  
162              
163             use constant {
164 1         856 DEBUG_OFF => 0x0,
165             DEBUG_DEFAULT_FLAGS => # 0x73f
166             DEBUG_ALL & ~(DEBUG_USE_SUB_ADDRESS|DEBUG_REPORT_GOTO),
167             DEBUG_PREPARE_FLAGS => # 0x73c
168             DEBUG_ALL & ~(DEBUG_USE_SUB_ADDRESS|DEBUG_REPORT_GOTO|DEBUG_SINGLE_STEP_ON),
169 1     1   3 };
  1         2  
170              
171             our @ISA;
172              
173             my ($autostart, $idekey, $cookie_expiration);
174              
175             # Unable to connect to Unix socket: /var/run/dbgp/uwsgi (No such file or directory)
176             # Running program outside the debugger...
177             sub _trap_connection_warnings {
178 0 0   0     return if $_[0] =~ /^Unable to connect to Unix socket: /;
179 0 0         return if $_[0] =~ /^Unable to connect to remote host: /;
180 0 0         return if $_[0] =~ /^Running program outside the debugger/;
181              
182 0           print STDERR $_[0];
183             }
184              
185             sub import {
186 0     0     my ($class, %args) = @_;
187              
188             die "Specify either 'remote_host' or 'client_socket'"
189 0 0 0       unless $args{remote_host} || $args{client_socket};
190              
191 0   0       $args{debug_client_path} //= do {
192 0           require Devel::Debug::DBGp;
193              
194 0           Devel::Debug::DBGp->debugger_path;
195             };
196              
197 0   0       $autostart = $args{autostart} // 1;
198 0           $idekey = $args{ide_key};
199 0   0       $cookie_expiration = $args{cookie_expiration} // 3600;
200              
201             my %options = (
202             Xdebug => 1,
203             KeepRunning => 1,
204             ConnectAtStart => ($args{debug_startup} ? 1 : 0),
205             ( LogFile => $args{log_path} ) x !!$args{log_path},
206 0 0         );
207              
208 0 0         if (!$args{remote_host}) {
209 0           my $error;
210 0           my ($user, $dbgp_client_dir) = @args{qw(user client_dir)};
211 0   0       my $group = getgrnam($)) || (split / /, $))[0];
212              
213 0 0 0       if (!$user || !$dbgp_client_dir) {
    0          
214             # pass through and hope for the best
215             } elsif (-d $dbgp_client_dir) {
216 0           my ($mode, $uid, $gid) = (stat($dbgp_client_dir))[2, 4, 5];
217 0   0       my $user_id = getpwnam($user) || die "Can't retrieve the UID for $user";
218              
219 0 0         $error = sprintf "invalid UID %d, should be %d", $uid, $user_id
220             unless $uid == $user_id;
221 0 0         $error = sprintf "invalid GID %d, should be %d", $gid, $)
222             unless $gid == $);
223 0 0         $error = sprintf "invalid permissions bits %04o, should be 0770", $mode & 0777
224             unless ($mode & 0777) == 0770;
225             } else {
226 0           $error = "directory not found";
227             }
228              
229 0 0         if ($error) {
230 0           print STDERR <<"EOT";
231             There was the following issue with the DBGp client directory '$dbgp_client_dir': $error
232              
233             You can fix it by running:
234             \$ sudo sh -c 'rm -rf $dbgp_client_dir &&
235             mkdir $dbgp_client_dir &&
236             chmod 2770 $dbgp_client_dir &&
237             chown $user:$group $dbgp_client_dir'
238             EOT
239 0           exit 1;
240             }
241              
242 0           $options{RemotePath} = $args{client_socket};
243             } else {
244 0           $options{RemotePort} = $args{remote_host};
245             }
246              
247             $ENV{PERLDB_OPTS} =
248 0           join " ", map +(sprintf "%s=%s", $_, $options{$_}),
249             sort keys %options;
250              
251 0 0         if ($args{enbugger}) {
252 0           require Enbugger;
253              
254 0           Enbugger->VERSION(2.014);
255 0           Enbugger->load_source;
256             }
257              
258 0           my $inc_path = $args{debug_client_path};
259 0 0         unshift @INC, ref $inc_path ? @$inc_path : $inc_path;
260             {
261 0           local $SIG{__WARN__} = \&_trap_connection_warnings;
  0            
262 0           require 'perl5db.pl';
263             }
264              
265 0           $^P = DEBUG_PREPARE_FLAGS;
266              
267 0           require Plack::Middleware;
268 0           require Plack::Request;
269 0           require Plack::Response;
270 0           require Plack::Util;
271              
272 0           @ISA = qw(Plack::Middleware);
273             }
274              
275             sub reopen_dbgp_connection {
276 0     0 0   local $SIG{__WARN__} = \&_trap_connection_warnings;
277 0           DB::connectOrReconnect();
278 0 0         DB::enable() if DB::isConnected();
279             }
280              
281             sub close_dbgp_connection {
282 0     0 0   DB::answerLastContinuationCommand('stopped');
283 0           DB::disconnect();
284 0           DB::disable();
285             # this works around uWSGI bug fixed by
286             # https://github.com/unbit/uwsgi/commit/c6f61719106908b82ba2714fd9d2836fb1c27f22
287 0           $^P = DEBUG_OFF;
288             }
289              
290             sub call {
291 0     0 0   my($self, $env) = @_;
292              
293 0           my ($stop_session, $start_session, $debug_idekey);
294 0 0         if ($autostart) {
295 0 0         $ENV{DBGP_IDEKEY} = $idekey if defined $idekey;
296              
297 0           reopen_dbgp_connection();
298             } else {
299 0           my $req = Plack::Request->new($env);
300 0           my $params = $req->parameters;
301 0           my $cookies = $req->cookies;
302 0           my $debug;
303              
304 0 0         if (exists $params->{XDEBUG_SESSION_STOP}) {
    0          
    0          
305 0           $stop_session = 1;
306             } elsif (exists $params->{XDEBUG_SESSION_START}) {
307 0           $debug_idekey = $params->{XDEBUG_SESSION_START};
308 0           $debug = $start_session = 1;
309             } elsif (exists $cookies->{XDEBUG_SESSION}) {
310 0           $debug_idekey = $cookies->{XDEBUG_SESSION};
311 0           $debug = 1;
312             }
313              
314 0 0         if ($debug) {
315 0           $ENV{DBGP_IDEKEY} = $debug_idekey;
316 0           reopen_dbgp_connection();
317             }
318             }
319              
320 0           my $res = $self->app->($env);
321              
322 0 0 0       if ($start_session || $stop_session) {
323 0           $res = Plack::Response->new(@$res);
324              
325 0 0         if ($start_session) {
    0          
326             $res->cookies->{XDEBUG_SESSION} = {
327 0           value => $debug_idekey,
328             expires => time + $cookie_expiration,
329             };
330             } elsif ($stop_session) {
331             $res->cookies->{XDEBUG_SESSION} = {
332 0           value => undef,
333             expires => time - 24 * 60 * 60,
334             };
335             }
336              
337 0           $res = $res->finalize;
338             }
339              
340             Plack::Util::response_cb($res, sub {
341             return sub {
342             # use $_[0] to try to avoid a copy
343 0 0 0       if (!defined $_[0] && DB::isConnected()) {
344 0           close_dbgp_connection();
345             }
346              
347 0           return $_[0];
348 0     0     };
349 0           });
350             }
351              
352             1;
353              
354             __END__