SuperCruncher has asked for the wisdom of the Perl Monks concerning the following question:

I'm about to start making a simple program in Perl. All it does is monitor an arbitrary number of URLs for changes (I mean changes in the pages pointed to by URLs, not changes in the actual URL itself by the way) and take certain action if a change is detected.

This sounds simple but the catch is that if a URL changes, the action to be taken will be completely different from URL to URL.

So I thought a relatively "clean" way of implementing this was to have a set of "plugin" files (Perl code) like foo.plugin, bar.plugin etc. These will specify a URL to check, and a 'do' function that will determine what is to be done if the URL has changed.

So, my questions:

  1. Is it OK to use eval like:
    foreach my $plugin (glob("*.plugin")) { do $plugin; }
  2. Can I check that the plugin is 'legal' (implements the correct functions, variables and so on) by using exists and then by remembering to use undef before I eval a new plugin? For example:
    foreach my $plugin (glob("*.plugin")) { do $plugin; if (!exists(&plugin::do_stuff) or !exists($url)) { die "Invalid plugin: function(s) not implemented.\n"; } # Call the various plugin 'methods' plugin::do_stuff(); undef (&plugin::do_stuff); }
Thanks in advance.

Replies are listed 'Best First'.
Re: Using eval for
by chip (Curate) on Dec 28, 2001 at 02:00 UTC
    Well, you could put it in its own package and then clear out the package....

    for my $plugin (...) { eval 'package Plugin; do $plugin'; # check for $@ here eval { no strict refs; &{'Plugin::func'}() } if ($@) { ...error.handling... } %Plugin:: = () }

    The soft reference is required to defeat the compiler's attempt to hold a pointer to *Plugin::func.

        -- Chip Salzenberg, Free-Floating Agent of Chaos

      Thanks for your reply. An eval inside an eval... strange. Let me just make sure I've got my head round that code:

      eval 'package Plugin; do $plugin'; - this creates a package which the 'plugin' is evaled into.

      eval { no strict refs; &{'Plugin::func'}() } - disables strict refs for the enclosing' block', then calls func in the Plugin package?

      %Plugin:: = () - I've never seen that before. I didn't know a package could be viewed in a sort of 'hash context'. So that sort of 'resets' the package to undef?

      BTW, the title of the node should have been "Using eval for 'plugins'" but Everything seems to have removed it in the preview stage (maybe because it didn't escape single quotes properly?).

        Right on all three counts. Good analysis.

        WRT the eval inside the eval: If you don't like that, you can read the file contents and then:

        eval 'package Plugin;' . $file_contents

        I just did what I did to save the trouble of open/read/close. On the other hand, the eval 'do' will hide your lexical vars from the plugin while the eval above will reveal them. Bug or feature? YOU be the judge.

        WRT the packages as hashes: Each element of %pkgname:: is a GLOB. You can do your own glob aliasing that way, and lots of other evil stuff. But beware the bandersnatch, er, beware the compiler's tendency to grab references to GLOBs and not let go even when they've been removed from their original places.

            -- Chip Salzenberg, Free-Floating Agent of Chaos

Re: Using eval for
by dmmiller2k (Chaplain) on Dec 28, 2001 at 02:15 UTC

    question 1: simply, yes.

    question 2: for what you are trying to do, exists is not the way to go. Also, you are using plugin:: as the name of the package, but for this to work, you would have to declare, package plugin; in all of your plugin files. Unloading, too, is more complicated than simply undef &plugin::do_stuff;.

    You probably want each plugin to be a perl object, with all such objects derived from a common base class, rather like this:

    # in Plugin.pm: package Plugin; # the common base class sub new { my $type = shift; my $class = ref($type) || $type; return bless {}, $class; } # the public entry point, calls a private entry point in each derived +class, _do_stuff(), to actually do the work sub do_stuff { my $self = shift; die 'Illegal plugin function' unless ($self->can('_do_stuff')); # polymorphic method call return $self->_do_stuff(); } # in Whatever.pm: package Whatever; use base qw( Plugin ); # subclass of Plugin sub new { my $type = shift; my $class = ref($type) || $type; return bless $class->SUPER::new, $class; } sub _do_stuff { # whatever this plugin does goes here } # etc.

    Then, in your main program, you would have code similar to this:

    use Basename; for my $plugin_file ( ... ) { my $plugin_pkg = basename $pluginfile, '.pm'; eval { use $plugin_pkg; my $plugin_object = new $plugin_pkg; $plugin_object->do_stuff(); } if ($@) { # handle the error } }

    This way there'd be no name clashes nor would you necessarily have to unload a plugin, once loaded.

    dmm

    You can give a man a fish and feed him for a day ...
    Or, you can
    teach him to fish and feed him for a lifetime
Re: Using eval for
by rob_au (Abbot) on Dec 28, 2001 at 02:20 UTC
    I've actually already written a node on this here - Sorry, I just wanted to see how merlyn felt when he could do this :-)

    On to your specific questions, in addition to chip's excellent comments, the end solution will come down to how much freedom you really want to give to these plug-ins. Another solution which you may want to look at would be to use Safe to limit allowed operations. This module allows the creation of compartments in which unsafe Perl code can be evaluated with a new namespace and operator mask - The advantage that this offers is that the evaluated code can only do what you want it to do and cannot change variables outside its own scope (unless explicitly shared).

     

    perl -e 's&&rob@cowsnet.com.au&&&split/[@.]/&&s&.com.&_&&&print'

      The plugins are going to be written by myself only, so 'trust' isn't really a problem - so I don't need to use Safe.

      I read your solution and it was interesting to see another approach. However, it doesn't have the same sort of "modularity" that I'd like: adding another site would require editing of the main source code file (I think!).