Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I change lines of code in a loaded module in Perl?

Tags:

module

perl

When I use the FLV::Info module to extract metadata from or merge multiple FLV files, I frequently receive a "Tag size is too small" error and then the module will just refuse to work. Someone issued a bug report here three years ago but there does not seem to be a fix.

Well, recently I find if I simply comment out the following lines of code in Tag.pm, one of FLV::Info's dependency modules like so:

=pod
if ($datasize < 11)
   {
      die "Tag size is too small ($datasize) at byte " . $file->get_pos(-10);
   }
=cut

FLV::Info will then readily do the work as expected.

I'm not sure if this is a very dumb question but I feel curious:

Is there a simple way of changing a couple of lines of code in a loaded module without modifying the original .pm file?

Any ideas, suggestions or comments? Thanks like always :)

UPDATE

Big thanks to @Shwern. Your answer is highly satisfying :) Also thanks to @DVK for the suggestion and that "monkey patch" term and @brian for the book recommendation.

Here's my feedback for tests on a sample FLV file that would throw me "Tag size is too small" error if I use the original module without doing anything to it.

The "eval it back" approach solves the problem

use FLV::Info;

use Data::Dump::Streamer;
my $original = FLV::Tag->can("parse");
my $code = Dump($original)->Out;
#$code =~ s{\Qif ($datasize < 11)\E}{if (0)}; #This somehow won't work
$code =~ s{die "Tag}{warn "Tag}; #Let it warn but not die

no warnings 'redefine';
*FLV::Tag::parse = eval $code;

my $reader = FLV::Info->new();
$reader->parse('sample.flv');
my %info = $reader->get_info();
print "$info{video_count} video frames\n";
print $reader->report();

The "overide die to not die" approach also works

BEGIN {
    *CORE::GLOBAL::die = sub { return CORE::die(@_) };
}
use FLV::Info;

{
    local *CORE::GLOBAL::die = sub {
         return if $_[0] =~ /^Tag size is too small/;
         return CORE::die(@_);
};

my $reader = FLV::Info->new();
$reader->parse('sample.flv');
my %info = $reader->get_info();
print "$info{video_count} video frames\n";
print $reader->report();
}

The "redefine" approach, however, does not work as I expected.

I copy-and-pasted the original FLV::Tag::parse subroutine and commented out the lines of code exactly the way I modified the original Tag.pm file like so:

use FLV::Info;
no warnings 'redefine';
*FLV::Tag::parse = sub {
    ...
    ...
=pod
   if ($datasize < 11)
   {
      die "Tag size is too small ($datasize) at byte " . $file->get_pos(-10);
   }
=cut
   ...
   ...
};

my $reader = FLV::Info->new();
$reader->parse('sample.flv');
my %info = $reader->get_info();
print "$info{video_count} video frames\n";
print $reader->report();

but I got this error:

Unknown tag type 18 at byte 13 (0xd)

Well, even if copy-and-pasted exactly the same parse subroutine without any modification in my redefinition, I'm receiving the "Unknown tag type" error instead of "Tag size is too small".

This is strange!

For reference, the "eval it back" and "override die to not die" approaches will give me the following:

1992 video frames
File name                sample.flv
File size                5767831 bytes
Duration                 about 79.6 seconds
Video                    1992 frames
  codec                  AVC
  type                   interframe/keyframe
Audio                    1712 packets
  format                 AAC
  rate                   44100 Hz
  size                   16 bit
  type                   stereo
Meta                     1 event
  audiocodecid           10
  audiosamplerate        22050
  audiosamplesize        16
  audiosize              342817
  creationdate           unknown
  datasize               805
  duration               79.6
  filesize               5767869
  framerate              25
  height                 300
  keyframes              {
    >>>                    'filepositions' => [
    >>>                                         '780',
    >>>                                         '865',
    >>>                                         '1324122',
    >>>                                         '2348913',
    >>>                                         '2978630',
    >>>                                         '3479001',
    >>>                                         '3973756',
    >>>                                         '4476281',
    >>>                                         '4997226',
    >>>                                         '5391890'
    >>>                                       ],
    >>>                    'times' => [
    >>>                                 '0',
    >>>                                 '0',
    >>>                                 '9.6',
    >>>                                 '19.2',
    >>>                                 '28.8',
    >>>                                 '38.4',
    >>>                                 '46.32',
    >>>                                 '55.92',
    >>>                                 '64.88',
    >>>                                 '73.88'
    >>>                               ]
    >>>                  }
  lastkeyframetimestamp  73.88
  lasttimestamp          79.6
  metadatacreator        Manitu Group FLV MetaData Injector 2
  metadatadate           1281964633858
  stereo                 1
  videocodecid           7
  videosize              5424234
  width                  400

FINAL UPDATE

I've figured out why the "redefine" approach failed by turning on strict and warnings pragma. Thanks to @Schwern for the reminder :)

Add the following lines of code (copied from the FLV::Util module) first and then do the redefining of the FLV::Tag::parse subroutine.

Readonly::Hash our %TAG_CLASSES => (
   8  => 'FLV::AudioTag',
   9  => 'FLV::VideoTag',
   18 => 'FLV::MetaTag',
);
like image 639
Mike Avatar asked Sep 06 '10 05:09

Mike


1 Answers

Simple? No. But there are some crazy things you can do. Here's some bad ideas.

One of the more obvious is to put a hacked copy of the .pm file into your project, somewhere so its seen before the system version.

Another is similar, but to cut & paste the whole routine into your code and inject it after loading the original.

use FLV::Tag;

no warnings 'redefine';
*FLV::Tag::parse = sub {
    ...copy of FLV::Tag::parse with your edits...
};

You could override die to not die when it sees that message.

BEGIN {
    # In order to override die() later, you must override it at compile time.
    *CORE::GLOBAL::die = sub { return CORE::die(@_) };
}

{
    local *CORE::GLOBAL::die = sub {
        return if $_[0] =~ /^Tag size too small/;
        return CORE::die(@_);
    }

    ...do your thing...
}

You could dump the contents of that subroutine back into Perl, do a string replace on the code, and eval it back.

use Data::Dump::Streamer;
my $original = FLV::Tag->can("parse");
my $code = Dump($original)->Out;
$code =~ s{\Qif ($datasize < 11)\E}{if( 0 )};

no warnings 'redefine';
*FLV::Tag::parse = eval $code;

Or you could walk the opcode tree of that subroutine and change the condition, which I leave as an exercise for someone with more time on their hands.

They are all bad ideas. You're better off just changing the code in place and contacting the author again about it letting them know there's new information.

like image 142
Schwern Avatar answered Oct 03 '22 00:10

Schwern