Using

Scripting

Non-Blocking Execution in Perl

XChat is not a threaded application and is event based, as such, running a process will block XChat until the process is returned, and XChat can continue on. Many people will attempt to get around this issue by using threads, but using threads without careful consideration may cause problems within XChat. As an example, the following code may be fairly straight forward, but has unintended consequences.

badscript.pl
  1. use strict;
  2. use warnings;
  3. use Xchat qw(:all);
  4. use threads;
  5.  
  6. register('stupid error demo', '001', 'bah, base test case');
  7.  
  8. hook_command('demoproblem', sub {
  9.         threads->create(\&the_thread);
  10.         return EAT_XCHAT;
  11. });
  12.  
  13. sub the_thread {
  14.         command('say george');
  15. }
  16.  
  17. hook_print('Your Message', sub {
  18.         if ($_[0][1] =~ m/hi/) {}
  19.         return EAT_NONE;
  20. });

With this code, running /demoproblem will result in the following output:

 Use of uninitialized value in pattern match (m//) at /path/to/your/badscript.pl line 18.

<@testuser> george

This is because the contents of $_[0][1] has been messed with by the thread. While it may work fine in this specific case, imagine if more scripts catch Your Message. The error list may end up being quite long, for a single problem.

Additionally, if you attempt to change the context within the thread, you could very easily cause XChat to crash. This solution is not ideal.

One Solution - threads and threads::shared

One solution to this problem is using Perl's threads::shared module in conjunction with XChat's hook_timer. In this scenario, an array is kept with the results that XChat could deal with, as well as where to run these results. As long as all XChat specific code (such as emit_print, command, and get_list) is not done within the thread, XChat should be fine. If you do need access to things found in get_list, consider getting this information first, passing it to the thread, and only using the thread for process that would otherwise block XChat.

This code requires using newer versions of the threads and threads::shared modules. The newer modules are found starting in Perl 5.10.

noblock-thread-hook_timer.pl
  1. use strict;
  2. use warnings;
  3. use Xchat qw(:all);
  4. use threads;
  5. use threads::shared;
  6.  
  7. register('No-block: thread & timer', '001', 'demonstrate using thread & timer for non blocking');
  8.  
  9. my @result_commands = ();
  10. share(@result_commands);
  11. my $active = 0;
  12. share($active);
  13.  
  14. hook_command('noblock', sub {
  15.         deal_with('george');
  16.         deal_with('bob');
  17.         return EAT_XCHAT;
  18. });
  19.  
  20. # called by threads->create, this is where any commands should be done that
  21. # would otherwise block XChat
  22. # $_[0] will be the context to run the output in
  23. # $_[1] will be the string to deal with
  24. sub thread_command {
  25.         # this sleep does absolutely nothing, just used for demonstration
  26.         sleep 2;
  27.  
  28.         # For simplicity, keep track of the context and input, as thread_command
  29.         # takes two parameters
  30.         my ($context, $input) = @_;
  31.  
  32.         # shared is straight forward in Perl 5.10.1, but this method works in 5.10
  33.         my $result = [];
  34.         share($result);
  35.         $result->[0] = $context;
  36.         # Normally, something more interesting than just 'say' could be done to
  37.         # modify the input, some command could be run and the result used, but
  38.         # a demonstration is a demonstration
  39.         $result->[1] = 'say '.$input;
  40.  
  41.         # add the results and where to send them to the results list
  42.         push(@result_commands, $result);
  43.  
  44.         # Now done with this thread, decrement so hook_timer may stop if need be
  45.         $active--;
  46. }
  47.  
  48. sub deal_with {
  49.         # only really need to start up a thread if $active was 0
  50.         # dealing with it outside of the thread due to the checking against 1 later
  51.         $active++;
  52.  
  53.         # create a thread which will do the provided command, but context
  54.         # is used for where the result should be added
  55.         threads->create(\&thread_command, get_context(), $_[0]);
  56.  
  57.         # if $active is 1, start a hook_timer to deal with the results, as one
  58.         # wouldn't be running
  59.         if ($active == 1) {
  60.                 # start a timer at .1 seconds
  61.                 hook_timer( 100,
  62.                         sub {
  63.                                 # is there any thing waiting in @commands?
  64.                                 while (scalar @result_commands) {
  65.                                         # remove first result, set context to it, and run command
  66.                                         my $command = shift @result_commands;
  67.                                         set_context($command->[0]);
  68.                                         command($command->[1]);
  69.                                 }
  70.                                 # do we keep it or remove? if $active 0, we aren't expecting any
  71.                                 # more data, so we can remove it, otherwise, keep
  72.                                 if ($active == 0) {
  73.                                         return REMOVE;
  74.                                 }
  75.                                 else {
  76.                                         return KEEP;
  77.                                 }
  78.                         }
  79.                 );
  80.         }
  81. }

In this case, you can run /noblock multiple times in a row, on different contexts, and it will print the lines in the correct channel. Additionally, the problem listed at the top of the page with the "Use of uninitialized value" will not show up when hooking Your Message.

What does the code do? Parameters are passed to deal_with, which then increments a counter for lines that are dealt with. It then calls the thread creation, passing these parameters to the thread, and then decides if a timer is needed to look for results. This is determined by checking if $active is 1, which would happen on the first instance. If $active is more than 1, a hook_timer already would be running (in theory). When a thread completes, it decrements the shared variable. When the timer completes its execution that time, if the value of $active is 0, all of the active threads have completed, and XChat doesn't need to look for anymore commands to send, otherwise it continues.

Note that this code is just an example, and fairly specific to the task at hand. Normally code won't need to simply say two lines and go on. The 2 seconds of sleep would normally hang XChat until execution, but XChat is able to handle the sleep, and still allow messages to come in, and input to be added. Instead, running some other code that would block XChat execution (such as waiting for a process in back ticks or making a TCP connection) would be done inside of thread_command. Any command XChat should run (for display, or messaging a user) would be done in $result->[1].

Other solutions - Using CPAN modules

As XChat is event based, perhaps a more accurate method would be to use CPAN modules such as POE or EV which are actually designed for event based programming. In the solution I gave above, XChat uses hook_timer to check if a message is available every .1 seconds. While this may be satisfactory in many cases, it isn't exactly what is required. Having a module specifically look for an event is much more accurate.



Print - Recent Changes - Search
Page last modified on March 10, 2010, at 04:45 AM