Emulating switch or if/elsif/else with events.

A while ago I wrote a simple e-mail server that implemented the pop3 protocol. Pop3 has three main stages:

The third stage is the most interesting. Client requests are a lot like shell commands. They consist of a command name and zero or more parameters, all separated by spaces. For example, this is the command to delete the first message in a pop3 mailbox:

  DELE 1

In the server, I used POE events rather than a hash or large if/elsif/else tree to dispatch pop3 commands to their proper handlers. Raw client input was handled by a function that looked similar to this.

sub request_input {
  my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
  my ($command, $parameters) = split ' ', $input, 2;
  $kernel->yield("request_command_" . lc($command), $parameters);
}

That function splits the client request into a command and optional parameters. Then it fires a new event based on the command that was entered. The new event is prefixed with "request_command_" so that user input can't trigger private handlers (like _stop). Pop3 commands are case-insensitive, so the client's command is folded to lowercase.

In the pop3 server, every valid command had its own event handler. For example, there was a function to handle the request_command_dele event. POE's normal event dispatching ensured that each command was handled by the proper code.

Unrecognized commands, however, had no handlers. Instead, POE routed them to the _default handler. In the pop3 server, _default's handler responded with an error message:

sub _default {
  my ($heap, $caught_event) = @_[HEAP, ARG0];
  if ($caught_event =~ /^request_command_/) {
    $heap->{readwrite_wheel}->put("-ERR unknown command");
  }
  return 0;
}

That handler makes sure it's catching an invalid command, and not a cabbage or something. The handler emits an error message if it was triggered by bad input. Finally, because _default can catch signals, it returns zero to ensure they are not handled.

Adding and removing command handlers at runtime.

POE::Kernel's state() method can add, update, and remove handlers from a running Session. This can be used to change the commands that a session will accept.

For example, adding a change_command handler lets a session modify itself.

sub change_command {
  my ($kernel, $heap, $command, $coderef) = @_[KERNEL, HEAP, ARG0, ARG1];
  $kernel->state("request_command_$command", $coderef);
}

The state() call in this function will add or update an event handler if $coderef is defined. If $coderef isn't defined, the handler (and thus the command) will be removed.

For example, it may be useful to enable a "shutdown" command if a user has authenticated themselves as an administrator:

$kernel->yield(change_command => shutdown => \&handle_command_shutdown);

The command may be rescinded later, perhaps automatically after a period of inactivity:

$kernel->yield(change_command => request_command_shutdown => undef);

Switching amongst groups of command handlers.

Wheel::ReadWrite's event() method will change the events a wheel generates. It's often used to switch the function that's called to handle input. Combined with the switch emulation above, it's possible to implement protocols that have distinct stages where different commands are accepted.

For example, this input handler fires command events for the first pop3 stage. It emits events that begin with "user_command_", so input in this stage will never trigger handlers for other stages.

sub user_input {
  my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
  my ($command, $parameters) = split ' ', $input, 2;
  $kernel->yield("user_command_" . lc($command), $parameters);
}

Likewise, this handler fires command events for the second pop3 stage.

sub password_input {
  my ($kernel, $heap, $input) = @_[KERNEL, HEAP, ARG0];
  my ($command, $parameters) = split ' ', $input, 2;
  $kernel->yield("password_command_" . lc($command), $parameters);
}

Finally, command handlers can switch the server from one stage to another by calling ReadWrite's event() method.

$heap->{readwrite_wheel}->event(InputEvent => "user_input");
$heap->{readwrite_wheel}->event(InputEvent => "pass_input");
$heap->{readwrite_wheel}->event(InputEvent => "request_input");