# must be quoted to work correctly with JSON protocol
our $VERSION = "v2023.1"; # DO NOT REMOVE FUCKING v, IT KEEPS PROPER VERSIONING
# to ensure protocol compatibility between the IDE and the debugger, we will use $API_VERSION variable, to be able
# to bump debugger version without necessity to update IDE part.
my $API_VERSION = "2019.1";
package DB;
use 5.008;
use strict;
use PadWalker qw/peek_my peek_our/;
use Encode;
use PerlIO;
#use Carp;
#sub FLAG_REPORT_GOTO() {0x80;}
use constant {
STEP_CONTINUE => 0,
STEP_INTO => 1,
STEP_OVER => 2,
};
use constant {
# see PERLDBf_* constants in perl.h
DEBUG_ALL => 0x7ff,
DEBUG_SINGLE_STEP_ON => 0x20,
DEBUG_USE_SUB_ADDRESS => 0x40,
DEBUG_REPORT_GOTO => 0x80,
};
use constant {
# debugger enabled
DEBUG_DEFAULT_FLAGS => DEBUG_ALL & ~(DEBUG_USE_SUB_ADDRESS|DEBUG_REPORT_GOTO), # 0x73f
# instrument code, but don't call DB::DB (see sub disable for DB::sub)
DEBUG_PREPARE_FLAGS => DEBUG_ALL & ~(DEBUG_USE_SUB_ADDRESS|DEBUG_REPORT_GOTO|DEBUG_SINGLE_STEP_ON), # 0x73c
};
# Each array @{"::_<$filename"} holds the lines of $filename for a file compiled by Perl. The same is also true for evaled
# strings that contain subroutines, or which are currently being executed. The $filename for evaled strings looks like
# (eval 34) .
# Values in this array are magical in numeric context: they compare equal to zero only if the line is not breakable.
#
# # @DB::dbline is an alias for @{"::_<current_file"} , which holds the lines of the currently-selected file (compiled by
# Perl), either explicitly chosen with the debugger's f command, or implicitly by flow of execution.
#
our @dbline = (); # list of lines in currently loaded file
# Each hash %{"::_<$filename"} contains breakpoints and actions keyed by line number. Individual entries (as opposed to
# the whole hash) are settable. Perl only cares about Boolean true here, although the values used by perl5db.pl have the
# form "$break_condition\0$action" .
#
# The same holds for evaluated strings that contain subroutines, or which are currently being executed. The $filename
# for evaled strings looks like (eval 34) .
#
# %DB::dbline is an alias for %{"::_<current_file"} , which contains breakpoints and actions keyed by line number in
# the currently-selected file, either explicitly chosen with the debugger's f command, or implicitly by flow of execution.
# As previously noted, individual entries (as opposed to the whole hash) are settable. Perl only cares about Boolean
# true here, although the values used by perl5db.pl have the form "$break_condition\0$action" .
#
# Actions in current file (keys are line numbers). The values are strings that have the sprintf(3) format
# ("%s\000%s", breakcondition, actioncode) .
our %dbline = (); # actions in current file (keyed by line number)
# Each scalar ${"::_<$filename"} contains "::_<$filename" . This is also the case for evaluated strings that contain
# subroutines, or which are currently being executed. The $filename for evaled strings looks like (eval 34) .
#
our $dbline;
# DB::dump_trace(skip[,count]) skips the specified number of frames and returns a list containing information about the
# calling frames (all of them, if count is missing). Each entry is reference to a hash with keys context (either ., $ ,
# or @ ), sub (subroutine name, or info about eval), args (undef or a reference to an array), file , and line .
#
# these are hardcoded in perl source (some are magical)
# When execution of the program reaches a subroutine call, a call to &DB::sub (args) is made instead, with $DB::sub
# holding the name of the called subroutine. (This doesn't happen if the subroutine was compiled in the DB package.)
our $sub = ''; # Name of current executing subroutine.
# A hash %DB::sub is maintained, whose keys are subroutine names and whose values have the form
# filename:startline-endline . filename has the form (eval 34) for subroutines defined inside evals.
#
# The keys of this hash are the names of all the known subroutines. Each value is an encoded string that has the
# sprintf(3) format ("%s:%d-%d", filename, fromline, toline) .
our %sub = (); # "filename:fromline-toline" for every known sub
# If you set $DB::single to 2, it's equivalent to having just typed the step over command, whereas a value of 1
# means the step into command.
our $single = 0; # single-step flag (set it to 1 to enable stops in BEGIN/use) 1 -into, 2 - over
# Signal flag. Will be set to a true value if a signal was caught. Clients may check for this flag to abort
# time-consuming operations.
our $signal = 0;
# The $DB::trace variable should be set to 1 to simulate having typed the t command.
# This flag is set to true if the API is tracing through subroutine calls.
our $trace = 0; # are we tracing through subroutine calls?
# For example, whenever you call Perl's built-in caller function from the package DB , the arguments that the
# corresponding stack frame was called with are copied to the @DB::args array. These mechanisms are enabled by calling
# Perl with the -d switch. Specifically, the following additional features are enabled (cf. $^P in perlvar):
our @args = (); # arguments of current subroutine or @ARGV array
our @ret = (); # return value of last sub executed in list context
our $ret = ''; # return value of last sub executed in scalar context
# custom values renderers. Consists of two items arrayrefs, with class as a first field, and code text as second field
# every blessed item is checked with this list and if it matches with some renderer - it is used as value in ide.
# e.g. ['Foo::Bar', '$it->as_string`] meaning that any 'Foo::Bar' object will be represented with as_string value
my @renderers = ();
my %_perl_file_id_to_path_map = (); # map of perl file ids without _< => real path detected on loading
my %_paths_to_perl_file_id_map = (); # maps real paths to _<filename
my %_loaded_breakpoints = (); # map of loaded breakpoints, set and not in form: path => line => object
my %_queued_breakpoints_files = (); # map of files with loaded and not set breakpoints
my %_references_cache = (); # cache of soft references from peek_my
my %_source_been_sent = (); # flags that source been sent
my %_file_name_sent = (); # flags that idea been notfied about this file loading
my %_evals_to_templates_map = (); # map of evals to templates or something (see template_handler). Structure: eval => target file
my %_templates_to_evals_map = (); # map of templates to evals or something (see template_handler). Structure: template => [eval1, eval2, ...]
my @glob_slots = qw/SCALAR ARRAY HASH CODE IO FORMAT/;
my $glob_slots = join '|', @glob_slots;
my $_dev_mode = $ENV{CAMELCADEDB_DEV_MODE}; # enable this to get verbose STDERR output from process
my $_debug_log_fh = *STDERR; # debug log fh. If omited, file will be created
my $_debug_log_filename = 'current_debug.log';
my $_debug_sub_handler = 1; # debug entering/leaving subs, works in dev mode
my $_debug_load_handler = 0; # debug modules loading
my $_debug_breakpoints = 0; # debug breakpoints setting
my $_script_charset = 'utf8'; # all sources and strings without utf flag will be encoded from this encoding to the utf
my $_skip_run_stop = 0; # flag for skipping forced stop on run phase
# this enables pausing and breakpoints setting while script is running, gives moderate overhead
my $_enable_noninteractive_mode = 0;
# this enables attemp to set a breakpoint on each leaving/entering sub, gives large overhead, proportional number of breakpoints
my $_enable_compile_time_breakpoints = 0;
my $_debug_socket;
my $_debug_packed_address;
my IO::Select $_debug_socket_select;
my $coder; # JSON::XS coder
my $deparser; # B::Deparse deparser
my $frame_prefix_step = " ";
my $frame_prefix = '';
my $_internal_process = 0;
my @saved; # saved runtime environment
my $current_package;
my $current_file_id;
my $current_line;
my $trace_code_stack_and_frames = 0; # report traces on entering code
my $trace_real_path = 0; # trasing real path transition
my $ready_to_go = 0; # set after debugger been initialized
my $_stack_frames = [ ]; # stack frames
sub _report($;@)
{
return unless $_dev_mode;
my ($message, @sprintf_args) = @_;
chomp $message;
unless ($_debug_log_fh)
{
open $_debug_log_fh, ">", $_debug_log_filename or die "Unable to open debug log $_debug_log_filename $!";
$_debug_log_fh->autoflush( 1 );
}
printf $_debug_log_fh "$frame_prefix$message\n", map {$_ // 'undef'} @sprintf_args;
}
sub _format_caller
{
my (@caller) = @_;
return sprintf "%s %s%s%s from %s::, %s line %s; %s %s %s %s",
map $_ // 'undef',
defined $caller[5] ? $caller[5] ? 'array' : 'scalar' : 'void', # wantarray
$caller[3], # target sub
$caller[4] ? '(@_)' : '', # has args
$caller[7] ? ' [require '.$caller[6].']' : '', # is_require and evaltext
$caller[0], # package
$caller[1], # filename
$caller[2], # line
$caller[7] ? '' : $caller[6] // '', # evaltext if no isrequire
$caller[8], # strcit
$caller[9], # warnings
$caller[10], # hinthash
;
}
sub _get_loaded_files_map
{
my %result = ();
foreach my $key (keys %::)
{
my $glob = $::{$key};
next unless $key =~ s/^_<//;
next unless *$glob{ARRAY} && scalar @{*$glob{ARRAY}};
$result{$key} = ${*$glob};
}
return \%result;
}
sub _get_file_descriptor_by_id
{
my ($file_id) = @_;
my $real_path = _get_real_path_by_normalized_perl_file_id( $file_id );
my $presentable_name;
if ($real_path =~ /^\(eval \d+\)/)
{
my $eval_map_entry = $_evals_to_templates_map{$real_path};
if ($eval_map_entry && $eval_map_entry->{path})
{
$presentable_name = $eval_map_entry->{path};
}
# else
# {
# $presentable_name = $real_path;
# $presentable_name =~ s/^(\(eval \d+\)).+$/$1/;
# }
}
return {
path => $real_path,
name => $presentable_name,
};
}
sub _send_loaded_files_names
{
my $loaded_files_map = _get_loaded_files_map();
my @files_to_add = ();
my @files_to_remove = ();
foreach my $file_id (keys %$loaded_files_map)
{
next if index( $file_id, 'Camelcadedb.pm' ) != -1 || exists $_file_name_sent{$file_id};
$_file_name_sent{$file_id} = 1;
push @files_to_add, _get_file_descriptor_by_id( $file_id );
}
foreach my $file_id (keys %_file_name_sent)
{
next if exists $loaded_files_map->{$file_id};
delete $_file_name_sent{$file_id};
push @files_to_remove, _get_file_descriptor_by_id( $file_id );
}
if (scalar @files_to_add + scalar @files_to_remove)
{
_send_event( "LOADED_FILES_DELTA", {
add => \@files_to_add,
remove => \@files_to_remove
} );
}
}
sub _send_breakpoint_reached_event
{
my ($breakpoint) = @_;
my $event_data = {
path => $breakpoint->{path},
line => $breakpoint->{line} - 1,
logmessage => $breakpoint->{action_result},
};
if ($breakpoint->{suspend})
{
$event_data->{suspend} = \1;
$event_data->{frames} = _calc_stack_frames();
}
else
{
$event_data->{suspend} = \0;
$event_data->{frames} = [ ];
}
_send_event( 'BREAKPOINT_REACHED', $event_data );
}
sub _send_event
{
my ($name, $data) = @_;
_send_data_to_debugger( +{
event => $name,
data => $data
} );
}
sub _dump_stack
{
my $depth = 0;
_report "Stack trace:\n" if $_dev_mode;
while()
{
my @caller = caller( $depth );
last unless defined $caller[2];
_report $frame_prefix_step."%s: %s\n", $depth++, _format_caller( @caller ) if $_dev_mode;
}
1;
}
sub _dump_frames
{
my $depth = 0;
_report "Frames trace:\n" if $_dev_mode;
foreach my $frame (@$_stack_frames)
{
_report $frame_prefix_step."%s: %s\n", $depth++,
join ', ', map $_ // 'undef', @$frame{qw/subname file current_line single/},
$frame->{is_use_block} ? '(use block)' : ''
if $_dev_mode;
}
1;
}
sub _deparse_code
{
my ($code) = @_;
$deparser ||= B::Deparse->new();
return $deparser->coderef2text( $code );
}
sub _send_transaction_response
{
my ($transaction_id, $data) = @_;
_send_data_to_debugger( +{
event => 'RESPONSE',
transactionId => $transaction_id,
data => $data,
}
);
}
sub _get_file_source_by_file_id
{
my ($file_id) = @_;
$_source_been_sent{$file_id} = 1;
{
no strict 'refs';
_report "Getting source of main::_<$file_id" if $_dev_mode;
my @lines = @{"main::_<$file_id"};
shift @lines;
return _to_utf8( join '', @lines );
}
}
sub _get_file_source_once_by_file_id
{
my ($file_id) = @_;
return if $_source_been_sent{$file_id};
return _get_file_source_by_file_id( $file_id );
}
sub _get_file_source_handler
{
my ($request_serialized_object) = @_;
my $transaction_wrapper = _deserialize( $request_serialized_object );
my ($transaction_id, $request_object) = @$transaction_wrapper{qw/id data/};
my $file_id = _get_perl_file_id_by_real_path( $request_object->{path} );
_report "Fetching source for $file_id $request_object->{path}" if $_dev_mode;
_send_transaction_response(
$transaction_id,
_get_file_source_once_by_file_id( $file_id ) // '# No source found for '.$file_id
);
}
sub _get_reference_subelements
{
my ($request_serialized_object) = @_;
my $transaction_wrapper = _deserialize( $request_serialized_object );
my ($transaction_id, $request_object) = @$transaction_wrapper{qw/id data/};
my ($offset, $size, $key) = @$request_object{qw/offset limit key/};
my $data = [ ];
my $source_data;
if ($key =~ /^\*(.+?)(?:\{($glob_slots)\})?$/) # hack for globs by names
{
no strict 'refs';
my ( $name, $slot) = ($1, $2);
if ($slot)
{
$source_data = *{$name}{$slot};
}
else
{
$source_data = \*{$name};
}
_report "Got glob ref $key => $source_data" if $_dev_mode;
}
else
{
$source_data = $_references_cache{$key};
}
if ($source_data)
{
my $reftype = Scalar::Util::reftype( $source_data );
if ($reftype eq 'ARRAY' && $#$source_data >= $offset)
{
my $last_index = $offset + $size;
for (my $item_number = $offset; $item_number < $last_index && $item_number < @$source_data; $item_number++)
{
push @$data, _get_reference_descriptor( "[$item_number]", \$source_data->[$item_number] );
}
}
elsif ($reftype eq 'HASH')
{
my $hash_iterator = Hash::StoredIterator::hash_get_iterator( $source_data );
my @keys = sort keys %$source_data;
Hash::StoredIterator::hash_set_iterator( $source_data, $hash_iterator );
if ($#keys >= $offset)
{
my $last_index = $offset + $size;
for (my $item_number = $offset; $item_number < $last_index && $item_number < @keys; $item_number++)
{
my $hash_key = $keys[$item_number];
push @$data, _get_reference_descriptor( "'$hash_key'", \$source_data->{$hash_key} );
}
}
}
elsif ($reftype eq 'REF')
{
push @$data, _get_reference_descriptor($source_data, $$source_data);
}
elsif ($reftype eq 'GLOB')
{
no strict 'refs';
foreach my $glob_slot (@glob_slots)
{
my $reference = *$source_data{$glob_slot};
next unless $reference;
my $desciptor = _get_reference_descriptor( $glob_slot, \$reference );
if ($glob_slot eq 'HASH' && $key =~ /^\*(::)*(main::)*(::)*DB(::)?$/)
{
$desciptor->{expandable} = \0;
$desciptor->{size} = 0;
}
push @$data, $desciptor;
}
}
else
{
_report "Dont know how to iterate $reftype" if $_dev_mode;
}
}
else
{
_report "No source data for $key\n" if $_dev_mode;
}
_send_transaction_response( $transaction_id, $data );
}
sub _format_variables_hash
{
my ($vars_hash) = @_;
my $result = [ ];
foreach my $variable (sort keys %$vars_hash)
{
my $value = $vars_hash->{$variable};
push @$result, _get_reference_descriptor( $variable, $value );
}
return $result;
}
sub _to_utf8
{
my ($value) = @_;
return $value unless $value;
if (utf8::is_utf8( $value )) # if values is marked as utf8
{
utf8::encode( $value ); # we just making octets from it
}
elsif ($value =~ /[\x80-\xFF]/) # otherwise, if we've got non-ascii symbols, we suppose it's in configured encoding
{
Encode::from_to( $value, $_script_charset, 'utf8' );
}
return $value;
}
sub _from_utf8
{
my ($value) = @_;
return $value unless $value;
if ($_script_charset ne 'utf8') # if script uses non-utf encoding, just encode data to the script encoding
{
Encode::from_to( $value, 'utf8', $_script_charset );
}
else # otherwise, decode octets to characters
{
utf8::decode( $value );
}
return $value;
}
sub _get_reference_descriptor
{
my ($name, $value) = @_;
my $key = $value;
my $reftype = Scalar::Util::reftype( $value );
my $ref = ref $value;
my $size = 0;
my $type = overload::StrVal( $value );
my $expandable = \0;
my $is_blessed = $ref && Scalar::Util::blessed( $value ) ? \1 : \0;
my $ref_depth = 0;
my $is_utf = \0;
my $layers = undef;
my $fileno = undef;
my $rendered = undef;
my $rendered_error = \0;
my $tied;
if (!$reftype)
{
$type = "SCALAR";
$tied = tied $value;
$is_utf = defined $value && utf8::is_utf8( $value ) ? \1 : \0;
$value = defined $value ? "\"$value\"" : 'undef'; #_escape_scalar(
$key //= 'undef';
}
elsif ($reftype eq 'SCALAR')
{
$tied = tied $$value;
$is_utf = defined $$value && utf8::is_utf8( $$value ) ? \1 : \0;
$value = defined $$value ? "\"$$value\"" : 'undef'; #_escape_scalar(
}
elsif ($reftype eq 'REF') {
$type = overload::StrVal($$value) || 'unknown';
$tied = tied $value;
$size = 1;
$expandable = \1;
$ref_depth = 1;
$ref = undef; # to prevent rendering data
}
elsif ($reftype eq 'ARRAY')
{
$size = scalar @$value;
$tied = tied @$value;
$value = sprintf "size = %s", $size;
$expandable = $size ? \1 : \0;
}
elsif ($reftype eq 'HASH')
{
$tied = tied %$value;
my $hash_iterator = Hash::StoredIterator::hash_get_iterator( $value );
$size = scalar keys %$value;
Hash::StoredIterator::hash_set_iterator( $value, $hash_iterator );
$value = sprintf "size = %s", $size;
$expandable = $size ? \1 : \0;
}
elsif ($reftype eq 'GLOB')
{
no strict 'refs';
$tied = tied *$value;
$size = scalar grep *$value{$_}, @glob_slots;
$value = "*".*$value{PACKAGE}."::".*$value{NAME};
$layers = _get_layers($key);
$fileno = fileno($key);
$expandable = $size ? \1 : \0;
}
my $char_code;
my $stringified_key = "$key";
$stringified_key =~ s{(.)}{
$char_code = ord( $1 );
$char_code < 32 ? '^'.chr( $char_code + 0x40 ) : $1
}gsex;
if ($reftype)
{
$_references_cache{$stringified_key} = $key;
}
if ($ref) {
my $got_renderer = 0;
for my $renderer (@renderers) {
if (UNIVERSAL::isa($key, $renderer->[0])) {
$got_renderer = 1;
$DB::Sandbox::it = $key;
$rendered = eval 'package DB::Sandbox;our $it;' . $renderer->[1];
if ($@) {
$rendered = $@;
$rendered_error = \1;
}
last;
}
}
unless ($got_renderer) {
$rendered = "$key";
if ($rendered eq $type) {
$rendered = undef;
}
}
}
$name = "$name";
$value = "$value";
$name =~ s{(.)}{
$char_code = ord( $1 );
$char_code < 32 ? '^'.chr( $char_code + 0x40 ) : $1
}gsex;
$value =~ s{([^\n\r\f\t])}{
$char_code = ord($1);
$char_code < 32 ? '^' . chr($char_code + 0x40) : $1
}gsex;
# handling encoding
my $result = {
name => _to_utf8("$name"),
value => _to_utf8("$value"),
type => "$type",
expandable => $expandable,
key => $stringified_key,
size => $size,
blessed => $is_blessed,
ref_depth => $ref_depth,
is_utf => $is_utf
};
if (defined $rendered) {
$rendered =~ s{([^\n\r\f\t])}{
$char_code = ord($1);
$char_code < 32 ? '^' . chr($char_code + 0x40) : $1
}gsex;
$result->{rendered} = _to_utf8($rendered);
$result->{rendered_error} = $rendered_error;
}
$result->{layers} = $layers if $layers;
$result->{fileno} = "".$fileno if defined $fileno;
$result->{tied_with} = _get_reference_descriptor(object => $tied) if $tied;
return $result;
}
sub _get_layers{
my $glob = shift;
my %result = ();
my $input_layers = _pack_layers(PerlIO::get_layers($glob, details => 1));
$result{input} = $input_layers if $input_layers && @$input_layers;
my $output_layers = _pack_layers(PerlIO::get_layers($glob, details => 1, output=>1));
$result{output} = $output_layers if $output_layers && @$output_layers;
return scalar keys %result ? \%result: undef;
}
sub _pack_layers{
my @result = ();
push @result, { name => shift, param => shift, flags => shift} while @_;
return \@result;
}
#
# Making scalar control elements visible, \n\r for now, need cool conception
#
my %map = (
"\n" => '\n',
"\r" => '\r',
"\f" => '\f',
"\t" => '\t',
);
sub _escape_scalar
{
my ($scalar) = @_;
$scalar =~ s{\\(?=[rnft])}{\\\\}sg;
$scalar =~ s/([\r\n\f\t])/$map{$1}/seg;
return $scalar;
}
sub _get_current_stack_frame
{
return $_stack_frames->[-1];
}
sub _send_data_to_debugger
{
my ($event) = @_;
_send_string_to_debugger( _serialize( $event ) );
}
sub _send_string_to_debugger
{
my ($string) = @_;
$string .= "\n";
print $_debug_socket $string;
_report "Sent to debugger: %s", $string if $_dev_mode;
}
sub _get_adjusted_line_number
{
my ($line_number) = @_;
return $line_number - 1;
}
#@returns JSON::XS
sub _get_seraizlier
{
unless ($coder)
{
$coder = JSON::XS->new();
$coder->latin1();
}
return $coder;
}
sub _serialize
{
my ($data) = @_;
return _get_seraizlier->encode( $data );
}
sub _deserialize
{
my ($json_string) = @_;
return _get_seraizlier->decode( $json_string );
}
sub _calc_stack_frames
{
my $frames = [ ];
my $depth = 0;
%_references_cache = ();
while ()
{
my ($package, $filename, $line, $subroutine, $hasargs,
$wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash) = caller( $depth );
my $cnt = 0;
my @frame_args = map _get_reference_descriptor( '$_['.$cnt++.']', $_ ), @DB::args;
last unless defined $filename;
if ($package && $package ne 'DB')
{
if (@$frames && $subroutine ne '(eval)')
{
$frames->[-1]->{file}->{name} = $subroutine;
}
my $global_variables = [ ];
my $global_variables_hash = eval {peek_our( $depth + 1 )};
unless ($@)
{
$global_variables = _format_variables_hash( $global_variables_hash );
}
my $lexical_variables = [ ];
my $variables_hash = eval {peek_my( $depth + 1 )};
unless ($@)
{
$lexical_variables = _format_variables_hash( $variables_hash );
}
$frames->[-1]->{args} = \@frame_args if scalar @$frames;
my $descriptor = _get_file_descriptor_by_id( $filename );
push @$frames, {
file => $descriptor,
line => $line - 1,
lexicals => $lexical_variables,
globals => $global_variables,
main_size => scalar keys %::,
args => [ ],
};
}
$depth++;
}
return $frames;
}
sub _is_use_frame
{
my $stack_frame = shift;
my $is_use_block = 0;
if (ref $stack_frame->{subname})
{
my $deparsed_block = _deparse_code( $stack_frame->{subname} );
$is_use_block = $deparsed_block =~ /require\s+[\w\:]+\s*;\s*do/si;
}
return $is_use_block;
}
sub _set_frames_single
{
my ($new_value) = @_;
foreach my $frame (@{$_stack_frames})
{
$frame->{single} = $new_value;
}
$DB::single = $new_value;
}
sub _hold_the_line
{
_set_frames_single( STEP_INTO );
}
sub _release_the_hounds
{
_set_frames_single( STEP_CONTINUE );
}
sub _process_command
{
my ($command) = @_;
_report "============> Got command: '%s'\n", $command if $_dev_mode;
if ($command eq 'q')
{
_report "Exiting" if $_dev_mode;
exit;
}
elsif ($command =~ /^e\s+(.+)$/) # eval expresion
{
my $data = $1;
my $transaction_data = _deserialize( $data );
my ($transaction_id, $request_object) = @$transaction_data{qw/id data/};
my $result = _eval_expression( _from_utf8( $request_object->{expression} // '' ) );
$result->{result} = _get_reference_descriptor( result => $result->{result} );
_send_transaction_response( $transaction_id, $result );
_report "Result is $result\n" if $_dev_mode;
}
elsif ($command eq 'pause')
{
_hold_the_line;
}
elsif ($command eq 'g')
{
_release_the_hounds();
return;
}
elsif ($command =~ /^b (.+)$/) # set breakpoints from proto
{
_process_new_breakpoints( $1 );
}
elsif ($command =~ /^p (.+)$/) # Run to cursor
{
_set_run_to_cursor_breakpoint( $1 );
_release_the_hounds();
return;
}
elsif ($command eq 'o') # over,
{
my $current_frame = _get_current_stack_frame;
if (_is_use_frame( $current_frame ))
{
$current_frame->{single} = STEP_INTO;
$DB::single = STEP_CONTINUE;
}
else
{
$DB::single = STEP_OVER;
}
return;
}
elsif ($command =~ /^getchildren (.+)$/) # expand,
{
_get_reference_subelements( $1 );
}
elsif ($command =~ /^get_source (.+)$/) # get eval/file source
{
_get_file_source_handler( $1 );
}
elsif ($command eq 'u') # step out
{
my $current_frame = _get_current_stack_frame;
if (_is_use_frame( $current_frame ))
{
$current_frame->{single} = STEP_CONTINUE;
}
$DB::single = STEP_CONTINUE;
return;
}
else
{
$DB::single = STEP_INTO;
return;
}
return 1;
}
my $input_buffer = '';
sub _get_next_command
{
my $read_bytes;
my $new_line_index = index $input_buffer, "\n";
if ($new_line_index == -1)
{
while( $read_bytes = sysread( $_debug_socket, $input_buffer, 10240, length( $input_buffer ) ))
{
last if ($new_line_index = index $input_buffer, "\n") > -1;
}
unless (defined $read_bytes)
{
die 'Debugging socket disconnected';
}
unless ($new_line_index > -1)
{
if (scalar @saved) {
($@, $!, $^E, $,, $/, $\, $^W) = @saved;
@saved = ();
}
print STDERR "Buffer $input_buffer has no newlines in it and nothing is in the socket\n";
exit -1;
}
}
my $command = substr $input_buffer, 0, $new_line_index + 1, '';
$command =~ s/[\r\n]+$//;
# printf STDERR "Got command: %s and left in buffer %s\n%s\n\%s\n", length $command, length $input_buffer ,$command, $input_buffer;
return $command;
}
sub _can_read
{
return length( $input_buffer ) > 0 || ($_debug_socket && $_debug_socket_select && scalar $_debug_socket_select->can_read( 0 ));
}
sub _event_handler
{
my ($breakpoint) = @_;
_send_loaded_files_names();
if ($breakpoint && !$breakpoint->{run_to_cursor})
{
_send_breakpoint_reached_event( $breakpoint );
}
else
{
_send_event( "STOP", _calc_stack_frames() );
}
while()
{
_report "Waiting for input\n" if $_dev_mode;
_process_command( _get_next_command ) || return;
}
}
sub _enter_frame
{
my ($old_db_single, $wantarray) = @_;
_report "Entering frame %s: %s %s-%s-%s, %s",
scalar @$_stack_frames + 1,
$DB::sub,
$DB::trace // 'undef',
$DB::signal // 'undef',
$old_db_single // 'undef',
$wantarray ? 'ARRAY' : defined $wantarray ? 'SCALAR' : 'VOID'
if $_debug_sub_handler && $_dev_mode;
$frame_prefix = $frame_prefix_step x (scalar @$_stack_frames + 1);
my $new_stack_frame = {
subname => $DB::sub,
single => $old_db_single,
};
push @{$_stack_frames}, $new_stack_frame;
_set_break_points_for_files() if $_enable_compile_time_breakpoints && $ready_to_go;
return $new_stack_frame;
}
sub _exit_frame
{
$_internal_process = 1;
my $frame = pop @$_stack_frames;
$frame_prefix = $frame_prefix_step x (scalar @$_stack_frames);
_report "Leaving frame %s, setting single to %s", (scalar @$_stack_frames + 1),
$frame->{single} if $_debug_sub_handler && $_dev_mode;
$DB::single = $frame->{single};
$_internal_process = 0;
}
sub _get_normalized_perl_file_id
{
my ($perl_file_id) = @_;
if ($perl_file_id =~ /_<(.+)$/)
{
return $1;
}
else
{
die "PANIC: Incorrect perl file id $perl_file_id";
}
}
sub _get_perl_file_id_by_real_path
{
my ($path) = @_;
return $path if $path =~ /^\(eval \d+\)/;
return exists $_paths_to_perl_file_id_map{$path} ? $_paths_to_perl_file_id_map{$path} : undef;
}
sub _get_real_path_by_normalized_perl_file_id
{
my $perl_file_id = shift;
unless ($perl_file_id)
{
_dump_stack && _dump_frames && die "Perl normalized file id undefined";
}
if (!exists $_perl_file_id_to_path_map{$perl_file_id})
{
no strict 'refs';
my $path = ${*{"::_<$perl_file_id"}};
return '' unless defined $path; # some subs created via XS
my $real_path = _calc_real_path( $path, $perl_file_id );
$_perl_file_id_to_path_map{$perl_file_id} = $real_path;
$_paths_to_perl_file_id_map{$real_path} = $perl_file_id;
}
return $_perl_file_id_to_path_map{$perl_file_id};
}
sub _get_real_path_by_perl_file_id
{
my ($perl_file_id) = @_;
return _get_real_path_by_normalized_perl_file_id( _get_normalized_perl_file_id( $perl_file_id ) );
}
sub _get_loaded_breakpoints_by_real_path
{
my ($real_path) = @_;
my $result = { };
if ($_loaded_breakpoints{$real_path})
{
_report "Found real breakpoints" if $_dev_mode && $_debug_breakpoints;
%$result = %{$_loaded_breakpoints{$real_path}};
}
# append breakpoints from templates
if (my $substituted_file_descriptor = $_evals_to_templates_map{$real_path})
{
my ($template_path, $lines_map) = @$substituted_file_descriptor{qw/path lines_map/};
_report "Found template file %s", $template_path if $_dev_mode && $_debug_breakpoints;
if (my $template_breakpoints = $_loaded_breakpoints{$template_path})
{
_report "Found template breakpoints" if $_dev_mode && $_debug_breakpoints;
foreach my $line (keys %$template_breakpoints)
{
if (my $mapped_line = $lines_map->{$line})
{
_report "Got mapped breakpoint %s => %s", $line, $mapped_line if $_dev_mode && $_debug_breakpoints;
$result->{$mapped_line} //= $template_breakpoints->{$line};
}
}
}
}
return scalar keys %$result ? $result : undef;
}
sub _get_current_breakpoint
{
return if $DB::single || $DB::signal;
my $current_breakpoint = $DB::dbline{$current_line};
return unless $current_breakpoint;
if ($current_breakpoint->{run_to_cursor})
{
$current_breakpoint->{remove} = 1;
$current_breakpoint->{line}--; # fixme find out why works without it
_process_breakpoints_descriptors( [ $current_breakpoint ] );
}
return $current_breakpoint;
}
sub _eval_expression
{
my ($expression ) = @_;
my $expr = "no strict; package $current_package;".'( $@, $!, $^E, $,, $/, $\, $^W ) = @DB::saved;'."$expression";
_report "Running %s\n", $expr if $_dev_mode;
my $result;
{
local $SIG{__WARN__} = sub {};
$result = eval $expr;
}
if (my $e = $@)
{
# fixme handle object exceptions
unless (ref $e) # message, change it
{
$e = join "; ", map {s/ at \(eval \d+.+$//;
$_ } grep $_, split /[\r\n]+/, $e;
}
$result = {
error => \1,
result => $e
};
}
else
{
$result = {
error => \0,
result => $result
};
}
return $result;
}
sub _reset_breakpoint
{
my ($breakpoint_descriptor, $real_line, $perl_breakpoints_map) = @_;
my $real_path = $breakpoint_descriptor->{path};
if (exists $_loaded_breakpoints{$real_path} && exists $_loaded_breakpoints{$real_path}->{$real_line})
{
delete $_loaded_breakpoints{$real_path}->{$real_line};
}
if ($perl_breakpoints_map)
{
$perl_breakpoints_map->{$real_line} = 0;
}
}
sub _set_run_to_cursor_breakpoint
{
my ($serialized_descriptor) = @_;
my $descriptor = _deserialize( $serialized_descriptor );
@$descriptor{qw/run_to_cursor condition remove suspend/} = (1, undef, undef, \1);
_process_breakpoints_descriptors( [ $descriptor ] );
}
sub _set_breakpoint
{
my ($breakpoint_descriptor, $real_line, $perl_breakpoints_map, $perl_source_lines) = @_;
my $event_data = {
path => $breakpoint_descriptor->{path},
line => $breakpoint_descriptor->{line} - 1,
};
_report 'Setting breakpoint to %s, real line %s, %s', $breakpoint_descriptor->{path}, $real_line,
$perl_source_lines->[$real_line] if $_dev_mode && $_debug_breakpoints;
$breakpoint_descriptor->{_processed} = 1;
if (!defined $perl_source_lines->[$real_line] || $perl_source_lines->[$real_line] == 0) {
_send_event("BREAKPOINT_DENIED", $event_data);
}
else {
$perl_breakpoints_map->{$real_line} = $breakpoint_descriptor;
_send_event("BREAKPOINT_SET", $event_data) unless $breakpoint_descriptor->{run_to_cursor};
}
}
sub _set_up_debugger
{
my ($json_data) = @_;
_report 'Setting up debugger: %s', $json_data if $_dev_mode;
my $set_up_data = _deserialize( $json_data );
$_script_charset = $set_up_data->{charset};
_process_breakpoints_descriptors( $set_up_data->{breakpoints} );
$_enable_compile_time_breakpoints = 1 if $set_up_data->{enableCompileTimeBreakpoints};
$_enable_noninteractive_mode = 1 if $set_up_data->{enableNonInteractiveMode};
if (ref $set_up_data->{renderers} eq 'ARRAY') {
for my $entry (@{$set_up_data->{renderers}}) {
if (ref $entry ne 'HASH') {
next;
}
my ($package, $code) = @$entry{qw/packageName renderExpression/};
if (!$package || !$code) {
next;
}
push @renderers, [ $package, $code ];
}
}
my $start_mode = $set_up_data->{startMode};
if ($set_up_data->{initCode})
{
eval $set_up_data->{initCode};
die "*** Debugger init code error:\n$@" if $@;
}
if ($start_mode eq 'RUN')
{
return STEP_CONTINUE;
}
elsif ($start_mode eq 'COMPILE')
{
return STEP_INTO;
}
else # here we should have a RUN_TO_BREAKPOINT
{
$_skip_run_stop = 1;
return STEP_CONTINUE;
}
}
sub _set_up_after_connect
{
my ($allow_fail) = @_;
$_debug_socket->autoflush( 1 );
$_debug_socket_select = IO::Select->new();
$_debug_socket_select->add( $_debug_socket );
_send_data_to_debugger( +{
event => 'READY',
version => $API_VERSION,
} );
_report "Waiting for set up data..." if $_dev_mode;
my $set_up_data = <$_debug_socket>;
return if !defined $set_up_data && $allow_fail;
die "Connection closed" unless defined $set_up_data;
$ready_to_go = 1;
$DB::single = _set_up_debugger( $set_up_data );
}
sub _process_new_breakpoints
{
my ($json_data) = @_;
_report "Processing breakpoints: %s", $json_data if $_dev_mode;
return _process_breakpoints_descriptors( _deserialize( $json_data ) );
}
sub _process_breakpoints_descriptors
{
my ($descriptors) = @_;
foreach my $descriptor (@$descriptors)
{
$descriptor->{line}++;
$descriptor->{condition} = _from_utf8( $descriptor->{condition} );
$descriptor->{action} = _from_utf8( $descriptor->{action} );
_report "Processing descriptor: %s %s %s", $descriptor->{path}, $descriptor->{line},
$descriptor->{remove} ? 'remove' : 'set' if $_dev_mode;
my ($real_path, $line) = @$descriptor{qw/path line/};
$_loaded_breakpoints{$real_path} //= { };
$_loaded_breakpoints{$real_path}->{$line} = $descriptor;
$_queued_breakpoints_files{$real_path} = 1;
}
_set_break_points_for_files() if $ready_to_go;
}
sub _set_break_points_for_files
{
return unless $ready_to_go;
my $paths_array = [ keys %_queued_breakpoints_files ];
return unless @{$paths_array};
my $default_context = undef;
foreach my $real_path (@{$paths_array})
{
_report "Setting breakpoints for %s", $real_path if $_dev_mode;
# mapping real path to file id
my $perl_file_id = $real_path =~ /^\(eval \d+\)/
? $real_path
: exists $_paths_to_perl_file_id_map{$real_path} ? $_paths_to_perl_file_id_map{$real_path} : next;
# getting perl source lines and breakpoints
my $glob = $::{"_<$perl_file_id"};
next unless $glob && *{$glob}{ARRAY} && scalar @{*{$glob}{ARRAY}};
my $perl_source_lines = *{$glob}{ARRAY};
my $perl_breakpoints_map = *{$glob}{HASH};
# getting breakpoints passed from the IDE
my $loaded_breakpoints_descriptors = _get_loaded_breakpoints_by_real_path( $real_path ) or next;
# switching context
my $old_context = _switch_context( $perl_file_id );
$default_context //= $old_context;
my @lines = keys %{$loaded_breakpoints_descriptors};
my $breakpoints_left = scalar @lines;
foreach my $real_line (@lines) {
my $breakpoint_descriptor = $loaded_breakpoints_descriptors->{$real_line};
if (exists $breakpoint_descriptor->{_processed}) {
$breakpoints_left--;
_report "Breakpoint is already set: %s, %s, %s",
@{$breakpoint_descriptor}{qw/path line remove/} if $_dev_mode;
next;
}
if ($real_line > $#$perl_source_lines) {
_report "Skip breakpoint setting, file seems not completely compiled. Breakpoint: %s, %s, %s, script lines: %s",
@{$breakpoint_descriptor}{qw/path line remove/}, $#$perl_source_lines if $_dev_mode;
next;
}
_report "Processing descriptor %s, %s, %s", @{$breakpoint_descriptor}{qw/path line remove/} if $_dev_mode;
if ($breakpoint_descriptor->{remove}) {
_reset_breakpoint($breakpoint_descriptor, $real_line, $perl_breakpoints_map);
}
else {
_set_breakpoint($breakpoint_descriptor, $real_line, $perl_breakpoints_map, $perl_source_lines);
}
$breakpoints_left--;
}
delete $_queued_breakpoints_files{$real_path} unless $breakpoints_left;
}
_switch_context( $default_context );
}
#sub mydie
#{
# my ($msg) = @_;
# print "$msg\n";
# print Carp::longmess;
# foreach my $key (sort keys %::)
# {
# print $key."\n" if $key =~ /^_</;
# }
# exit -1;
#}
sub _calc_real_path
{
my $path = shift;
my $new_filename = shift;
my $real_path;
if ($path =~ /^\(eval (\d+)/)
{
$real_path = $path;
}
else
{
$real_path = eval {Cwd::realpath( $path )};
unless ($real_path)
{
_report 'Unable to find real path for %s use as it is', $path if $_dev_mode;
$real_path = $path;
}
$real_path =~ s{\\}{/}g;
}
_report "$new_filename real path is $real_path\n" if $trace_real_path && $_dev_mode;
return $real_path;
}
sub _switch_context
{
my ($context_key) = @_;
return unless $context_key;
$context_key =~ s/^_<//;
no strict 'refs';
my $current_context = $DB::dbline;
*DB::dbline = *{"::_<$context_key"};
return $current_context;
}
# When the execution of your program reaches a point that can hold a breakpoint, the DB::DB() subroutine is called if
# any of the variables $DB::trace , $DB::single , or $DB::signal is true. These variables are not localizable. This
# feature is disabled when executing inside DB::DB() , including functions called from it unless $^D & (1<<30) is true.
sub step_handler
{
return if $_internal_process || !$ready_to_go;
$_internal_process = 1;
# Save eval failure, command failure, extended OS error, output field
# separator, input record separator, output record separator and
# the warning setting.
@saved = ( $@, $!, $^E, $,, $/, $\, $^W );
$, = ""; # output field separator is null string
$/ = "\n"; # input record separator is newline
$\ = ""; # output record separator is null string
$^W = 0; # warnings are off
# set breakpoints for evals if any appeared
_set_break_points_for_files() if $ready_to_go;
# updating current position
my @caller = caller();
($current_package, $current_file_id, $current_line) = @caller[0, 1, 2];
if (defined $current_file_id)
{
_report( <<'EOM',
Calling %s %s
EOM
_format_caller( @caller ),
${^GLOBAL_PHASE} // 'unknown',
) if $_dev_mode;
_switch_context( $current_file_id );
}
else
{
_dump_stack && _dump_frames && warn "CAN'T FIND CALLER;\n";
}
my $skip_event_handler = 0;
my $breakpoint;
if ($breakpoint = _get_current_breakpoint)
{
my $condition = $breakpoint->{condition};
if ($condition && !_eval_expression( $condition )->{result})
{
($@, $!, $^E, $,, $/, $\, $^W) = @saved;
@saved = ();
$_internal_process = 0;
return;
}
if (my $action = $breakpoint->{action})
{
$breakpoint->{action_result} = _to_utf8( _eval_expression( $action )->{result} );
}
if (!$breakpoint->{suspend})
{
_send_breakpoint_reached_event( $breakpoint );
($@, $!, $^E, $,, $/, $\, $^W) = @saved;
@saved = ();
$_internal_process = 0;
return;
}
foreach my $frame (@{$_stack_frames})
{
$frame->{single} = STEP_INTO;
}
$DB::single = STEP_INTO;
}
elsif ($DB::single && $_skip_run_stop)
{
$_skip_run_stop = 0;
$skip_event_handler = 1;
$DB::single = STEP_CONTINUE;
}
elsif ($_skip_run_stop)
{
$_skip_run_stop = 0;
}
my $old_db_single = $DB::single;
$DB::single = STEP_CONTINUE;
_report "Step with %s %s %s, %s-%s-%s %s",
$current_package // 'undef',
$current_file_id // 'undef',
$current_line // 'undef',
$DB::trace // 'undef',
$DB::signal // 'undef',
$old_db_single // 'undef',
${^GLOBAL_PHASE}
if $_dev_mode;
_report $DB::dbline[$current_line] if $_dev_mode;
_event_handler( $breakpoint ) unless $skip_event_handler;
$_internal_process = 0;
($@, $!, $^E, $,, $/, $\, $^W) = @saved;
@saved = ();
();
}
#
# This is a hook for templating engines working using perl evals.
# This hook should be invoked after evaluation of compiled template with template path
# and map of lines template_line => compiled_source_line
#
# {
# no strict 'refs';
# my $glob = *{'::DB::template_handler'};
#
# if ($glob && *{$glob}{CODE})
# {
# *{$glob}{CODE}->($filepath, $lines_map);
# }
# }
#
#
sub template_handler
{
my ($real_path, $lines_map) = @_;
my $last_eval_id = 0;
my $eval_target;
foreach my $main_key (keys %::)
{
if ($main_key =~ /^_<(\(eval (\d+)\).+?)$/)
{
if ($last_eval_id < $2)
{
$last_eval_id = $2;
$eval_target = $1;
}
}
}
if ($last_eval_id)
{
$real_path = Cwd::realpath( $real_path );
$_evals_to_templates_map{$eval_target} = {
path => $real_path,
lines_map => $lines_map
};
$_templates_to_evals_map{$real_path} //= {
lines_map => $lines_map,
evals => [ ]
};
push @{$_templates_to_evals_map{$real_path}->{evals}}, $eval_target;
delete $_file_name_sent{$eval_target}; # forces re-sending file descriptor
_report "Mapped template: %s to eval %s", $real_path, $eval_target if $_dev_mode;
$_queued_breakpoints_files{$eval_target} = 1;
_set_break_points_for_files() if $ready_to_go;
}
else
{
_report "Unable to locate top level eval for %s", $real_path if $_dev_mode;
}
}
# this pass-through flag handles quotation overload loop
sub sub_handler
{
my $stack_frame = undef;
my $old_db_single = $DB::single;
my $wantarray = wantarray;
if (!$_internal_process)
{
$_internal_process = 1;
_process_command( _get_next_command ) while $_enable_noninteractive_mode && _can_read;
$old_db_single = $DB::single; # might be overriden in commands
$DB::single = STEP_CONTINUE;
$stack_frame = _enter_frame( $old_db_single, $wantarray );
if ($current_package && $current_package eq 'DB')
{
_report "PANIC: Catched internal call" if $_dev_mode;
_dump_stack && _dump_frames();
die;
}
$DB::single = $old_db_single;
$_internal_process = 0;
}
my $stack_pointer = $#$_stack_frames;
if ($DB::single == STEP_OVER)
{
_report "Disabling step in in subcalls, will restore %s\n",
$_stack_frames->[-1]->{single}
if $_debug_sub_handler && $_dev_mode;
$DB::single = STEP_CONTINUE;
# my $die_handler = $SIG{__DIE__};
# local $SIG{__DIE__} = sub{
# _hold_the_line;
# goto &$die_handler if $die_handler;
# };
}
else
{
_report "Keeping step as %s\n", $old_db_single if $stack_frame && $_debug_sub_handler && $_dev_mode;
}
if ($DB::sub eq 'DESTROY' or substr( $DB::sub, -9 ) eq '::DESTROY' or !defined $wantarray)
{
no strict 'refs';
&$DB::sub;
$#$_stack_frames = $stack_pointer;
if ($stack_frame)
{
_exit_frame();
}
else
{
$DB::single = $old_db_single;
}
$DB::ret = undef; # return value
}
elsif ($wantarray)
{
no strict 'refs';
my @result = &$DB::sub;
$#$_stack_frames = $stack_pointer;
if ($stack_frame)
{
_exit_frame();
}
else
{
$DB::single = $old_db_single;
}
@DB::ret = @result; # return value
}
else
{
no strict 'refs';
my $result = &$DB::sub;
$#$_stack_frames = $stack_pointer;
if ($stack_frame)
{
_exit_frame();
}
else
{
$DB::single = $old_db_single;
}
$DB::ret = $result; # return value
}
}
# If the call is to an lvalue subroutine, and &DB::lsub is defined &DB::lsub (args) is called instead, otherwise
# falling back to &DB::sub (args).
#sub lsub_handler: lvalue
#{
# my $stack_frame = undef;
#
# my $old_db_single = $DB::single;
# if (!$_internal_process)
# {
# $_internal_process = 1;
#
# $DB::single = STEP_CONTINUE;
# $stack_frame = _enter_frame( $old_db_single );
#
# $DB::single = $old_db_single;
# $_internal_process = 0;
# }
#
# if ($DB::single == STEP_OVER)
# {
# _report "Disabling step in in subcalls\n" if $_dev_mode;
# $DB::single = STEP_CONTINUE;
# }
# else
# {
# _report "Keeping step as %s\n", $old_db_single if $stack_frame if $_dev_mode;
# }
#
# {
# no strict 'refs';
# my $result = &$DB::sub;
# if ($stack_frame)
# {
# _exit_frame();
# }
# else
# {
# $DB::single = $old_db_single;
# }
# return $DB::ret = $result;
# }
#}
# After each required file is compiled, but before it is executed, DB::postponed(*{"::_<$filename"}) is called if the
# subroutine DB::postponed exists. Here, the $filename is the expanded name of the required file, as found in the values
# of %INC.
#
# After each subroutine subname is compiled, the existence of $DB::postponed{subname} is checked. If this key exists,
# DB::postponed(subname) is called if the DB::postponed subroutine also exists.
sub load_handler
{
my $old_db_single = $DB::single;
$DB::single = STEP_CONTINUE;
my $old_internal_process = $_internal_process;
$_internal_process = 1;
my $perl_file_id = $_[0];
my $real_path = _get_real_path_by_perl_file_id( $perl_file_id );
_report "Loading module: %s => %s %s-%s-%s",
$perl_file_id,
$real_path,
$DB::trace // 'undef',
$DB::signal // 'undef',
$old_db_single // 'undef',
if $_debug_load_handler && $_dev_mode
;
_set_break_points_for_files() if $ready_to_go;
$_internal_process = $old_internal_process;
$DB::single = $old_db_single;
}
# When execution of the program uses goto to enter a non-XS subroutine and the 0x80 bit is set in $^P , a call to
# &DB::goto is made, with $DB::sub holding the name of the subroutine being entered.
#$^P |= FLAG_REPORT_GOTO;
#sub goto_handler
#{
# return if $_internal_process;
# $_internal_process = 1;
#
# my $old_db_single = $DB::single;
# $DB::single = STEP_CONTINUE;
#
# _report "Goto called%s from %s-%s-%s-%s",
# scalar @_ ? ' with '.(join ',', @_) : '',
# $DB::trace // 'undef',
# $DB::signal // 'undef',
# $old_db_single // 'undef',
# ${^GLOBAL_PHASE} // 'unknown',
# if $_dev_mode;
# $DB::single = $old_db_single;
# $_internal_process = 0;
#}
unless ($ENV{PERL5_DEBUG_ROLE} && $ENV{PERL5_DEBUG_HOST} && $ENV{PERL5_DEBUG_PORT})
{
printf STDERR <<'EOM', map $_ // 'undefined', @ENV{qw/PERL5_DEBUG_ROLE PERL5_DEBUG_HOST PERL5_DEBUG_PORT/};
Can't start debugging session. In order to make it work, you should set up environment variables:
PERL5_DEBUG_ROLE - set this to 'server' if you want to make Perl process act as a server, and to 'client' to make it connect to IDEA.
PERL5_DEBUG_HOST - host to bind or connect, depending on role.
PERL5_DEBUG_PORT - host to listen or connect, depending on role.
Atm we've got:
PERL5_DEBUG_ROLE=%s
PERL5_DEBUG_HOST=%s
PERL5_DEBUG_PORT=%s
EOM
exit;
}
my $_connect_at_start = exists $ENV{PERL5_DEBUG_AUTOSTART} ? $ENV{PERL5_DEBUG_AUTOSTART} : 1;
sub is_connected
{
return !!$_debug_socket;
}
sub _connect
{
my $_perl5_debug_host = $ENV{PERL5_DEBUG_HOST};
my $_perl5_debug_port = $ENV{PERL5_DEBUG_PORT};
# ${^TAINT} will be truthy if taint mode is on.
if (${^TAINT})
{
# The debugger will fail with "Insecure dependency in connect..."
# if we do not untaint the host and port variables, so we do so here.
($_perl5_debug_host) = $_perl5_debug_host =~ /(.*)/;
($_perl5_debug_port) = $_perl5_debug_port =~ /(.*)/;
}
my ($attempts, $allow_fail) = @_;
if ($ENV{PERL5_DEBUG_ROLE} eq 'server')
{
printf STDERR "Listening for the IDE connection at %s:%s...\n", $_perl5_debug_host, $_perl5_debug_port;
my $_server_socket = IO::Socket::INET->new(
Listen => 1,
LocalAddr => $_perl5_debug_host,
LocalPort => $_perl5_debug_port,
ReuseAddr => 1,
Proto => 'tcp',
) || die "Error binding to ${_perl5_debug_host}:${_perl5_debug_port}";
$_debug_packed_address = accept( $_debug_socket, $_server_socket );
}
else
{
foreach my $attempt (1 .. $attempts)
{
printf STDERR "($attempt)Connecting to the IDE from process %s at %s:%s...\n", $$, ${_perl5_debug_host},
${_perl5_debug_port};
$_debug_socket = IO::Socket::INET->new(
PeerAddr => $_perl5_debug_host,
PeerPort => $_perl5_debug_port,
ReuseAddr => 1,
Proto => 'tcp',
);
last if $_debug_socket || $attempt == $attempts;
sleep( 1 ); # this is kinda hacky
}
die "Error connecting to ${_perl5_debug_host}:${_perl5_debug_port}" if !$_debug_socket && !$allow_fail;
}
_set_up_after_connect( $allow_fail ) if $_debug_socket;
}
sub connect
{
_connect( 1, 1 );
}
sub disconnect
{
return unless is_connected();
$_debug_socket->close();
undef $_debug_socket_select;
undef $_debug_socket;
$ready_to_go = 0;
}
sub connect_or_reconnect
{
disconnect() if is_connected();
_connect( 1, 1 );
}
# we want disable() to completely bypass the debugger (except for the parts
# which are required for bookkeeping, like DB::postponed)
#
# setting %^P can disable DB::DB, but the only way to disable DB::sub is to
# make sure *DB::sub{CODE} is undef, while keeping %DB::sub and $DB::sub
# intact; the only way to do that is to save the glob slots we want to
# preserve, undef the glob and then restore the slots
#
# DB::lsub and DB::goto are easier, because we don't need to preserve the
# corresponding scalar/array/hash variables
my (%_orig_db_sub, %_disabled_db_sub, $_orig_db_lsub);
BEGIN
{
%_orig_db_sub = %_disabled_db_sub = map {
($_ => *DB::sub{$_}) x !!*DB::sub{$_}
} qw(SCALAR ARRAY HASH);
$_orig_db_sub{CODE} = \&sub_handler;
$_orig_db_lsub = undef; # \&lsub_handler
}
sub enable
{
$^P = DEBUG_DEFAULT_FLAGS;
undef *DB::sub;
*DB::sub = $_orig_db_sub{$_} for keys %_orig_db_sub;
#*DB::lsub = $_orig_db_lsub;
#*DB::goto = \&goto_handler;
}
sub disable
{
$DB::single = 0;
$^P = DEBUG_PREPARE_FLAGS;
undef *DB::sub;
undef *DB::lsub;
undef *DB::goto;
*DB::sub = $_disabled_db_sub{$_} for keys %_disabled_db_sub;
}
push @$_stack_frames, {
subname => 'SCRIPT',
file => $current_file_id,
current_line => $current_line,
single => STEP_INTO,
};
_dump_stack && _dump_frames if $trace_code_stack_and_frames;
$_internal_process = 1;
require Cwd;
Cwd::getcwd();
require B::Deparse;
require JSON::XS;
$frame_prefix = $frame_prefix_step;
foreach my $main_key (keys %::)
{
if ($main_key =~ /_<(.+)/)
{
_get_real_path_by_normalized_perl_file_id( $1 );
}
}
*DB::DB = \&step_handler;
*DB::postponed = \&load_handler;
if ($_connect_at_start)
{
_connect( 10, 0 );
enable();
}
$_internal_process = 0;
1; # End of Devel::Camelcadedb