49 if (defined(
'SIGUSR1') && $signum == SIGUSR1) {
70 flock($handle, LOCK_UN);
72 $logger = \Plop\Plop::getInstance();
74 'Removed lock on pidfile (%(pidfile)s)',
75 array(
'pidfile' => $pidfile)
87 public static function run()
93 $dic = new \Symfony\Component\DependencyInjection\ContainerBuilder();
94 $dic->setParameter(
'Erebot.src_dir', __DIR__);
95 $loader = new \Symfony\Component\DependencyInjection\Loader\XmlFileLoader(
97 new \Symfony\Component\Config\FileLocator(getcwd())
100 $dicConfig = dirname(__DIR__) .
101 DIRECTORY_SEPARATOR .
'data' .
102 DIRECTORY_SEPARATOR .
'defaults.xml';
103 $dicCwdConfig = getcwd() . DIRECTORY_SEPARATOR .
'defaults.xml';
104 if (!strncasecmp(__FILE__,
'phar://', 7)) {
105 if (!file_exists($dicCwdConfig)) {
106 copy($dicConfig, $dicCwdConfig);
108 $dicConfig = $dicCwdConfig;
109 } elseif (file_exists($dicCwdConfig)) {
110 $dicConfig = $dicCwdConfig;
112 $loader->load($dicConfig);
116 $hasPosix = in_array(
'posix', get_loaded_extensions());
117 $hasPcntl = in_array(
'pcntl', get_loaded_extensions());
119 $logger = $dic->get(
'logging');
120 $localeGetter = $dic->getParameter(
'i18n.default_getter');
121 $coreTranslatorCls = $dic->getParameter(
'core.classes.i18n');
124 class_alias(__CLASS__,
"Erebot",
false);
125 $translator =
new $coreTranslatorCls(
"Erebot");
133 foreach ($categories as $category) {
134 $locales = call_user_func($localeGetter);
135 $locales = empty($locales) ? array() : array($locales);
136 $localeSources = array(
142 foreach ($localeSources as $source => $multiple) {
143 if (!isset($_SERVER[$source])) {
147 $locales = explode(
':', $_SERVER[$source]);
149 $locales = array($_SERVER[$source]);
154 $translator->setLocale(
155 $translator->nameToCategory($category),
162 $version =
'dev-master';
163 if (!strncmp(__FILE__,
'phar://', 7)) {
164 $phar = new \Phar(\Phar::running(
true));
165 $md = $phar->getMetadata();
166 $version = $md[
'version'];
168 if (defined(
'Erebot_PHARS')) {
169 $phars = unserialize(Erebot_PHARS);
171 foreach ($phars as $module => $metadata) {
172 if (strncasecmp($module,
'Erebot_Module_', 14)) {
175 $version .=
"\n with $module version ${metadata['version']}";
179 \Console_CommandLine::registerAction(
'StoreProxy',
'\\Erebot\\Console\\StoreProxyAction');
180 $parser = new \Console_CommandLine(
184 $translator->gettext(
'A modular IRC bot written in PHP'),
185 'version' => $version,
186 'add_help_option' =>
true,
187 'add_version_option' =>
true,
188 'force_posix' =>
false,
191 $parser->accept(
new \
Erebot\Console\MessageProvider());
192 $parser->renderer->options_on_different_lines =
true;
194 $defaultConfigFile = getcwd() . DIRECTORY_SEPARATOR .
'Erebot.xml';
198 'short_name' =>
'-c',
199 'long_name' =>
'--config',
200 'description' => $translator->gettext(
201 'Path to the configuration file to use instead '.
202 'of "Erebot.xml", relative to the current '.
205 'help_name' =>
'FILE',
206 'action' =>
'StoreString',
207 'default' => $defaultConfigFile,
214 'short_name' =>
'-d',
215 'long_name' =>
'--daemon',
216 'description' => $translator->gettext(
217 'Run the bot in the background (daemon).'.
218 ' [requires the POSIX and pcntl extensions]' 220 'action' =>
'StoreTrue',
224 $noDaemon = new \Erebot\Console\ParallelOption(
227 'short_name' =>
'-n',
228 'long_name' =>
'--no-daemon',
229 'description' => $translator->gettext(
230 'Do not run the bot in the background. '.
231 'This is the default, unless the -d option '.
232 'is used or the bot is configured otherwise.' 234 'action' =>
'StoreProxy',
235 'action_params' => array(
'option' =>
'daemon'),
238 $parser->addOption($noDaemon);
243 'short_name' =>
'-p',
244 'long_name' =>
'--pidfile',
245 'description' => $translator->gettext(
246 "Store the bot's PID in this file." 248 'help_name' =>
'FILE',
249 'action' =>
'StoreString',
257 'short_name' =>
'-g',
258 'long_name' =>
'--group',
259 'description' => $translator->gettext(
260 'Set group identity to this GID/group during '.
261 'startup. The default is to NOT change group '.
262 'identity, unless configured otherwise.'.
263 ' [requires the POSIX extension]' 265 'help_name' =>
'GROUP/GID',
266 'action' =>
'StoreString',
274 'short_name' =>
'-u',
275 'long_name' =>
'--user',
276 'description' => $translator->gettext(
277 'Set user identity to this UID/username during '.
278 'startup. The default is to NOT change user '.
279 'identity, unless configured otherwise.'.
280 ' [requires the POSIX extension]' 282 'help_name' =>
'USER/UID',
283 'action' =>
'StoreString',
289 $parsed = $parser->parse();
291 $parser->displayError($exc->getMessage());
296 $config = new \Erebot\Config\Main(
297 $parsed->options[
'config'],
298 \
Erebot\Config\Main::LOAD_FROM_FILE,
302 $coreCls = $dic->getParameter(
'core.classes.core');
303 $bot =
new $coreCls($config, $translator);
304 $dic->set(
'bot', $bot);
309 'daemon' =>
'mustDaemonize',
310 'group' =>
'getGroupIdentity',
311 'user' =>
'getUserIdentity',
312 'pidfile' =>
'getPidfile',
314 foreach ($overrides as $option => $func) {
315 if ($parsed->options[$option] === null) {
316 $parsed->options[$option] = $config->$func();
325 if ($parsed->options[
'daemon']) {
328 $translator->gettext(
329 'The posix extension is required in order '.
330 'to start the bot in the background' 338 $translator->gettext(
339 'The pcntl extension is required in order '.
340 'to start the bot in the background' 346 foreach (array(
'SIGCHLD',
'SIGUSR1',
'SIGALRM') as $signal) {
347 if (defined($signal)) {
350 array(__CLASS__,
'startupSighandler')
356 $translator->gettext(
'Starting the bot in the background...')
361 $translator->gettext(
362 'Could not start in the background (unable to fork)' 368 pcntl_wait($dummy, WUNTRACED);
370 pcntl_signal_dispatch();
373 $parent = posix_getppid();
376 foreach (array(
'SIGTSTP',
'SIGTOU',
'SIGTIN',
'SIGHUP') as $signal) {
377 if (defined($signal)) {
378 pcntl_signal(constant($signal), SIG_IGN);
383 foreach (array(
'SIGCHLD',
'SIGUSR1',
'SIGALRM') as $signal) {
384 if (defined($signal)) {
385 pcntl_signal(constant($signal), SIG_DFL);
392 $translator->gettext(
'Could not change umask')
396 if (posix_setsid() == -1) {
398 $translator->gettext(
399 'Could not start in the background (unable to create a new session)' 410 $translator->gettext(
411 'Could not start in the background (unable to fork)' 421 if (!chdir(DIRECTORY_SEPARATOR)) {
423 $translator->gettext(
'Could not change directory to "%(path)s"'),
424 array(
'path' => DIRECTORY_SEPARATOR)
429 foreach (array(
'STDIN',
'STDOUT',
'STDERR') as $stream) {
430 if (defined($stream)) {
431 fclose(constant($stream));
440 $stdin = fopen(
'/dev/null',
'r');
441 $stdout = fopen(
'/dev/null',
'w');
442 $stderr = fopen(
'/dev/null',
'w');
444 if (defined(
'SIGUSR1')) {
445 posix_kill($parent, SIGUSR1);
448 $translator->gettext(
'Successfully started in the background')
454 $identd = $dic->get(
'identd');
455 }
catch (\InvalidArgumentException $e) {
461 $prompt = $dic->get(
'prompt');
462 }
catch (\InvalidArgumentException $e) {
467 if ($parsed->options[
'group'] !== null &&
468 $parsed->options[
'group'] !=
'') {
471 $translator->gettext(
472 'The posix extension is needed in order '.
473 'to change group identity.' 476 } elseif (posix_getuid() !== 0) {
478 $translator->gettext(
479 'Only the "root" user may change group identity! '.
480 'Your current UID is %(uid)d' 482 array(
'uid' => posix_getuid())
485 if (ctype_digit($parsed->options[
'group'])) {
486 $info = posix_getgrgid((
int) $parsed->options[
'group']);
488 $info = posix_getgrnam($parsed->options[
'group']);
491 if ($info ===
false) {
493 $translator->gettext(
'No such group "%(group)s"'),
494 array(
'group' => $parsed->options[
'group'])
499 if (!posix_setgid($info[
'gid'])) {
501 $translator->gettext(
502 'Could not set group identity '.
503 'to "%(name)s" (%(id)d)' 506 'id' => $info[
'gid'],
507 'name' => $info[
'name'],
514 $translator->gettext(
515 'Successfully changed group identity '.
516 'to "%(name)s" (%(id)d)' 519 'name' => $info[
'name'],
520 'id' => $info[
'gid'],
527 if ($parsed->options[
'user'] !== null ||
528 $parsed->options[
'user'] !=
'') {
531 $translator->gettext(
532 'The posix extension is needed in order '.
533 'to change user identity.' 536 } elseif (posix_getuid() !== 0) {
538 $translator->gettext(
539 'Only the "root" user may change user identity! '.
540 'Your current UID is %(uid)d' 542 array(
'uid' => posix_getuid())
545 if (ctype_digit($parsed->options[
'user'])) {
546 $info = posix_getpwuid((
int) $parsed->options[
'user']);
548 $info = posix_getpwnam($parsed->options[
'user']);
551 if ($info ===
false) {
553 $translator->gettext(
'No such user "%(user)s"'),
554 array(
'user' => $parsed->options[
'user'])
559 if (!posix_setuid($info[
'uid'])) {
561 $translator->gettext(
562 'Could not set user identity '.
563 'to "%(name)s" (%(id)d)' 566 'name' => $info[
'name'],
567 'id' => $info[
'uid'],
573 $translator->gettext(
574 'Successfully changed user identity '.
575 'to "%(name)s" (%(id)d)' 578 'name' => $info[
'name'],
579 'id' => $info[
'uid'],
586 if ($parsed->options[
'pidfile'] !== null &&
587 $parsed->options[
'pidfile'] !=
'') {
588 $pid = @file_get_contents($parsed->options[
'pidfile']);
591 if ($pid !==
false) {
592 $pid = (int) rtrim($pid);
595 $translator->gettext(
596 'The pidfile (%(pidfile)s) contained garbage. ' .
599 array(
'pidfile' => $parsed->options[
'pidfile'])
604 $res = posix_errno();
608 $translator->gettext(
609 'Erebot is already running ' .
618 $translator->gettext(
619 'Found stalled PID %(pid)d in pidfile '.
620 '"%(pidfile)s". Removing it' 623 'pidfile' => $parsed->options[
'pidfile'],
627 @unlink($parsed->options[
'pidfile']);
632 $translator->gettext(
633 'Found another program\'s PID %(pid)d in '.
634 'pidfile "%(pidfile)s". Exiting' 637 'pidfile' => $parsed->options[
'pidfile'],
645 $translator->gettext(
646 'Unknown error while checking for '.
647 'the existence of another running '.
648 'instance of Erebot (%(error)s)' 650 array(
'error' => posix_get_last_error())
657 $pidfile = fopen($parsed->options[
'pidfile'],
'wt');
658 flock($pidfile, LOCK_EX | LOCK_NB, $wouldBlock);
661 $translator->gettext(
662 'Could not lock pidfile (%(pidfile)s). '.
663 'Is the bot already running?' 665 array(
'pidfile' => $parsed->options[
'pidfile'])
670 $pid = sprintf(
"%u\n", getmypid());
671 $res = fwrite($pidfile, $pid);
672 if ($res !== strlen($pid)) {
674 $translator->gettext(
675 'Unable to write PID to pidfile (%(pidfile)s)' 677 array(
'pidfile' => $parsed->options[
'pidfile'])
683 $translator->gettext(
684 'PID (%(pid)d) written into %(pidfile)s' 687 'pidfile' => $parsed->options[
'pidfile'],
692 register_shutdown_function(
693 array(__CLASS__,
'cleanupPidfile'),
695 $parsed->options[
'pidfile']
700 if ($hasPosix && posix_getuid() === 0) {
702 $translator->gettext(
'You SHOULD NOT run Erebot as root!')
706 if ($identd !== null) {
710 if ($prompt !== null) {
716 $bot->start($dic->get(
'factory.connection'));
Base class for other (Erebot-related) exceptions.
static startupSighandler($signum)
static cleanupPidfile($handle, $pidfile)
Provides the entry-point for Erebot.