Erebot  latest
A modular IRC bot for PHP 7.0+
Core.php
1 <?php
2 /*
3  This file is part of Erebot, a modular IRC bot written in PHP.
4 
5  Copyright © 2010 François Poirotte
6 
7  Erebot is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11 
12  Erebot is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  GNU General Public License for more details.
16 
17  You should have received a copy of the GNU General Public License
18  along with Erebot. If not, see <http://www.gnu.org/licenses/>.
19 */
20 
21 namespace Erebot;
22 
23 /*
25 // Especially, we don't want to depend on it too much.
26 include_once(__DIR__.'/orm/Doctrine.php');
27 
28 // Initialize Doctrine.
29 spl_autoload_register(array('Doctrine', 'autoload'));
30 $manager = Doctrine_Manager::getInstance();
31 $manager->setAttribute(
32  Doctrine_Core::ATTR_VALIDATE,
33  Doctrine_Core::VALIDATE_ALL
34 );
35 $manager->setAttribute(
36  Doctrine_Core::ATTR_EXPORT,
37  Doctrine_Core::EXPORT_ALL
38 );
39 $manager->setAttribute(
40  Doctrine_Core::ATTR_MODEL_LOADING,
41  Doctrine_Core::MODEL_LOADING_CONSERVATIVE
42 );
43 unset($manager);
44 */
45 
59 class Core implements \Erebot\Interfaces\Core
60 {
62  protected $connections;
63 
65  protected $timers;
66 
68  protected $mainCfg;
69 
71  protected $running;
72 
74  protected $translator;
75 
86  public function __construct(
87  \Erebot\Interfaces\Config\Main $config,
88  \Erebot\Intl\TranslatorInterface $translator
89  ) {
90  $this->connections =
91  $this->timers = array();
92  $this->running = false;
93  $this->mainCfg = $config;
94  $this->translator = $translator;
95 
96  // If pcntl_signal is not supported,
97  // the bot won't be able to stop!
98  // See start() for reasons why (hint: infinite loop).
99  if (function_exists('pcntl_signal')) {
100  /* These ought to be the most common signals
101  * we expect to receive, eventually. */
102  $signals = array(
103  SIGINT,
104  SIGQUIT,
105  SIGALRM,
106  SIGTERM,
107  );
108 
109  foreach ($signals as $signal) {
110  pcntl_signal($signal, array($this, 'handleSignal'), true);
111  }
112 
113  pcntl_signal(SIGHUP, array($this, 'handleSIGHUP'), true);
114  }
115  }
116 
120  public function __destruct()
121  {
122  $this->stop();
123  }
124 
128  public function __clone()
129  {
130  throw new \Exception("Cloning forbidden!");
131  }
132 
134  public function getConnections()
135  {
136  return $this->connections;
137  }
138 
153  protected function realStart(\Erebot\Interfaces\ConnectionFactory $factory)
154  {
155  $logger = \Plop\Plop::getInstance();
156  $logger->info($this->gettext('Erebot is starting'));
157 
158  // This is changed by handleSignal()
159  // when the bot should stop.
160  $this->running = time();
161 
162  $this->createConnections($factory, $this->mainCfg);
163 
164  // Main loop
165  while ($this->running) {
166  pcntl_signal_dispatch();
167 
168  $read = $write = $except = array();
169  $actives = array('connections' => array(), 'timers' => array());
170 
171  if ($this->connections === null) {
172  break;
173  }
174 
175  // Find out connections in need of some handling.
176  foreach ($this->connections as $index => $connection) {
177  $socket = $connection->getSocket();
178 
179  if ($connection instanceof \Erebot\Interfaces\SendingConnection &&
180  $connection->getIO()->inWriteQueue()) {
181  $write[] = $socket;
182  }
183 
184  if ($connection instanceof \Erebot\Interfaces\ReceivingConnection) {
185  $read[] = $socket;
186  }
187 
188  $except[] = $socket;
189  $actives['connections'][$index] = $socket;
190  }
191 
192  // Find out timed out timers.
193  foreach ($this->timers as $index => $timer) {
194  $stream = $timer->getStream();
195 
196  $read[] = $stream;
197  $actives['timers'][$index] = $stream;
198  }
199 
200  // No activity.
201  if (count($read) + count($write) + count($except) == 0) {
202  $logger->debug(
203  $this->gettext('No more connections to handle, leaving...')
204  );
205  return $this->stop();
206  }
207 
208  try {
209  // Block until there is activity. Since timers
210  // are treated as streams too, we will also wake
211  // up whenever a timer fires.
212  // Throws a warning under PHP 5.2 when a signal is received.
213  $nb = @stream_select($read, $write, $except, null);
214  } catch (\Erebot\ErrorReportingException $e) {
215  if ($this->running) {
216  $logger->exception($this->gettext('Got exception'), $e);
217  } else {
218  /* If the bot is not running anymore,
219  * this probably means we received a signal.
220  * We continue to the next iteration,
221  * which will make the bot exit properly. */
222  continue;
223  }
224  }
225 
226  // Handle exceptional (out-of-band) data.
227  // It seems that PHP will mark signal interruptions with OOB data.
228  // We simply do a new iteration, because the signal dispatcher
229  // will be called right away if needed.
230  // For older versions, we use declare(ticks) (see Patches.php).
231  if (count($except)) {
232  continue;
233  }
234 
235  // Handle read-ready "sockets"
236  foreach ($read as $socket) {
237  do {
238  // Is it a connection?
239  $index = array_search(
240  $socket,
241  $actives['connections'],
242  true
243  );
244  if ($index !== false) {
245  // Read as much data from the connection as possible.
246  try {
247  $this->connections[$index]->read();
248  } catch (\Erebot\ConnectionFailureException $e) {
249  $logger->info(
250  $this->gettext(
251  'Connection failed, removing it '.
252  'from the pool'
253  )
254  );
255  $this->removeConnection(
256  $this->connections[$index]
257  );
258  }
259  break;
260  }
261 
262  // Is it a timer?
263  $index = array_search($socket, $actives['timers'], true);
264  if ($index !== false) {
265  $timer = $this->timers[$index];
266  // During shutdown, weird things happen to timers,
267  // including magical disappearance.
268  if (!is_object($timer)) {
269  unset($this->timers[$index]);
270  break;
271  }
272  $restart = $timer->activate();
273 
274  // Maybe the callback function
275  // removed the timer already.
276  if (!isset($this->timers[$index])) {
277  break;
278  }
279 
280  // Otherwise, restart or remove it as necessary.
281  if ($restart === true || $timer->getRepetition()) {
282  $timer->reset();
283  } else {
284  unset($this->timers[$index]);
285  }
286 
287  break;
288  }
289 
290  // No, it's superman!! Let's do nothing then...
291  } while (0);
292  }
293 
294  // Take care of incoming data waiting for processing.
295  if (is_array($this->connections)) {
296  foreach ($this->connections as $connection) {
297  if ($connection instanceof \Erebot\Interfaces\ReceivingConnection) {
298  $connection->process();
299  }
300  }
301  }
302 
303  // Handle write-ready sockets (flush outgoing data).
304  foreach ($write as $socket) {
305  $index = array_search($socket, $actives['connections']);
306  if ($index !== false && isset($this->connections[$index]) &&
307  $this->connections[$index] instanceof \Erebot\Interfaces\SendingConnection) {
308  $this->connections[$index]->write();
309  }
310  }
311  }
312  }
313 
314  public function start(\Erebot\Interfaces\ConnectionFactory $factory)
315  {
316  try {
317  return $this->realStart($factory);
318  } catch (\Erebot\StopException $e) {
319  // This exception is raised by Erebot::handleSignal()
320  // whenever one of SIGINT, SIGQUIT, SIGALRM, or SIGTERM
321  // is received and indicates the bot is stopping.
322  }
323  }
324 
325  public function stop()
326  {
327  $logger = \Plop\Plop::getInstance();
328 
329  if (!$this->running) {
330  return;
331  }
332 
333  foreach ($this->connections as $connection) {
334  if ($connection instanceof \Erebot\Interfaces\EventDispatcher) {
335  $eventsProducer = $connection->getEventsProducer();
336  $connection->dispatch($eventsProducer->makeEvent('!ExitEvent'));
337  }
338  }
339 
340  $logger->info($this->gettext('Erebot has stopped'));
341  unset(
342  $this->timers,
343  $this->connections,
344  $this->mainCfg
345  );
346 
347  $this->running = false;
348  $this->connections =
349  $this->timers =
350  $this->mainCfg = null;
351  throw new \Erebot\StopException();
352  }
353 
361  public function handleSignal($signum)
362  {
363  $consts = get_defined_constants(true);
364  $signame = '???';
365  foreach ($consts['pcntl'] as $name => $value) {
366  if (!strncmp($name, 'SIG', 3) &&
367  strncmp($name, 'SIG_', 4) &&
368  $signum == $value) {
369  $signame = $name;
370  break;
371  }
372  }
373 
374  $logger = \Plop\Plop::getInstance();
375  $logger->info(
376  $this->gettext(
377  'Received signal #%(signum)d (%(signame)s)'
378  ),
379  array(
380  'signum' => $signum,
381  'signame' => $signame,
382  )
383  );
384 
385  // Print some statistics.
386  if (function_exists('memory_get_peak_usage')) {
387  $logger->debug($this->gettext('Memory usage:'));
388 
389  $limit = trim(ini_get('memory_limit'));
390  $limit = \Erebot\Utils::parseHumanSize($limit."B");
391  $stats = array(
392  (string) $this->gettext("Allocated:") =>
393  \Erebot\Utils::humanSize(memory_get_peak_usage(true)),
394  (string) $this->gettext("Used:") =>
395  \Erebot\Utils::humanSize(memory_get_peak_usage(false)),
396  (string) $this->gettext("Limit:") =>
397  \Erebot\Utils::humanSize($limit),
398  );
399 
400  foreach ($stats as $key => $value) {
401  $logger->debug(
402  '%(key)-16s%(value)10s',
403  array(
404  'key' => $key,
405  'value' => $value,
406  )
407  );
408  }
409  }
410 
411  $this->stop();
412  }
413 
414  public function getTimers()
415  {
416  return $this->timers;
417  }
418 
419  public function addTimer(\Erebot\TimerInterface $timer)
420  {
421  $key = array_search($timer, $this->timers);
422  if ($key !== false) {
423  throw new \Erebot\InvalidValueException('Timer already registered');
424  }
425 
426  $timer->reset();
427  $this->timers[] = $timer;
428  }
429 
430  public function removeTimer(\Erebot\TimerInterface $timer)
431  {
432  $key = array_search($timer, $this->timers);
433  if ($key === false) {
434  throw new \Erebot\NotFoundException('Timer not found');
435  }
436 
437  unset($this->timers[$key]);
438  }
439 
440  public function addConnection(\Erebot\Interfaces\Connection $connection)
441  {
442  $key = array_search($connection, $this->connections);
443  if ($key !== false) {
444  throw new \Erebot\InvalidValueException(
445  'Already handling this connection'
446  );
447  }
448 
449  $this->connections[] = $connection;
450  }
451 
452  public function removeConnection(\Erebot\Interfaces\Connection $connection)
453  {
454  /* $this->connections is unset during destructor call,
455  * but the destructing code depends on this method.
456  * we silently ignore the problem. */
457  if (!isset($this->connections)) {
458  return;
459  }
460 
461  $key = array_search($connection, $this->connections);
462  if ($key === false) {
463  throw new \Erebot\NotFoundException('No such connection');
464  }
465 
466  unset($this->connections[$key]);
467  }
468 
469  public function gettext($message)
470  {
471  return $this->translator->gettext($message);
472  }
473 
474  public function getRunningTime()
475  {
476  if (!$this->running) {
477  return false;
478  }
479  return time() - $this->running;
480  }
481 
489  public function handleSIGHUP($signum)
490  {
491  return $this->reload();
492  }
493 
505  public function reload(\Erebot\Interfaces\Config\Main $config = null)
506  {
507  $logger = \Plop\Plop::getInstance();
508 
509  $msg = $this->gettext('Reloading the configuration');
510  $logger->info($msg);
511 
512  if (!count($this->connections)) {
513  $logger->info($this->gettext('No active connections... Aborting.'));
514  return;
515  }
516 
517  if ($config === null) {
518  $configFile = $this->mainCfg->getConfigFile();
519  if ($configFile === null) {
520  $msg = $this->gettext('No configuration file to reload');
521  $logger->info($msg);
522  return;
523  }
524 
526  $config = new \Erebot\Config\Main(
527  $configFile,
528  \Erebot\Interfaces\Config\Main::LOAD_FROM_FILE
529  );
530  }
531 
532  $connectionCls = get_class($this->connections[0]);
533  $this->createConnections($connectionCls, $config);
534  $msg = $this->gettext('Successfully reloaded the configuration');
535  $logger->info($msg);
536  }
537 
547  protected function createConnections(
548  \Erebot\Interfaces\ConnectionFactory $factory,
549  \Erebot\Interfaces\Config\Main $config
550  ) {
551  $logger = \Plop\Plop::getInstance();
552 
553  // List existing connections so they
554  // can eventually be reused.
555  $newConnections =
556  $currentConnections = array();
557  foreach ($this->connections as $connection) {
558  $connCfg = $connection->getConfig(null);
559  if ($connCfg) {
560  $netCfg = $connCfg->getNetworkCfg();
561  $currentConnections[$netCfg->getName()] = $connection;
562  } else {
563  $newConnections[] = $connection;
564  }
565  }
566 
567  // Let's establish some contacts.
568  $networks = $config->getNetworks();
569  foreach ($networks as $network) {
570  $netName = $network->getName();
571  if (isset($currentConnections[$netName])) {
572  try {
573  $uris = $currentConnections[$netName]
574  ->getConfig(null)
575  ->getConnectionURI();
576  $uri = new \Erebot\URI($uris[count($uris) - 1]);
577  $serverCfg = $network->getServerCfg((string) $uri);
578 
579  $logger->info(
580  $this->gettext(
581  'Reusing existing connection ' .
582  'for network "%(network)s"'
583  ),
584  array('network' => $netName)
585  );
586  // Move it from existing connections to new connections,
587  // marking it as still being in use.
588  $copy = clone $currentConnections[$netName];
589  $copy->reload($serverCfg);
590  $newConnections[] = $copy;
591  unset($currentConnections[$netName]);
592  continue;
593  } catch (\Erebot\NotFoundException $e) {
594  // Nothing to do.
595  }
596  }
597 
598  if (!in_array('\\Erebot\\Module\\AutoConnect', $network->getModules(true))) {
599  continue;
600  }
601 
602  $servers = $network->getServers();
603  foreach ($servers as $server) {
604  $uris = $server->getConnectionURI();
605  $serverUri = new \Erebot\URI($uris[count($uris) - 1]);
606  try {
607  $connection = $factory->newConnection($this, $server);
608 
609  // Drop connection to a (now-)unconfigured
610  // server on that network.
611  if (isset($currentConnections[$netName])) {
612  $currentConnections[$netName]->disconnect();
613  unset($currentConnections[$netName]);
614  }
615 
616  $logger->info(
617  $this->gettext('Trying to connect to "%(uri)s"...'),
618  array('uri' => $serverUri)
619  );
620  $connection->connect();
621  $newConnections[] = $connection;
622 
623  $logger->info(
624  $this->gettext('Successfully connected to "%(uri)s"...'),
625  array('uri' => $serverUri)
626  );
627 
628  break;
629  } catch (\Erebot\ConnectionFailureException $e) {
630  // Nothing to do... We simply
631  // try the next server on the
632  // list until we successfully
633  // connect or cycle the list.
634  $logger->exception(
635  $this->gettext('Could not connect to "%(uri)s"'),
636  $e,
637  array('uri' => $serverUri)
638  );
639  }
640  }
641  }
642 
643  // Gracefully quit leftover connections.
644  foreach ($currentConnections as $connection) {
645  $connection->disconnect();
646  }
647 
648  $this->connections = $newConnections;
649  $this->mainCfg = $config;
650  }
651 }
Provides core functionalities for Erebot.
Definition: Core.php:59
$translator
Translator object for messages the bot may display.
Definition: Core.php:74
createConnections(\Erebot\Interfaces\ConnectionFactory $factory,\Erebot\Interfaces\Config\Main $config)
Definition: Core.php:547
$running
Indicates whether the bot is currently running or not.
Definition: Core.php:71
realStart(\Erebot\Interfaces\ConnectionFactory $factory)
Definition: Core.php:153
$connections
Connections to handle.
Definition: Core.php:62
handleSignal($signum)
Definition: Core.php:361
reload(\Erebot\Interfaces\Config\Main $config=null)
Definition: Core.php:505
handleSIGHUP($signum)
Definition: Core.php:489
$mainCfg
Main configuration for the bot.
Definition: Core.php:68
__destruct()
Definition: Core.php:120
getConnections()
Definition: Core.php:134
__clone()
Definition: Core.php:128
$timers
Timers to trigger.
Definition: Core.php:65
__construct(\Erebot\Interfaces\Config\Main $config,\Erebot\Intl\TranslatorInterface $translator)
Definition: Core.php:86
Connection factory.