pbot/PBot/Timer.pm

370 lines
10 KiB
Perl
Raw Normal View History

# File: Timer.pm
# Author: pragma_
#
2020-03-06 22:21:44 +01:00
# Purpose: Provides functionality to register subroutines/events to be invoked
# at a future time, optionally recurring.
#
2020-03-06 22:21:44 +01:00
# If no subroutines/events are registered/enqueued, the default on_tick()
# method, which can be overridden, is invoked.
#
# Uses own internal seconds counter and relative-intervals to avoid
# timeout desyncs due to system clock changes.
#
# Note: Uses ALARM signal.
License project under MPL2 This patch adds the file LICENSE which is the verbatim copy of the Mozilla Public License Version 2.0 as retreived from https://www.mozilla.org/media/MPL/2.0/index.815ca599c9df.txt on 2017-03-05. This patch also places license headers for the MPL2 type A variant of the license header in the following files: PBot/AntiFlood.pm PBot/BanTracker.pm PBot/BlackList.pm PBot/BotAdminCommands.pm PBot/BotAdmins.pm PBot/ChanOpCommands.pm PBot/ChanOps.pm PBot/Channels.pm PBot/Commands.pm PBot/DualIndexHashObject.pm PBot/EventDispatcher.pm PBot/FactoidCommands.pm PBot/FactoidModuleLauncher.pm PBot/Factoids.pm PBot/HashObject.pm PBot/IRCHandlers.pm PBot/IgnoreList.pm PBot/IgnoreListCommands.pm PBot/Interpreter.pm PBot/LagChecker.pm PBot/Logger.pm PBot/MessageHistory.pm PBot/MessageHistory_SQLite.pm PBot/NickList.pm PBot/PBot.pm PBot/Plugins.pm PBot/Plugins/AntiAway.pm PBot/Plugins/AntiKickAutoRejoin.pm PBot/Plugins/AntiRepeat.pm PBot/Plugins/AntiTwitter.pm PBot/Plugins/AutoRejoin.pm PBot/Plugins/Counter.pm PBot/Plugins/Quotegrabs.pm PBot/Plugins/Quotegrabs/Quotegrabs_Hashtable.pm PBot/Plugins/Quotegrabs/Quotegrabs_SQLite.pm PBot/Plugins/UrlTitles.pm PBot/Plugins/_Example.pm PBot/Refresher.pm PBot/Registerable.pm PBot/Registry.pm PBot/RegistryCommands.pm PBot/SQLiteLogger.pm PBot/SQLiteLoggerLayer.pm PBot/SelectHandler.pm PBot/StdinReader.pm PBot/Timer.pm PBot/Utils/ParseDate.pm PBot/VERSION.pm build/update-version.pl modules/acronym.pl modules/ago.pl modules/c11std.pl modules/c2english.pl modules/c2english/CGrammar.pm modules/c2english/c2eng.pl modules/c99std.pl modules/cdecl.pl modules/cfaq.pl modules/cjeopardy/IRCColors.pm modules/cjeopardy/QStatskeeper.pm modules/cjeopardy/Scorekeeper.pm modules/cjeopardy/cjeopardy.pl modules/cjeopardy/cjeopardy_answer.pl modules/cjeopardy/cjeopardy_filter.pl modules/cjeopardy/cjeopardy_hint.pl modules/cjeopardy/cjeopardy_qstats.pl modules/cjeopardy/cjeopardy_scores.pl modules/cjeopardy/cjeopardy_show.pl modules/codepad.pl modules/compiler_block.pl modules/compiler_client.pl modules/compiler_vm/Diff.pm modules/compiler_vm/cc modules/compiler_vm/compiler_client.pl modules/compiler_vm/compiler_server.pl modules/compiler_vm/compiler_server_vbox_win32.pl modules/compiler_vm/compiler_server_watchdog.pl modules/compiler_vm/compiler_vm_client.pl modules/compiler_vm/compiler_vm_server.pl modules/compiler_vm/compiler_watchdog.pl modules/compiler_vm/languages/_c_base.pm modules/compiler_vm/languages/_default.pm modules/compiler_vm/languages/bash.pm modules/compiler_vm/languages/bc.pm modules/compiler_vm/languages/bf.pm modules/compiler_vm/languages/c11.pm modules/compiler_vm/languages/c89.pm modules/compiler_vm/languages/c99.pm modules/compiler_vm/languages/clang.pm modules/compiler_vm/languages/clang11.pm modules/compiler_vm/languages/clang89.pm modules/compiler_vm/languages/clang99.pm modules/compiler_vm/languages/clangpp.pm modules/compiler_vm/languages/clisp.pm modules/compiler_vm/languages/cpp.pm modules/compiler_vm/languages/freebasic.pm modules/compiler_vm/languages/go.pm modules/compiler_vm/languages/haskell.pm modules/compiler_vm/languages/java.pm modules/compiler_vm/languages/javascript.pm modules/compiler_vm/languages/ksh.pm modules/compiler_vm/languages/lua.pm modules/compiler_vm/languages/perl.pm modules/compiler_vm/languages/python.pm modules/compiler_vm/languages/python3.pm modules/compiler_vm/languages/qbasic.pm modules/compiler_vm/languages/scheme.pm modules/compiler_vm/languages/server/_c_base.pm modules/compiler_vm/languages/server/_default.pm modules/compiler_vm/languages/server/c11.pm modules/compiler_vm/languages/server/c89.pm modules/compiler_vm/languages/server/c99.pm modules/compiler_vm/languages/server/clang.pm modules/compiler_vm/languages/server/clang11.pm modules/compiler_vm/languages/server/clang89.pm modules/compiler_vm/languages/server/clang99.pm modules/compiler_vm/languages/server/cpp.pm modules/compiler_vm/languages/server/freebasic.pm modules/compiler_vm/languages/server/haskell.pm modules/compiler_vm/languages/server/java.pm modules/compiler_vm/languages/server/qbasic.pm modules/compiler_vm/languages/server/tendra.pm modules/compiler_vm/languages/sh.pm modules/compiler_vm/languages/tendra.pm modules/compliment modules/cstd.pl modules/define.pl modules/dice_roll.pl modules/excuse.sh modules/expand_macros.pl modules/fnord.pl modules/funnyish_quote.pl modules/g.pl modules/gdefine.pl modules/gen_cfacts.pl modules/gencstd.pl modules/get_title.pl modules/getcfact.pl modules/google.pl modules/gspy.pl modules/gtop10.pl modules/gtop15.pl modules/headlines.pl modules/horoscope modules/horrorscope modules/ideone.pl modules/insult.pl modules/love_quote.pl modules/man.pl modules/map.pl modules/math.pl modules/prototype.pl modules/qalc.pl modules/random_quote.pl modules/seen.pl modules/urban modules/weather.pl modules/wikipedia.pl pbot.pl pbot.sh It is highly recommended that this list of files is reviewed to ensure that all files are the copyright of the sole maintainer of the repository. If any files with license headers contain the intellectual property of anyone else, it is recommended that a request is made to revise this patch or that the explicit permission of the co-author is gained to allow for the license of the work to be changed. I (Tomasz Kramkowski), the contributor, take no responsibility for any legal action taken against the maintainer of this repository for incorrectly claiming copyright to any work not owned by the maintainer of this repository.
2017-03-05 22:33:31 +01:00
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package PBot::Timer;
use parent 'PBot::Class';
use warnings; use strict;
2019-07-11 03:40:53 +02:00
use feature 'unicode_strings';
2020-03-06 22:21:44 +01:00
use Time::Duration qw/concise duration/;
our $seconds ||= 0;
our $waitfor ||= 1;
our @timer_funcs;
2020-03-06 22:21:44 +01:00
# alarm signal handler (poor-man's timer)
2019-06-26 18:34:19 +02:00
$SIG{ALRM} = sub {
$seconds += $waitfor;
2020-02-15 23:38:32 +01:00
foreach my $func (@timer_funcs) { &$func; }
};
sub initialize {
2020-02-15 23:38:32 +01:00
my ($self, %conf) = @_;
2020-03-06 22:21:44 +01:00
my $timeout = $conf{timeout} // 10;
$self->{name} = $conf{name} // "Unnamed ${timeout}s Timer";
$self->{enabled} = 0;
$self->{event_queue} = [];
$self->{last} = $seconds;
$self->{timeout} = $timeout;
$self->{pbot}->{commands}->register(sub { $self->cmd_eventqueue(@_) }, 'eventqueue', 1);
$self->{pbot}->{capabilities}->add('admin', 'can-eventqueue', 1);
2020-03-06 22:21:44 +01:00
$self->{timer_func} = sub { $self->on_tick_handler(@_) };
}
sub cmd_eventqueue {
my ($self, $context) = @_;
2020-03-06 22:21:44 +01:00
my $usage = "Usage: eventqueue list [filter regex] | add <relative time> <command> [-repeat] | remove <event>";
2020-03-06 22:21:44 +01:00
2020-05-02 05:59:51 +02:00
my $command = $self->{pbot}->{interpreter}->shift_arg($context->{arglist});
if (not defined $command) {
return $usage;
}
if ($command eq 'list') {
return "No events queued." if not @{$self->{event_queue}};
my $result = eval {
my $text = "Queued events:\n";
2020-05-02 05:59:51 +02:00
my ($regex) = $self->{pbot}->{interpreter}->shift_arg($context->{arglist});
my $i = 0;
2020-04-06 05:33:14 +02:00
my $events = 0;
foreach my $event (@{$self->{event_queue}}) {
$i++;
if ($regex) {
2020-04-06 05:33:14 +02:00
next unless $event->{id} =~ /$regex/i;
}
2020-04-06 05:33:14 +02:00
$events++;
my $duration = concise duration $event->{timeout} - $seconds;
$text .= " $i) in $duration: $event->{id}";
$text .= ' [R]' if $event->{repeating};
$text .= ";\n";
}
2020-03-06 22:21:44 +01:00
2020-04-06 05:33:14 +02:00
return "No events found." if $events == 0;
return $text;
};
if ($@) {
my $error = $@;
$error =~ s/ at PBot.*//;
return "Bad regex: $error";
}
return $result;
}
if ($command eq 'add') {
2020-05-02 05:59:51 +02:00
my ($duration, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
return "Usage: eventqueue add <relative time> <command> [-repeat]" if not defined $duration or not defined $command;
my ($delay, $error) = $self->{pbot}->{parsedate}->parsedate($duration);
return $error if defined $error;
my $repeating = 0;
$repeating = 1 if $command =~ s/^-repeat\s+|\s+-repeat$//g;
my $cmd = {
nick => $context->{nick},
user => $context->{user},
host => $context->{host},
command => $command,
};
$self->{pbot}->{interpreter}->add_to_command_queue($context->{from}, $cmd, $delay, $repeating);
return "Command added to event queue.";
2020-03-06 22:21:44 +01:00
}
if ($command eq 'remove') {
2020-05-02 05:59:51 +02:00
my ($regex) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 1);
return "Usage: eventqueue remove <event>" if not defined $regex;
$regex =~ s/\*/.*?/g;
return $self->dequeue_event($regex);
}
return "Unknown command '$command'. $usage";
}
sub start {
2020-02-15 23:38:32 +01:00
my $self = shift;
$self->{enabled} = 1;
push @timer_funcs, $self->{timer_func};
2020-03-06 22:21:44 +01:00
alarm 1;
}
sub stop {
2020-02-15 23:38:32 +01:00
my $self = shift;
$self->{enabled} = 0;
@timer_funcs = grep { $_ != $self->{timer_func} } @timer_funcs;
}
2020-03-06 22:21:44 +01:00
sub find_enqueue_position {
my ($self, $value) = @_;
2020-02-15 23:38:32 +01:00
2020-03-06 22:21:44 +01:00
return 0 if not @{$self->{event_queue}};
2020-02-15 23:38:32 +01:00
2020-03-06 22:21:44 +01:00
if ($value < $self->{event_queue}->[0]->{timeout}) {
return 0;
}
2020-02-15 23:38:32 +01:00
2020-03-06 22:21:44 +01:00
if ($value > $self->{event_queue}->[@{$self->{event_queue}} - 1]->{timeout}) {
return scalar @{$self->{event_queue}};
}
my $lo = 0;
my $hi = scalar @{$self->{event_queue}} - 1;
while ($lo <= $hi) {
my $mid = int (($hi + $lo) / 2);
if ($value < $self->{event_queue}->[$mid]->{timeout}) {
$hi = $mid - 1;
} elsif ($value > $self->{event_queue}->[$mid]->{timeout}) {
$lo = $mid + 1;
} else {
while ($mid < @{$self->{event_queue}} and $self->{event_queue}->[$mid]->{timeout} == $value) {
$mid++;
}
2020-03-06 22:21:44 +01:00
return $mid;
}
}
2020-03-06 22:21:44 +01:00
return $lo;
}
sub replace_subref_or_enqueue_event {
my ($self, $ref, $interval, $id, $repeating) = @_;
my @events = grep { $_->{id} eq $id } @{$self->{event_queue}};
if (not @events) {
$self->enqueue_event($ref, $interval, $id, $repeating);
return;
}
foreach my $event (@events) {
$event->{subref} = $ref;
}
}
sub replace_or_enqueue_event {
my ($self, $ref, $interval, $id, $repeating) = @_;
$self->dequeue_event($id) if $self->exists($id);
$self->enqueue_event($ref, $interval, $id, $repeating);
}
sub enqueue_event_unless_exists {
my ($self, $ref, $interval, $id, $repeating) = @_;
return if $self->exists($id);
$self->enqueue_event($ref, $interval, $id, $repeating);
}
2020-03-06 22:21:44 +01:00
sub enqueue_event {
my ($self, $ref, $interval, $id, $repeating) = @_;
2020-03-06 22:21:44 +01:00
$id ||= 'anonymous event';
$repeating ||= 0;
2020-03-06 22:21:44 +01:00
my $event = {
id => $id,
subref => $ref,
interval => $interval,
timeout => $seconds + $interval,
repeating => $repeating,
};
2020-03-06 22:21:44 +01:00
my $i = $self->find_enqueue_position($event->{timeout});
splice @{$self->{event_queue}}, $i, 0, $event;
if ($interval < $waitfor) {
$self->waitfor($interval);
}
2020-03-06 22:21:44 +01:00
my $debug = $self->{pbot}->{registry}->get_value('timer', 'debug') // 0;
if ($debug > 1) {
$self->{pbot}->{logger}->log("Enqueued new timer event $id at position $i: timeout=$event->{timeout} interval=$interval repeating=$repeating\n");
}
}
2020-03-06 22:21:44 +01:00
sub dequeue_event {
my ($self, $id, $execute) = @_;
2020-03-06 22:21:44 +01:00
my $result = eval {
2020-03-06 22:21:44 +01:00
$id = quotemeta $id;
$id =~ s/\\\.\\\*\\\?/.*?/g;
$id =~ s/\\\.\\\*/.*/g;
2020-04-06 05:33:14 +02:00
my $regex = qr/^$id$/i;
my $count = @{$self->{event_queue}};
my @removed = grep { $_->{id} =~ /$regex/i; } @{$self->{event_queue}};
2020-03-06 22:21:44 +01:00
@{$self->{event_queue}} = grep { $_->{id} !~ /$regex/i; } @{$self->{event_queue}};
$count -= @{$self->{event_queue}};
if ($execute) {
foreach my $event (@removed) {
$event->{subref}->($event);
}
}
return "No matching events." if not $count;
return "Removed $count event" . ($count == 1 ? '' : 's') . ': ' . join(', ', map { $_->{id} } @removed);
2020-03-06 22:21:44 +01:00
};
if ($@) {
my $error = $@;
$self->{pbot}->{logger}->log("Error in dequeue_event: $error\n");
$error =~ s/ at PBot.*//;
return "$error";
2020-03-06 22:21:44 +01:00
}
return $result;
2020-03-06 22:21:44 +01:00
}
sub execute_and_dequeue_event {
my ($self, $id) = @_;
return $self->dequeue_event($id, 1);
}
2020-03-06 22:21:44 +01:00
sub register {
my ($self, $ref, $interval, $id) = @_;
$self->enqueue_event($ref, $interval, $id, 1);
}
sub unregister {
2020-02-15 23:38:32 +01:00
my ($self, $id) = @_;
2020-03-06 22:21:44 +01:00
$self->dequeue_event($id);
}
sub exists {
my ($self, $id) = @_;
return scalar grep { $_->{id} eq $id } @{$self->{event_queue}};
}
sub update_repeating {
my ($self, $id, $repeating) = @_;
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
if ($self->{event_queue}->[$i]->{id} eq $id) {
$self->{event_queue}->[$i]->{repeating} = $repeating;
last;
}
}
}
sub update_interval {
2020-03-06 22:21:44 +01:00
my ($self, $id, $interval, $dont_enqueue) = @_;
2020-03-06 22:21:44 +01:00
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
if ($self->{event_queue}->[$i]->{id} eq $id) {
if ($dont_enqueue) {
$self->{event_queue}->[$i]->{interval} = $interval;
} else {
my $event = splice(@{$self->{event_queue}}, $i, 1);
$self->enqueue_event($event->{subref}, $interval, $id, $event->{repeating});
}
2020-02-15 23:38:32 +01:00
last;
}
}
}
sub waitfor {
my ($self, $duration) = @_;
$duration = 1 if $duration < 1;
alarm $duration;
$waitfor = $duration;
}
2020-03-06 22:21:44 +01:00
sub on_tick_handler {
my ($self) = @_;
return if not $self->{enabled};
my $debug = $self->{pbot}->{registry}->get_value('timer', 'debug') // 0;
$self->{pbot}->{logger}->log("$self->{name} tick $seconds\n") if $debug;
if (@{$self->{event_queue}}) {
my $next_tick = 1;
2020-03-06 22:21:44 +01:00
my @enqueue = ();
for (my $i = 0; $i < @{$self->{event_queue}}; $i++) {
if ($seconds >= $self->{event_queue}->[$i]->{timeout}) {
my $event = $self->{event_queue}->[$i];
$self->{pbot}->{logger}->log("Processing timer event $i: $event->{id}\n") if $debug > 1;
$event->{subref}->($event);
2020-03-06 22:21:44 +01:00
splice @{$self->{event_queue}}, $i--, 1;
push @enqueue, $event if $event->{repeating};
} else {
if ($debug > 2) {
$self->{pbot}->{logger}->log("Event not ready yet: $self->{event_queue}->[$i]->{id} (timeout=$self->{event_queue}->[$i]->{timeout})\n");
}
$next_tick = $self->{event_queue}->[$i]->{timeout} - $seconds;
2020-03-06 22:21:44 +01:00
last;
}
}
$self->waitfor($next_tick);
2020-03-06 22:21:44 +01:00
foreach my $event (@enqueue) {
$self->enqueue_event($event->{subref}, $event->{interval}, $event->{id}, 1);
}
} else {
# no queued events, call default overridable on_tick() method if timeout has elapsed
if ($seconds - $self->{last} >= $self->{timeout}) {
$self->{last} = $seconds;
$self->on_tick;
}
$self->waitfor($self->{timeout} - $seconds - $self->{last});
2020-03-06 22:21:44 +01:00
}
}
# default overridable handler, executed whenever timeout is triggered
sub on_tick {
my ($self) = @_;
$self->{pbot}->{logger}->log("Tick! $self->{name} $self->{timeout} $self->{last} $seconds\n");
}
1;