1 /++ 2 The main module, housing startup logic and the main event loop. 3 4 No module (save [kameloso.entrypoint]) should be importing this. 5 6 See_Also: 7 [kameloso.kameloso], 8 [kameloso.common], 9 [kameloso.config] 10 11 Copyright: [JR](https://github.com/zorael) 12 License: [Boost Software License 1.0](https://www.boost.org/users/license.html) 13 14 Authors: 15 [JR](https://github.com/zorael) 16 +/ 17 module kameloso.main; 18 19 private: 20 21 import kameloso.common : logger; 22 import kameloso.kameloso : Kameloso; 23 import kameloso.net : ListenAttempt; 24 import kameloso.plugins.common.core : IRCPlugin; 25 import kameloso.pods : CoreSettings; 26 import dialect.defs; 27 import lu.common : Next; 28 import std.stdio : stdout; 29 import std.typecons : Flag, No, Yes; 30 31 32 // gcOptions 33 /++ 34 A value line for [rt_options] to fine-tune the garbage collector. 35 36 Older compilers don't support all the garbage collector options newer 37 compilers do (breakpoints being at `2.085` for the precise garbage collector 38 and cleanup behaviour, and `2.098` for the forking one). So in one way or 39 another we need to specialise for compiler versions. This is one way. 40 41 See_Also: 42 [rt_options] 43 https://dlang.org/spec/garbage.html 44 +/ 45 enum gcOptions = () 46 { 47 import std.array : Appender; 48 49 Appender!(char[]) sink; 50 sink.reserve(128); 51 sink.put("gcopt="); 52 53 version(GCStatsOnExit) 54 { 55 sink.put("profile:1 "); 56 } 57 else version(unittest) 58 { 59 // Always print profile information on unittest builds 60 sink.put("profile:1 "); 61 } 62 63 sink.put("cleanup:finalize "); 64 65 version(PreciseGC) 66 { 67 sink.put("gc:precise "); 68 } 69 70 static if (__VERSION__ >= 2098L) 71 { 72 version(ConcurrentGC) 73 { 74 sink.put("fork:1 "); 75 } 76 } 77 78 // Tweak these numbers as we see fit 79 sink.put("initReserve:8 minPoolSize:8"); // incPoolSize:16 80 81 return sink.data; 82 }().idup; 83 84 85 // rt_options 86 /++ 87 Fine-tune the garbage collector. 88 89 See_Also: 90 [gcOptions] 91 https://dlang.org/spec/garbage.html 92 +/ 93 extern(C) 94 public __gshared const string[] rt_options = 95 [ 96 /++ 97 Garbage collector options. 98 +/ 99 gcOptions, 100 101 /++ 102 Tells the garbage collector to scan the DATA and TLS segments precisely, 103 on Windows. 104 +/ 105 "scanDataSeg=precise", 106 ]; 107 108 109 // globalAbort 110 /++ 111 Abort flag. 112 113 This is set when the program is interrupted (such as via Ctrl+C). Other 114 parts of the program will be monitoring it, to take the cue and abort when 115 it is set. 116 117 Must be `__gshared` or it doesn't seem to work on Windows. 118 +/ 119 public __gshared bool globalAbort; 120 121 122 // globalHeadless 123 /++ 124 Headless flag. 125 126 If this is true the program should not output anything to the terminal. 127 +/ 128 public __gshared bool globalHeadless; 129 130 131 version(Posix) 132 { 133 // signalRaised 134 /++ 135 The value of the signal, when the process was sent one that meant it 136 should abort. This determines the shell exit code to return. 137 +/ 138 private int signalRaised; 139 } 140 141 142 // signalHandler 143 /++ 144 Called when a signal is raised, usually `SIGINT`. 145 146 Sets the [globalAbort] variable to true so other parts of the program knows to 147 gracefully shut down. 148 149 Params: 150 sig = Integer value of the signal raised. 151 +/ 152 extern (C) 153 void signalHandler(int sig) nothrow @nogc @system 154 { 155 import core.stdc.stdio : printf; 156 157 // $ kill -l 158 // https://man7.org/linux/man-pages/man7/signal.7.html 159 static immutable string[32] signalNames = 160 [ 161 0 : "<err>", /// Should never happen. 162 1 : "HUP", /// Hangup detected on controlling terminal or death of controlling process. 163 2 : "INT", /// Interrupt from keyboard. 164 3 : "QUIT", /// Quit from keyboard. 165 4 : "ILL", /// Illegal instruction. 166 5 : "TRAP", /// Trace/breakpoint trap. 167 6 : "ABRT", /// Abort signal from `abort(3)`. 168 7 : "BUS", /// Bus error: access to an undefined portion of a memory object. 169 8 : "FPE", /// Floating-point exception. 170 9 : "KILL", /// Kill signal. 171 10 : "USR1", /// User-defined signal 1. 172 11 : "SEGV", /// Invalid memory reference. 173 12 : "USR2", /// User-defined signal 2. 174 13 : "PIPE", /// Broken pipe: write to pipe with no readers. 175 14 : "ALRM", /// Timer signal from `alarm(2)`. 176 15 : "TERM", /// Termination signal. 177 16 : "STKFLT",/// Stack fault on coprocessor. (unused?) 178 17 : "CHLD", /// Child stopped or terminated. 179 18 : "CONT", /// Continue if stopped. 180 19 : "STOP", /// Stop process. 181 20 : "TSTP", /// Stop typed at terminal. 182 21 : "TTIN", /// Terminal input for background process. 183 22 : "TTOU", /// Terminal output for background process. 184 23 : "URG", /// Urgent condition on socket. (4.2 BSD) 185 24 : "XCPU", /// CPU time limit exceeded. (4.2 BSD) 186 25 : "XFSZ", /// File size limit exceeded. (4.2 BSD) 187 26 : "VTALRM",/// Virtual alarm clock. (4.2 BSD) 188 27 : "PROF", /// Profile alarm clock. 189 28 : "WINCH", /// Window resize signal. (4.3 BSD, Sun) 190 29 : "POLL", /// Pollable event; a synonym for `SIGIO`: I/O now possible. (System V) 191 30 : "PWR", /// Power failure. (System V) 192 31 : "SYS", /// Bad system call. (SVr4) 193 ]; 194 195 if (!globalHeadless && (sig < signalNames.length)) 196 { 197 if (!globalAbort) 198 { 199 printf("...caught signal SIG%s!\n", signalNames[sig].ptr); 200 } 201 else if (sig == 2) 202 { 203 printf("...caught another signal SIG%s! " ~ 204 "(press Enter if nothing happens, or Ctrl+C again)\n", signalNames[sig].ptr); 205 } 206 } 207 208 if (globalAbort) resetSignals(); 209 globalAbort = true; 210 211 version(Posix) 212 { 213 signalRaised = sig; 214 } 215 } 216 217 218 // messageFiber 219 /++ 220 A Generator Fiber function that checks for concurrency messages and performs 221 action based on what was received. 222 223 The return value yielded to the caller tells it whether the received action 224 means the bot should exit or not. 225 226 Params: 227 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 228 +/ 229 void messageFiber(ref Kameloso instance) 230 { 231 import kameloso.common : OutgoingLine; 232 import kameloso.constants : Timeout; 233 import kameloso.messaging : Message; 234 import kameloso.string : replaceTokens; 235 import kameloso.thread : OutputRequest, ThreadMessage; 236 import std.concurrency : yield; 237 import std.datetime.systime : Clock; 238 import core.time : Duration, msecs; 239 240 // The Generator we use this function with popFronts the first thing it does 241 // after being instantiated. We're not ready for that yet, so catch the next 242 // yield (which is upon messenger.call()). 243 yield(Next.init); 244 245 // Loop forever; we'll just terminate the Generator when we want to quit. 246 while (true) 247 { 248 auto next = Next.continue_; 249 250 /++ 251 Handle [kameloso.thread.ThreadMessage]s based on their 252 [kameloso.thread.ThreadMessage.Type|Type]s. 253 +/ 254 void onThreadMessage(ThreadMessage message) scope 255 { 256 with (ThreadMessage.Type) 257 switch (message.type) 258 { 259 case pong: 260 /+ 261 PONGs literally always have the same content, so micro-optimise 262 this a bit by only allocating the string once and keeping it 263 if the contents don't change. 264 +/ 265 static string pongline; 266 267 if (!pongline.length || (pongline[6..$] != message.content)) 268 { 269 pongline = "PONG :" ~ message.content; 270 } 271 272 instance.priorityBuffer.put(OutgoingLine(pongline, Yes.quiet)); 273 break; 274 275 case ping: 276 // No need to micro-optimise here, PINGs should be very rare 277 immutable pingline = "PING :" ~ message.content; 278 instance.priorityBuffer.put(OutgoingLine(pingline, Yes.quiet)); 279 break; 280 281 case sendline: 282 instance.outbuffer.put(OutgoingLine( 283 message.content, 284 cast(Flag!"quiet")instance.settings.hideOutgoing)); 285 break; 286 287 case quietline: 288 instance.outbuffer.put(OutgoingLine( 289 message.content, 290 Yes.quiet)); 291 break; 292 293 case immediateline: 294 instance.immediateBuffer.put(OutgoingLine( 295 message.content, 296 cast(Flag!"quiet")instance.settings.hideOutgoing)); 297 break; 298 299 case shortenReceiveTimeout: 300 instance.flags.wantReceiveTimeoutShortened = true; 301 break; 302 303 case busMessage: 304 foreach (plugin; instance.plugins) 305 { 306 plugin.onBusMessage(message.content, message.payload); 307 } 308 break; 309 310 case quit: 311 // This will automatically close the connection. 312 immutable reason = message.content.length ? 313 message.content : 314 instance.bot.quitReason; 315 immutable quitMessage = "QUIT :" ~ reason.replaceTokens(instance.parser.client); 316 instance.priorityBuffer.put(OutgoingLine( 317 quitMessage, 318 cast(Flag!"quiet")message.quiet)); 319 instance.flags.quitMessageSent = true; 320 next = Next.returnSuccess; 321 break; 322 323 case reconnect: 324 import kameloso.thread : Boxed; 325 326 if (auto boxedReexecFlag = cast(Boxed!bool)message.payload) 327 { 328 // Re-exec explicitly requested 329 instance.flags.askedToReexec = boxedReexecFlag.payload; 330 } 331 else 332 { 333 // Normal reconnect 334 instance.flags.askedToReconnect = true; 335 } 336 337 immutable quitMessage = message.content.length ? 338 message.content : 339 "Reconnecting."; 340 instance.priorityBuffer.put(OutgoingLine( 341 "QUIT :" ~ quitMessage, 342 No.quiet)); 343 instance.flags.quitMessageSent = true; 344 next = Next.retry; 345 break; 346 347 case wantLiveSummary: 348 instance.flags.wantLiveSummary = true; 349 break; 350 351 case abort: 352 *instance.abort = true; 353 break; 354 355 case reload: 356 foreach (plugin; instance.plugins) 357 { 358 if (!plugin.isEnabled) continue; 359 360 try 361 { 362 if (!message.content.length || (plugin.name == message.content)) 363 { 364 plugin.reload(); 365 } 366 } 367 catch (Exception e) 368 { 369 enum pattern = "The <l>%s</> plugin threw an exception when reloading: <l>%s"; 370 logger.errorf(pattern, plugin.name, e.msg); 371 version(PrintStacktraces) logger.trace(e); 372 } 373 } 374 break; 375 376 case save: 377 import kameloso.config : writeConfigurationFile; 378 syncGuestChannels(instance); 379 writeConfigurationFile(instance, instance.settings.configFile); 380 break; 381 382 case popCustomSetting: 383 size_t[] toRemove; 384 385 foreach (immutable i, immutable line; instance.customSettings) 386 { 387 import lu.string : nom; 388 389 string slice = line; // mutable 390 immutable setting = slice.nom!(Yes.inherit)('='); 391 if (setting == message.content) toRemove ~= i; 392 } 393 394 foreach_reverse (immutable i; toRemove) 395 { 396 import std.algorithm.mutation : SwapStrategy, remove; 397 instance.customSettings = instance.customSettings 398 .remove!(SwapStrategy.unstable)(i); 399 } 400 break; 401 402 case putUser: 403 import kameloso.thread : Boxed; 404 405 auto boxedUser = cast(Boxed!IRCUser)message.payload; 406 assert(boxedUser, "Incorrectly cast message payload: " ~ typeof(boxedUser).stringof); 407 408 auto user = boxedUser.payload; 409 410 foreach (plugin; instance.plugins) 411 { 412 if (auto existingUser = user.nickname in plugin.state.users) 413 { 414 immutable prevClass = existingUser.class_; 415 *existingUser = user; 416 existingUser.class_ = prevClass; 417 } 418 else 419 { 420 plugin.state.users[user.nickname] = user; 421 } 422 } 423 break; 424 425 default: 426 enum pattern = "onThreadMessage received unexpected message type: <l>%s"; 427 logger.errorf(pattern, message.type); 428 if (instance.settings.flush) stdout.flush(); 429 break; 430 } 431 } 432 433 /// Reverse-formats an event and sends it to the server. 434 void eventToServer(Message m) scope 435 { 436 import lu.string : splitLineAtPosition; 437 import std.conv : text; 438 import std.format : format; 439 440 enum maxIRCLineLength = 512-2; // sans CRLF 441 442 version(TwitchSupport) 443 { 444 // The first two checks are probably superfluous 445 immutable fast = 446 (instance.parser.server.daemon == IRCServer.Daemon.twitch) && 447 (m.event.type != IRCEvent.Type.QUERY) && 448 (m.properties & Message.Property.fast); 449 } 450 451 immutable background = (m.properties & Message.Property.background); 452 immutable quietFlag = cast(Flag!"quiet") 453 (instance.settings.hideOutgoing || (m.properties & Message.Property.quiet)); 454 immutable force = (m.properties & Message.Property.forced); 455 immutable priority = (m.properties & Message.Property.priority); 456 immutable immediate = (m.properties & Message.Property.immediate); 457 458 string line; 459 string prelude; 460 string[] lines; 461 462 with (IRCEvent.Type) 463 switch (m.event.type) 464 { 465 case CHAN: 466 enum pattern = "PRIVMSG %s :"; 467 prelude = pattern.format(m.event.channel); 468 lines = m.event.content.splitLineAtPosition(' ', maxIRCLineLength-prelude.length); 469 break; 470 471 case QUERY: 472 version(TwitchSupport) 473 { 474 if (instance.parser.server.daemon == IRCServer.Daemon.twitch) 475 { 476 /*if (m.event.target.nickname == instance.parser.client.nickname) 477 { 478 // "You cannot whisper to yourself." (whisper_invalid_self) 479 return; 480 }*/ 481 482 enum pattern = "PRIVMSG #%s :/w %s "; 483 prelude = pattern.format(instance.parser.client.nickname, m.event.target.nickname); 484 } 485 } 486 487 enum pattern = "PRIVMSG %s :"; 488 if (!prelude.length) prelude = pattern.format(m.event.target.nickname); 489 lines = m.event.content.splitLineAtPosition(' ', maxIRCLineLength-prelude.length); 490 break; 491 492 case EMOTE: 493 immutable emoteTarget = m.event.target.nickname.length ? 494 m.event.target.nickname : 495 m.event.channel; 496 497 version(TwitchSupport) 498 { 499 if (instance.parser.server.daemon == IRCServer.Daemon.twitch) 500 { 501 enum pattern = "PRIVMSG %s :/me "; 502 prelude = pattern.format(emoteTarget); 503 lines = m.event.content.splitLineAtPosition(' ', maxIRCLineLength-prelude.length); 504 } 505 } 506 507 if (!prelude.length) 508 { 509 import dialect.common : IRCControlCharacter; 510 enum pattern = "PRIVMSG %s :%cACTION %s%2$c"; 511 line = format(pattern, emoteTarget, cast(char)IRCControlCharacter.ctcp, m.event.content); 512 } 513 break; 514 515 case MODE: 516 import lu.string : strippedRight; 517 518 enum pattern = "MODE %s %s %s"; 519 line = format(pattern, m.event.channel, m.event.aux[0], m.event.content.strippedRight); 520 break; 521 522 case TOPIC: 523 enum pattern = "TOPIC %s :%s"; 524 line = pattern.format(m.event.channel, m.event.content); 525 break; 526 527 case INVITE: 528 enum pattern = "INVITE %s %s"; 529 line = pattern.format(m.event.channel, m.event.target.nickname); 530 break; 531 532 case JOIN: 533 if (m.event.aux[0].length) 534 { 535 // Key, assume only one channel 536 line = text("JOIN ", m.event.channel, ' ', m.event.aux[0]); 537 } 538 else 539 { 540 prelude = "JOIN "; 541 lines = m.event.channel.splitLineAtPosition(',', maxIRCLineLength-prelude.length); 542 } 543 break; 544 545 case KICK: 546 immutable reason = m.event.content.length ? 547 " :" ~ m.event.content : 548 string.init; 549 enum pattern = "KICK %s %s%s"; 550 line = format(pattern, m.event.channel, m.event.target.nickname, reason); 551 break; 552 553 case PART: 554 if (m.event.content.length) 555 { 556 // Reason given, assume only one channel 557 line = text( 558 "PART ", m.event.channel, " :", 559 m.event.content.replaceTokens(instance.parser.client)); 560 } 561 else 562 { 563 prelude = "PART "; 564 lines = m.event.channel.splitLineAtPosition(',', maxIRCLineLength-prelude.length); 565 } 566 break; 567 568 case NICK: 569 line = "NICK " ~ m.event.target.nickname; 570 break; 571 572 case PRIVMSG: 573 if (m.event.channel.length) 574 { 575 goto case CHAN; 576 } 577 else 578 { 579 goto case QUERY; 580 } 581 582 case RPL_WHOISACCOUNT: 583 import kameloso.constants : Timeout; 584 import std.datetime.systime : Clock; 585 586 immutable now = Clock.currTime.toUnixTime; 587 immutable then = instance.previousWhoisTimestamps.get(m.event.target.nickname, 0); 588 immutable hysteresis = force ? 1 : Timeout.whoisRetry; 589 590 version(TraceWhois) 591 { 592 import std.stdio : writef, writefln, writeln; 593 594 enum pattern = "[TraceWhois] messageFiber caught request to " ~ 595 "WHOIS \"%s\" from %s (quiet:%s, background:%s)"; 596 writef( 597 pattern, 598 m.event.target.nickname, 599 m.caller, 600 cast(bool)quietFlag, 601 cast(bool)background); 602 } 603 604 if ((now - then) > hysteresis) 605 { 606 version(TraceWhois) 607 { 608 writeln(" ...and actually issuing."); 609 } 610 611 line = "WHOIS " ~ m.event.target.nickname; 612 instance.previousWhoisTimestamps[m.event.target.nickname] = now; 613 propagateWhoisTimestamp(instance, m.event.target.nickname, now); 614 } 615 else 616 { 617 version(TraceWhois) 618 { 619 writefln(" ...but already issued %d seconds ago.", (now - then)); 620 } 621 } 622 623 version(TraceWhois) 624 { 625 if (instance.settings.flush) stdout.flush(); 626 } 627 break; 628 629 case QUIT: 630 immutable rawReason = m.event.content.length ? 631 m.event.content : 632 instance.bot.quitReason; 633 immutable reason = rawReason.replaceTokens(instance.parser.client); 634 line = "QUIT :" ~ reason; 635 instance.flags.quitMessageSent = true; 636 next = Next.returnSuccess; 637 break; 638 639 case UNSET: 640 line = m.event.content; 641 break; 642 643 default: 644 logger.error("No outgoing event case for type <l>", m.event.type); 645 break; 646 } 647 648 void appropriateline(const string finalLine) 649 { 650 if (immediate) 651 { 652 instance.immediateBuffer.put(OutgoingLine(finalLine, quietFlag)); 653 return; 654 } 655 656 version(TwitchSupport) 657 { 658 if (/*(instance.parser.server.daemon == IRCServer.Daemon.twitch) &&*/ fast) 659 { 660 // Send a line via the fastbuffer, faster than normal sends. 661 instance.fastbuffer.put(OutgoingLine(finalLine, quietFlag)); 662 return; 663 } 664 } 665 666 if (priority) 667 { 668 instance.priorityBuffer.put(OutgoingLine(finalLine, quietFlag)); 669 } 670 else if (background) 671 { 672 // Send a line via the low-priority background buffer. 673 instance.backgroundBuffer.put(OutgoingLine(finalLine, quietFlag)); 674 } 675 else if (quietFlag) 676 { 677 instance.outbuffer.put(OutgoingLine(finalLine, Yes.quiet)); 678 } 679 else 680 { 681 instance.outbuffer.put(OutgoingLine(finalLine, cast(Flag!"quiet")instance.settings.hideOutgoing)); 682 } 683 } 684 685 if (lines.length) 686 { 687 foreach (immutable i, immutable splitLine; lines) 688 { 689 immutable finalLine = m.event.tags.length ? 690 text('@', m.event.tags, ' ', prelude, splitLine) : 691 text(prelude, splitLine); 692 appropriateline(finalLine); 693 } 694 } 695 else if (line.length) 696 { 697 if (m.event.tags.length) line = text('@', m.event.tags, ' ', line); 698 appropriateline(line); 699 } 700 } 701 702 /// Proxies the passed message to the [kameloso.logger.logger]. 703 void proxyLoggerMessages(OutputRequest request) scope 704 { 705 if (instance.settings.headless) return; 706 707 with (OutputRequest.Level) 708 final switch (request.logLevel) 709 { 710 case writeln: 711 import kameloso.logger : LogLevel; 712 import kameloso.terminal.colours.tags : expandTags; 713 import std.stdio : writeln; 714 715 writeln(request.line.expandTags(LogLevel.off)); 716 if (instance.settings.flush) stdout.flush(); 717 break; 718 719 case trace: 720 logger.trace(request.line); 721 break; 722 723 case log: 724 logger.log(request.line); 725 break; 726 727 case info: 728 logger.info(request.line); 729 break; 730 731 case warning: 732 logger.warning(request.line); 733 break; 734 735 case error: 736 logger.error(request.line); 737 break; 738 739 case critical: 740 logger.critical(request.line); 741 break; 742 743 case fatal: 744 logger.fatal(request.line); 745 break; 746 } 747 } 748 749 /// Timestamp of when the loop started. 750 immutable loopStartTime = Clock.currTime; 751 static immutable maxReceiveTime = Timeout.messageReadMsecs.msecs; 752 753 while (!*instance.abort && 754 (next == Next.continue_) && 755 ((Clock.currTime - loopStartTime) <= maxReceiveTime)) 756 { 757 import std.concurrency : receiveTimeout; 758 import std.variant : Variant; 759 760 immutable receivedSomething = receiveTimeout(Duration.zero, 761 &onThreadMessage, 762 &eventToServer, 763 &proxyLoggerMessages, 764 (Variant v) scope 765 { 766 // Caught an unhandled message 767 enum pattern = "Main thread message fiber received unknown Variant: <l>%s"; 768 logger.warningf(pattern, v.type); 769 } 770 ); 771 772 if (!receivedSomething) break; 773 } 774 775 yield(next); 776 } 777 778 assert(0, "`while (true)` loop break in `messageFiber`"); 779 } 780 781 782 // mainLoop 783 /++ 784 This loops creates a [std.concurrency.Generator|Generator] 785 [core.thread.fiber.Fiber|Fiber] to loop over the connected [std.socket.Socket|Socket]. 786 787 Full lines are stored in [kameloso.net.ListenAttempt|ListenAttempt]s, which 788 are yielded in the [std.concurrency.Generator|Generator] to be caught here, 789 consequently parsed into [dialect.defs.IRCEvent|IRCEvent]s, and then dispatched 790 to all plugins. 791 792 Params: 793 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 794 795 Returns: 796 [lu.common.Next.returnFailure|Next.returnFailure] if circumstances mean 797 the bot should exit with a non-zero exit code, 798 [lu.common.Next.returnSuccess|Next.returnSuccess] if it should exit by 799 returning `0`, 800 [lu.common.Next.retry|Next.retry] if the bot should reconnect to the server. 801 [lu.common.Next.continue_|Next.continue_] is never returned. 802 +/ 803 auto mainLoop(ref Kameloso instance) 804 { 805 import kameloso.constants : Timeout; 806 import kameloso.net : ListenAttempt, listenFiber; 807 import std.concurrency : Generator; 808 import std.datetime.systime : Clock; 809 import core.thread : Fiber; 810 811 /// Variable denoting what we should do next loop. 812 Next next; 813 814 alias State = ListenAttempt.State; 815 816 // Instantiate a Generator to read from the socket and yield lines 817 auto listener = new Generator!ListenAttempt(() => 818 listenFiber( 819 instance.conn, 820 *instance.abort, 821 Timeout.connectionLost)); 822 auto messenger = new Generator!Next(() => messageFiber(instance)); 823 824 scope(exit) 825 { 826 destroy(listener); 827 destroy(messenger); 828 } 829 830 /++ 831 Invokes the messenger generator. 832 +/ 833 Next callMessenger() 834 { 835 try 836 { 837 messenger.call(); 838 } 839 catch (Exception e) 840 { 841 import kameloso.string : doublyBackslashed; 842 843 enum pattern = "Unhandled messenger exception: <l>%s</> (at <l>%s</>:<l>%d</>)"; 844 logger.warningf(pattern, e.msg, e.file.doublyBackslashed, e.line); 845 version(PrintStacktraces) logger.trace(e); 846 return Next.returnFailure; 847 } 848 849 if (messenger.state == Fiber.State.HOLD) 850 { 851 return messenger.front; 852 } 853 else 854 { 855 logger.error("Internal error, thread messenger Fiber ended abruptly."); 856 return Next.returnFailure; 857 } 858 } 859 860 // Start plugins before the loop starts and immediately read messages sent. 861 try 862 { 863 instance.startPlugins(); 864 immutable messengerNext = callMessenger(); 865 if (messengerNext != Next.continue_) return messengerNext; 866 } 867 catch (Exception e) 868 { 869 enum pattern = "Exception thrown when starting plugins: <l>%s"; 870 logger.errorf(pattern, e.msg); 871 logger.trace(e.info); 872 return Next.returnFailure; 873 } 874 875 /// The history entry for the current connection. 876 Kameloso.ConnectionHistoryEntry* historyEntry; 877 878 immutable historyEntryIndex = instance.connectionHistory.length; // snapshot index, 0 at first 879 instance.connectionHistory ~= Kameloso.ConnectionHistoryEntry.init; 880 historyEntry = &instance.connectionHistory[historyEntryIndex]; 881 historyEntry.startTime = Clock.currTime.toUnixTime; 882 historyEntry.stopTime = historyEntry.startTime; // In case we abort before the first read is recorded 883 884 /// UNIX timestamp of when the Socket receive timeout was shortened. 885 long timeWhenReceiveWasShortened; 886 887 /// `Timeout.maxShortenDurationMsecs` in hecto-nanoseconds. 888 enum maxShortenDurationHnsecs = Timeout.maxShortenDurationMsecs * 10_000; 889 890 do 891 { 892 if (*instance.abort) return Next.returnFailure; 893 894 if (!instance.settings.headless && instance.flags.wantLiveSummary) 895 { 896 // Live connection summary requested. 897 printSummary(instance); 898 instance.flags.wantLiveSummary = false; 899 } 900 901 if (listener.state == Fiber.State.TERM) 902 { 903 // Listening Generator disconnected by itself; reconnect 904 return Next.retry; 905 } 906 907 immutable now = Clock.currTime; 908 immutable nowInUnix = now.toUnixTime; 909 immutable nowInHnsecs = now.stdTime; 910 911 /// The timestamp of the next scheduled delegate or fiber across all plugins. 912 long nextGlobalScheduledTimestamp; 913 914 /// Whether or not blocking was disabled on the socket to force an instant read timeout. 915 bool socketBlockingDisabled; 916 917 foreach (plugin; instance.plugins) 918 { 919 if (!plugin.isEnabled) continue; 920 921 if (plugin.state.specialRequests.length) 922 { 923 processSpecialRequests(instance, plugin); 924 } 925 926 if (plugin.state.scheduledFibers.length || 927 plugin.state.scheduledDelegates.length) 928 { 929 if (plugin.state.nextScheduledTimestamp <= nowInHnsecs) 930 { 931 processScheduledDelegates(plugin, nowInHnsecs); 932 processScheduledFibers(plugin, nowInHnsecs); 933 plugin.state.updateSchedule(); // Something is always removed 934 instance.conn.socket.blocking = false; // Instantly timeout read to check messages 935 socketBlockingDisabled = true; 936 937 if (*instance.abort) return Next.returnFailure; 938 } 939 940 if (!nextGlobalScheduledTimestamp || 941 (plugin.state.nextScheduledTimestamp < nextGlobalScheduledTimestamp)) 942 { 943 nextGlobalScheduledTimestamp = plugin.state.nextScheduledTimestamp; 944 } 945 } 946 } 947 948 // Set timeout *before* the receive, else we'll just be applying the delay too late 949 if (nextGlobalScheduledTimestamp) 950 { 951 immutable delayToNextMsecs = 952 cast(uint)((nextGlobalScheduledTimestamp - nowInHnsecs) / 10_000); 953 954 if (delayToNextMsecs < instance.conn.receiveTimeout) 955 { 956 instance.conn.receiveTimeout = (delayToNextMsecs > 0) ? 957 delayToNextMsecs : 958 1; 959 } 960 } 961 962 // Once every 24h, clear the `previousWhoisTimestamps` AA. 963 // That should be enough to stop it from being a memory leak. 964 if ((nowInUnix % 86_400) == 0) 965 { 966 instance.previousWhoisTimestamps = null; 967 propagateWhoisTimestamps(instance); 968 } 969 970 // Call the generator, query it for event lines 971 listener.call(); 972 973 listenerloop: 974 foreach (const attempt; listener) 975 { 976 if (*instance.abort) return Next.returnFailure; 977 978 immutable actionAfterListen = listenAttemptToNext(instance, attempt); 979 980 with (Next) 981 final switch (actionAfterListen) 982 { 983 case continue_: 984 import std.algorithm.comparison : max; 985 986 historyEntry.bytesReceived += max(attempt.bytesReceived, 0); 987 historyEntry.stopTime = nowInUnix; 988 ++historyEntry.numEvents; 989 processLineFromServer(instance, attempt.line, nowInUnix); 990 break; 991 992 case retry: 993 // Break and try again 994 historyEntry.stopTime = nowInUnix; 995 break listenerloop; 996 997 case returnFailure: 998 return Next.retry; 999 1000 case returnSuccess: // should never happen 1001 case crash: // ditto 1002 import lu.conv : Enum; 1003 import std.conv : text; 1004 assert(0, text("`listenAttemptToNext` returned `", Enum!Next.toString(actionAfterListen), "`")); 1005 } 1006 } 1007 1008 // Check concurrency messages to see if we should exit 1009 next = callMessenger(); 1010 if (*instance.abort) return Next.returnFailure; 1011 //else if (next != Next.continue_) return next; // process buffers before passing on Next.retry 1012 1013 bool bufferHasMessages = ( 1014 !instance.outbuffer.empty | 1015 !instance.backgroundBuffer.empty | 1016 !instance.immediateBuffer.empty | 1017 !instance.priorityBuffer.empty); 1018 1019 version(TwitchSupport) 1020 { 1021 bufferHasMessages |= !instance.fastbuffer.empty; 1022 } 1023 1024 /// Adjusted receive timeout based on outgoing message buffers. 1025 uint timeoutFromMessages = uint.max; 1026 1027 if (bufferHasMessages) 1028 { 1029 import kameloso.net : SocketSendException; 1030 1031 try 1032 { 1033 immutable untilNext = sendLines(instance); 1034 1035 if ((untilNext > 0.0) && (untilNext < instance.connSettings.messageBurst)) 1036 { 1037 immutable untilNextMsecs = cast(uint)(untilNext * 1000); 1038 1039 if (untilNextMsecs < instance.conn.receiveTimeout) 1040 { 1041 timeoutFromMessages = untilNextMsecs; 1042 } 1043 } 1044 } 1045 catch (SocketSendException _) 1046 { 1047 logger.error("Failure sending data to server! Connection lost?"); 1048 return Next.retry; 1049 } 1050 } 1051 1052 if (timeWhenReceiveWasShortened && 1053 (nowInHnsecs > (timeWhenReceiveWasShortened + maxShortenDurationHnsecs))) 1054 { 1055 // Shortened duration passed, reset timestamp to disable it 1056 timeWhenReceiveWasShortened = 0L; 1057 } 1058 1059 if (instance.flags.wantReceiveTimeoutShortened) 1060 { 1061 // Set the timestamp and unset the bool 1062 instance.flags.wantReceiveTimeoutShortened = false; 1063 timeWhenReceiveWasShortened = nowInHnsecs; 1064 } 1065 1066 if ((timeoutFromMessages < uint.max) || 1067 nextGlobalScheduledTimestamp || 1068 timeWhenReceiveWasShortened) 1069 { 1070 import kameloso.constants : ConnectionDefaultFloats; 1071 import std.algorithm.comparison : min; 1072 1073 immutable defaultTimeout = timeWhenReceiveWasShortened ? 1074 cast(uint)(Timeout.receiveMsecs * ConnectionDefaultFloats.receiveShorteningMultiplier) : 1075 instance.connSettings.receiveTimeout; 1076 1077 immutable untilNextGlobalScheduled = nextGlobalScheduledTimestamp ? 1078 cast(uint)(nextGlobalScheduledTimestamp - nowInHnsecs)/10_000 : 1079 uint.max; 1080 1081 immutable supposedNewTimeout = 1082 min(defaultTimeout, timeoutFromMessages, untilNextGlobalScheduled); 1083 1084 if (supposedNewTimeout != instance.conn.receiveTimeout) 1085 { 1086 instance.conn.receiveTimeout = (supposedNewTimeout > 0) ? 1087 supposedNewTimeout : 1088 1; 1089 } 1090 } 1091 1092 if (socketBlockingDisabled) 1093 { 1094 // Restore blocking behaviour. 1095 instance.conn.socket.blocking = true; 1096 } 1097 } 1098 while (next == Next.continue_); 1099 1100 return next; 1101 } 1102 1103 1104 // sendLines 1105 /++ 1106 Sends strings to the server from the message buffers. 1107 1108 Broken out of [mainLoop] to make it more legible. 1109 1110 Params: 1111 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 1112 1113 Returns: 1114 How many milliseconds until the next message in the buffers should be sent. 1115 +/ 1116 auto sendLines(ref Kameloso instance) 1117 { 1118 if (!instance.immediateBuffer.empty) 1119 { 1120 cast(void)instance.throttleline( 1121 instance.immediateBuffer, 1122 No.dryRun, 1123 No.sendFaster, 1124 Yes.immediate); 1125 } 1126 1127 if (!instance.priorityBuffer.empty) 1128 { 1129 immutable untilNext = instance.throttleline(instance.priorityBuffer); 1130 if (untilNext > 0.0) return untilNext; 1131 } 1132 1133 version(TwitchSupport) 1134 { 1135 if (!instance.fastbuffer.empty) 1136 { 1137 immutable untilNext = instance.throttleline( 1138 instance.fastbuffer, 1139 No.dryRun, 1140 Yes.sendFaster); 1141 if (untilNext > 0.0) return untilNext; 1142 } 1143 } 1144 1145 if (!instance.outbuffer.empty) 1146 { 1147 immutable untilNext = instance.throttleline(instance.outbuffer); 1148 if (untilNext > 0.0) return untilNext; 1149 } 1150 1151 if (!instance.backgroundBuffer.empty) 1152 { 1153 immutable untilNext = instance.throttleline(instance.backgroundBuffer); 1154 if (untilNext > 0.0) return untilNext; 1155 } 1156 1157 return 0.0; 1158 } 1159 1160 1161 // listenAttemptToNext 1162 /++ 1163 Translates the [kameloso.net.ListenAttempt.State|ListenAttempt.State] 1164 received from a [std.concurrency.Generator|Generator] into a [lu.common.Next|Next], 1165 while also providing warnings and error messages. 1166 1167 Params: 1168 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 1169 attempt = The [kameloso.net.ListenAttempt|ListenAttempt] to map the `.state` value of. 1170 1171 Returns: 1172 A [lu.common.Next|Next] describing what action [mainLoop] should take next. 1173 +/ 1174 auto listenAttemptToNext(ref Kameloso instance, const ListenAttempt attempt) 1175 { 1176 // Handle the attempt; switch on its state 1177 with (ListenAttempt.State) 1178 final switch (attempt.state) 1179 { 1180 case unset: // should never happen 1181 case prelisten: // ditto 1182 import lu.conv : Enum; 1183 import std.conv : text; 1184 assert(0, text("listener yielded `", Enum!(ListenAttempt.State).toString(attempt.state), "` state")); 1185 1186 case isEmpty: 1187 // Empty line yielded means nothing received; break foreach and try again 1188 return Next.retry; 1189 1190 case hasString: 1191 // hasString means we should drop down and continue processing 1192 return Next.continue_; 1193 1194 case warning: 1195 // Benign socket error; break foreach and try again 1196 import kameloso.constants : Timeout; 1197 import kameloso.thread : interruptibleSleep; 1198 import core.time : msecs; 1199 1200 version(Posix) 1201 { 1202 import kameloso.common : errnoStrings; 1203 enum pattern = "Connection error! (<l>%s</>) (<t>%s</>)"; 1204 logger.warningf(pattern, attempt.error, errnoStrings[attempt.errno]); 1205 } 1206 else version(Windows) 1207 { 1208 enum pattern = "Connection error! (<l>%s</>) (<t>%d</>)"; 1209 logger.warningf(pattern, attempt.error, attempt.errno); 1210 } 1211 else 1212 { 1213 static assert(0, "Unsupported platform, please file a bug."); 1214 } 1215 1216 // Sleep briefly so it won't flood the screen on chains of errors 1217 static immutable readErrorGracePeriod = Timeout.readErrorGracePeriodMsecs.msecs; 1218 interruptibleSleep(readErrorGracePeriod, *instance.abort); 1219 return Next.retry; 1220 1221 case timeout: 1222 // No point printing the errno, it'll just be EAGAIN or EWOULDBLOCK. 1223 logger.error("Connection timed out."); 1224 instance.conn.connected = false; 1225 return Next.returnFailure; 1226 1227 case error: 1228 if (attempt.bytesReceived == 0) 1229 { 1230 //logger.error("Connection error: empty server response!"); 1231 logger.error("Connection lost."); 1232 } 1233 else 1234 { 1235 version(Posix) 1236 { 1237 import kameloso.common : errnoStrings; 1238 enum pattern = "Connection error: invalid server response! (<l>%s</>) (<t>%s</>)"; 1239 logger.errorf(pattern, attempt.error, errnoStrings[attempt.errno]); 1240 } 1241 else version(Windows) 1242 { 1243 enum pattern = "Connection error: invalid server response! (<l>%s</>) (<t>%d</>)"; 1244 logger.errorf(pattern, attempt.error, attempt.errno); 1245 } 1246 else 1247 { 1248 static assert(0, "Unsupported platform, please file a bug."); 1249 } 1250 } 1251 1252 instance.conn.connected = false; 1253 return Next.returnFailure; 1254 } 1255 } 1256 1257 1258 // processLineFromServer 1259 /++ 1260 Processes a line read from the server, constructing an 1261 [dialect.defs.IRCEvent|IRCEvent] and dispatches it to all plugins. 1262 1263 Params: 1264 instance = The current [kameloso.kameloso.Kameloso|Kameloso] instance. 1265 raw = A raw line as read from the server. 1266 nowInUnix = Current timestamp in UNIX time. 1267 +/ 1268 void processLineFromServer(ref Kameloso instance, const string raw, const long nowInUnix) 1269 { 1270 import kameloso.string : doublyBackslashed; 1271 import dialect.common : IRCParseException; 1272 import lu.string : NomException; 1273 import std.typecons : Flag, No, Yes; 1274 import std.utf : UTFException; 1275 import core.exception : UnicodeException; 1276 1277 // Delay initialising the event so we don't do it twice; 1278 // once here, once in toIRCEvent 1279 IRCEvent event = void; 1280 bool eventWasInitialised; 1281 1282 scope(failure) 1283 { 1284 if (!instance.settings.headless) 1285 { 1286 import lu.string : contains; 1287 import std.algorithm.searching : canFind; 1288 1289 // Something asserted 1290 logger.error("scopeguard tripped."); 1291 printEventDebugDetails(event, raw, cast(Flag!"eventWasInitialised")eventWasInitialised); 1292 1293 // Print the raw line char by char if it contains non-printables 1294 if (raw.canFind!((c) => c < ' ')) 1295 { 1296 import std.stdio : writefln; 1297 import std.string : representation; 1298 1299 foreach (immutable c; raw.representation) 1300 { 1301 writefln("%3d: '%c'", c, cast(char)c); 1302 } 1303 } 1304 1305 if (instance.settings.flush) stdout.flush(); 1306 } 1307 } 1308 1309 try 1310 { 1311 // Sanitise and try again once on UTF/Unicode exceptions 1312 import std.encoding : sanitize; 1313 1314 try 1315 { 1316 event = instance.parser.toIRCEvent(raw); 1317 } 1318 catch (UTFException e) 1319 { 1320 event = instance.parser.toIRCEvent(sanitize(raw)); 1321 if (event.errors.length) event.errors ~= " | "; 1322 event.errors ~= "UTFException: " ~ e.msg; 1323 } 1324 catch (UnicodeException e) 1325 { 1326 event = instance.parser.toIRCEvent(sanitize(raw)); 1327 if (event.errors.length) event.errors ~= " | "; 1328 event.errors ~= "UnicodeException: " ~ e.msg; 1329 } 1330 1331 eventWasInitialised = true; 1332 1333 // Save timestamp in the event itself. 1334 event.time = nowInUnix; 1335 1336 version(TwitchSupport) 1337 { 1338 if (instance.parser.server.daemon == IRCServer.Daemon.twitch && event.content.length) 1339 { 1340 import std.algorithm.searching : endsWith; 1341 1342 /+ 1343 On Twitch, sometimes the content string ends with an invisible 1344 [ 243, 160, 128, 128 ], possibly because of a browser extension 1345 circumventing the duplicate message block. 1346 1347 It wrecks things. So slice it away if detected. 1348 +/ 1349 1350 static immutable ubyte[] badTail = [ 243, 160, 128, 128 ]; 1351 1352 if ((cast(ubyte[])event.content).endsWith(badTail)) 1353 { 1354 event.content = cast(string)(cast(ubyte[])event.content[0..$-badTail.length]); 1355 } 1356 } 1357 } 1358 1359 version(TwitchSupport) 1360 { 1361 // If it's an RPL_WELCOME event, record it as having been seen so we 1362 // know we can't reconnect without waiting a bit. 1363 if (event.type == IRCEvent.Type.RPL_WELCOME) 1364 { 1365 instance.flags.sawWelcome = true; 1366 } 1367 } 1368 1369 alias ParserUpdates = typeof(instance.parser.updates); 1370 1371 if (instance.parser.updates & ParserUpdates.client) 1372 { 1373 // Parsing changed the client; propagate 1374 instance.parser.updates &= ~ParserUpdates.client; 1375 instance.propagate(instance.parser.client); 1376 } 1377 1378 if (instance.parser.updates & ParserUpdates.server) 1379 { 1380 // Parsing changed the server; propagate 1381 instance.parser.updates &= ~ParserUpdates.server; 1382 instance.propagate(instance.parser.server); 1383 } 1384 1385 // Let each plugin postprocess the event 1386 foreach (plugin; instance.plugins) 1387 { 1388 if (!plugin.isEnabled) continue; 1389 1390 try 1391 { 1392 plugin.postprocess(event); 1393 } 1394 catch (NomException e) 1395 { 1396 enum pattern = `NomException %s.postprocess: tried to nom "<l>%s</>" with "<l>%s</>"`; 1397 logger.warningf(pattern, plugin.name, e.haystack, e.needle); 1398 printEventDebugDetails(event, raw); 1399 version(PrintStacktraces) logger.trace(e.info); 1400 } 1401 catch (UTFException e) 1402 { 1403 enum pattern = "UTFException %s.postprocess: <l>%s"; 1404 logger.warningf(pattern, plugin.name, e.msg); 1405 version(PrintStacktraces) logger.trace(e.info); 1406 } 1407 catch (UnicodeException e) 1408 { 1409 enum pattern = "UnicodeException %s.postprocess: <l>%s"; 1410 logger.warningf(pattern, plugin.name, e.msg); 1411 version(PrintStacktraces) logger.trace(e.info); 1412 } 1413 catch (Exception e) 1414 { 1415 enum pattern = "Exception %s.postprocess: <l>%s"; 1416 logger.warningf(pattern, plugin.name, e.msg); 1417 printEventDebugDetails(event, raw); 1418 version(PrintStacktraces) logger.trace(e); 1419 } 1420 finally 1421 { 1422 if (plugin.state.updates != typeof(plugin.state.updates).nothing) 1423 { 1424 instance.checkPluginForUpdates(plugin); 1425 } 1426 } 1427 } 1428 1429 // Let each plugin process the event 1430 foreach (plugin; instance.plugins) 1431 { 1432 if (!plugin.isEnabled) continue; 1433 1434 try 1435 { 1436 plugin.onEvent(event); 1437 if (plugin.state.hasPendingReplays) processPendingReplays(instance, plugin); 1438 if (plugin.state.readyReplays.length) processReadyReplays(instance, plugin); 1439 processAwaitingDelegates(plugin, event); 1440 processAwaitingFibers(plugin, event); 1441 if (*instance.abort) return; // handled in mainLoop listenerloop 1442 } 1443 catch (NomException e) 1444 { 1445 enum pattern = `NomException %s: tried to nom "<l>%s</>" with "<l>%s</>"`; 1446 logger.warningf(pattern, plugin.name, e.haystack, e.needle); 1447 printEventDebugDetails(event, raw); 1448 version(PrintStacktraces) logger.trace(e.info); 1449 } 1450 catch (UTFException e) 1451 { 1452 enum pattern = "UTFException %s: <l>%s"; 1453 logger.warningf(pattern, plugin.name, e.msg); 1454 version(PrintStacktraces) logger.trace(e.info); 1455 } 1456 catch (UnicodeException e) 1457 { 1458 enum pattern = "UnicodeException %s: <l>%s"; 1459 logger.warningf(pattern, plugin.name, e.msg); 1460 version(PrintStacktraces) logger.trace(e.info); 1461 } 1462 catch (Exception e) 1463 { 1464 enum pattern = "Exception %s: <l>%s"; 1465 logger.warningf(pattern, plugin.name, e.msg); 1466 printEventDebugDetails(event, raw); 1467 version(PrintStacktraces) logger.trace(e); 1468 } 1469 finally 1470 { 1471 if (plugin.state.updates != typeof(plugin.state.updates).nothing) 1472 { 1473 instance.checkPluginForUpdates(plugin); 1474 } 1475 } 1476 } 1477 1478 // Take some special actions on select event types 1479 with (IRCEvent.Type) 1480 switch (event.type) 1481 { 1482 case SELFCHAN: 1483 case SELFEMOTE: 1484 case SELFQUERY: 1485 // Treat self-events as if we sent them ourselves, to properly 1486 // rate-limit the account itself. This stops Twitch from 1487 // giving spam warnings. We can easily tell whether it's a channel 1488 // we're the broadcaster in, but no such luck with whether 1489 // we're a moderator. For now, just assume we're moderator 1490 // in all our home channels. 1491 1492 version(TwitchSupport) 1493 { 1494 import std.algorithm.searching : canFind; 1495 1496 // Send faster in home channels. Assume we're a mod and won't be throttled. 1497 // (There's no easy way to tell from here.) 1498 if (event.channel.length && instance.bot.homeChannels.canFind(event.channel)) 1499 { 1500 instance.throttleline(instance.fastbuffer, Yes.dryRun, Yes.sendFaster); 1501 } 1502 else 1503 { 1504 instance.throttleline(instance.outbuffer, Yes.dryRun); 1505 } 1506 } 1507 else 1508 { 1509 instance.throttleline(instance.outbuffer, Yes.dryRun); 1510 } 1511 break; 1512 1513 case QUIT: 1514 // Remove users from the WHOIS history when they quit the server. 1515 instance.previousWhoisTimestamps.remove(event.sender.nickname); 1516 break; 1517 1518 case NICK: 1519 // Transfer WHOIS history timestamp when a user changes its nickname. 1520 if (const timestamp = event.sender.nickname in instance.previousWhoisTimestamps) 1521 { 1522 instance.previousWhoisTimestamps[event.target.nickname] = *timestamp; 1523 instance.previousWhoisTimestamps.remove(event.sender.nickname); 1524 } 1525 break; 1526 1527 default: 1528 break; 1529 } 1530 } 1531 catch (IRCParseException e) 1532 { 1533 enum pattern = "IRCParseException: <l>%s</> (at <l>%s</>:<l>%d</>)"; 1534 logger.warningf(pattern, e.msg, e.file.doublyBackslashed, e.line); 1535 printEventDebugDetails(event, raw); 1536 version(PrintStacktraces) logger.trace(e.info); 1537 } 1538 catch (NomException e) 1539 { 1540 enum pattern = `NomException: tried to nom "<l>%s</>" with "<l>%s</>" (at <l>%s</>:<l>%d</>)`; 1541 logger.warningf(pattern, e.haystack, e.needle, e.file.doublyBackslashed, e.line); 1542 printEventDebugDetails(event, raw); 1543 version(PrintStacktraces) logger.trace(e.info); 1544 } 1545 catch (UTFException e) 1546 { 1547 enum pattern = "UTFException: <l>%s"; 1548 logger.warningf(pattern, e.msg); 1549 version(PrintStacktraces) logger.trace(e.info); 1550 } 1551 catch (UnicodeException e) 1552 { 1553 enum pattern = "UnicodeException: <l>%s"; 1554 logger.warningf(pattern, e.msg); 1555 version(PrintStacktraces) logger.trace(e.info); 1556 } 1557 catch (Exception e) 1558 { 1559 enum pattern = "Unhandled exception: <l>%s</> (at <l>%s</>:<l>%d</>)"; 1560 logger.warningf(pattern, e.msg, e.file.doublyBackslashed, e.line); 1561 printEventDebugDetails(event, raw); 1562 version(PrintStacktraces) logger.trace(e); 1563 } 1564 } 1565 1566 1567 // processAwaitingDelegates 1568 /++ 1569 Processes the awaiting delegates of an 1570 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1571 1572 Does not remove delegates after calling them. They are expected to remove 1573 themselves after finishing if they aren't awaiting any further events. 1574 1575 Params: 1576 plugin = The [kameloso.plugins.common.core.IRCPlugin|IRCPlugin] whose 1577 [dialect.defs.IRCEvent.Type|IRCEvent.Type]-awaiting delegates to 1578 iterate and process. 1579 event = The triggering const [dialect.defs.IRCEvent|IRCEvent]. 1580 +/ 1581 void processAwaitingDelegates(IRCPlugin plugin, const ref IRCEvent event) 1582 { 1583 /++ 1584 Handle awaiting delegates of a specified type. 1585 +/ 1586 void processImpl(void delegate(IRCEvent)[] dgsForType) 1587 { 1588 foreach (immutable i, dg; dgsForType) 1589 { 1590 try 1591 { 1592 dg(event); 1593 } 1594 catch (Exception e) 1595 { 1596 enum pattern = "Exception %s.awaitingDelegates[%d]: <l>%s"; 1597 logger.warningf(pattern, plugin.name, i, e.msg); 1598 printEventDebugDetails(event, event.raw); 1599 version(PrintStacktraces) logger.trace(e); 1600 } 1601 } 1602 } 1603 1604 if (plugin.state.awaitingDelegates[event.type].length) 1605 { 1606 processImpl(plugin.state.awaitingDelegates[event.type]); 1607 } 1608 1609 if (plugin.state.awaitingDelegates[IRCEvent.Type.ANY].length) 1610 { 1611 processImpl(plugin.state.awaitingDelegates[IRCEvent.Type.ANY]); 1612 } 1613 } 1614 1615 1616 // processAwaitingFibers 1617 /++ 1618 Processes the awaiting [core.thread.fiber.Fiber|Fiber]s of an 1619 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1620 1621 Don't delete [core.thread.fiber.Fiber|Fiber]s, as they can be reset and reused. 1622 1623 Params: 1624 plugin = The [kameloso.plugins.common.core.IRCPlugin|IRCPlugin] whose 1625 [dialect.defs.IRCEvent.Type|IRCEvent.Type]-awaiting 1626 [core.thread.fiber.Fiber|Fiber]s to iterate and process. 1627 event = The triggering [dialect.defs.IRCEvent|IRCEvent]. 1628 +/ 1629 void processAwaitingFibers(IRCPlugin plugin, const ref IRCEvent event) 1630 { 1631 import core.thread : Fiber; 1632 1633 /++ 1634 Handle awaiting Fibers of a specified type. 1635 +/ 1636 void processAwaitingFibersImpl( 1637 Fiber[] fibersForType, 1638 ref Fiber[] expiredFibers) 1639 { 1640 foreach (immutable i, fiber; fibersForType) 1641 { 1642 try 1643 { 1644 if (fiber.state == Fiber.State.HOLD) 1645 { 1646 import kameloso.thread : CarryingFiber; 1647 1648 // Specialcase CarryingFiber!IRCEvent to update it to carry 1649 // the current IRCEvent. 1650 1651 if (auto carryingFiber = cast(CarryingFiber!IRCEvent)fiber) 1652 { 1653 carryingFiber.payload = event; 1654 carryingFiber.call(); 1655 1656 // We need to reset the payload so that we can differentiate 1657 // between whether the Fiber was called due to an incoming 1658 // (awaited) event or due to a timer. delegates will have 1659 // to cache the event if they don't want it to get reset. 1660 carryingFiber.resetPayload(); 1661 } 1662 else 1663 { 1664 fiber.call(); 1665 } 1666 } 1667 1668 if (fiber.state == Fiber.State.TERM) 1669 { 1670 expiredFibers ~= fiber; 1671 } 1672 } 1673 catch (Exception e) 1674 { 1675 enum pattern = "Exception %s.awaitingFibers[%d]: <l>%s"; 1676 logger.warningf(pattern, plugin.name, i, e.msg); 1677 printEventDebugDetails(event, event.raw); 1678 version(PrintStacktraces) logger.trace(e); 1679 expiredFibers ~= fiber; 1680 } 1681 } 1682 } 1683 1684 Fiber[] expiredFibers; 1685 1686 if (plugin.state.awaitingFibers[event.type].length) 1687 { 1688 processAwaitingFibersImpl( 1689 plugin.state.awaitingFibers[event.type], 1690 expiredFibers); 1691 } 1692 1693 if (plugin.state.awaitingFibers[IRCEvent.Type.ANY].length) 1694 { 1695 processAwaitingFibersImpl( 1696 plugin.state.awaitingFibers[IRCEvent.Type.ANY], 1697 expiredFibers); 1698 } 1699 1700 // Clean up processed Fibers 1701 foreach (expiredFiber; expiredFibers) 1702 { 1703 // Detect duplicates that were already destroyed and skip 1704 if (!expiredFiber) continue; 1705 1706 foreach (ref fibersByType; plugin.state.awaitingFibers) 1707 { 1708 foreach_reverse (immutable i, /*ref*/ fiber; fibersByType) 1709 { 1710 import std.algorithm.mutation : SwapStrategy, remove; 1711 1712 if (fiber is expiredFiber) 1713 { 1714 fibersByType = fibersByType.remove!(SwapStrategy.unstable)(i); 1715 } 1716 } 1717 } 1718 1719 destroy(expiredFiber); 1720 } 1721 } 1722 1723 1724 // processScheduledDelegates 1725 /++ 1726 Processes the queued [kameloso.thread.ScheduledDelegate|ScheduledDelegate]s of an 1727 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1728 1729 Params: 1730 plugin = The [kameloso.plugins.common.core.IRCPlugin|IRCPlugin] whose 1731 queued [kameloso.thread.ScheduledDelegate|ScheduledDelegate]s to 1732 iterate and process. 1733 nowInHnsecs = Current timestamp to compare the 1734 [kameloso.thread.ScheduledDelegate|ScheduledDelegate]'s timestamp with. 1735 +/ 1736 void processScheduledDelegates(IRCPlugin plugin, const long nowInHnsecs) 1737 in ((nowInHnsecs > 0), "Tried to process queued `ScheduledDelegate`s with an unset timestamp") 1738 { 1739 size_t[] toRemove; 1740 1741 foreach (immutable i, scheduledDg; plugin.state.scheduledDelegates) 1742 { 1743 if (scheduledDg.timestamp > nowInHnsecs) continue; 1744 1745 try 1746 { 1747 scheduledDg.dg(); 1748 } 1749 catch (Exception e) 1750 { 1751 enum pattern = "Exception %s.scheduledDelegates[%d]: <l>%s"; 1752 logger.warningf(pattern, plugin.name, i, e.msg); 1753 version(PrintStacktraces) logger.trace(e); 1754 } 1755 finally 1756 { 1757 destroy(scheduledDg.dg); 1758 } 1759 1760 toRemove ~= i; // Always removed a scheduled delegate after processing 1761 } 1762 1763 // Clean up processed delegates 1764 foreach_reverse (immutable i; toRemove) 1765 { 1766 import std.algorithm.mutation : SwapStrategy, remove; 1767 plugin.state.scheduledDelegates = plugin.state.scheduledDelegates 1768 .remove!(SwapStrategy.unstable)(i); 1769 } 1770 } 1771 1772 1773 // processScheduledFibers 1774 /++ 1775 Processes the queued [kameloso.thread.ScheduledFiber|ScheduledFiber]s of an 1776 [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1777 1778 Params: 1779 plugin = The [kameloso.plugins.common.core.IRCPlugin|IRCPlugin] whose 1780 queued [kameloso.thread.ScheduledFiber|ScheduledFiber]s to iterate 1781 and process. 1782 nowInHnsecs = Current timestamp to compare the 1783 [kameloso.thread.ScheduledFiber|ScheduledFiber]'s timestamp with. 1784 +/ 1785 void processScheduledFibers(IRCPlugin plugin, const long nowInHnsecs) 1786 in ((nowInHnsecs > 0), "Tried to process queued `ScheduledFiber`s with an unset timestamp") 1787 { 1788 import core.thread : Fiber; 1789 1790 size_t[] toRemove; 1791 1792 foreach (immutable i, scheduledFiber; plugin.state.scheduledFibers) 1793 { 1794 if (scheduledFiber.timestamp > nowInHnsecs) continue; 1795 1796 try 1797 { 1798 if (scheduledFiber.fiber.state == Fiber.State.HOLD) 1799 { 1800 scheduledFiber.fiber.call(); 1801 } 1802 } 1803 catch (Exception e) 1804 { 1805 enum pattern = "Exception %s.scheduledFibers[%d]: <l>%s"; 1806 logger.warningf(pattern, plugin.name, i, e.msg); 1807 version(PrintStacktraces) logger.trace(e); 1808 } 1809 finally 1810 { 1811 // destroy the Fiber if it has ended 1812 if (scheduledFiber.fiber.state == Fiber.State.TERM) 1813 { 1814 destroy(scheduledFiber.fiber); 1815 } 1816 } 1817 1818 // Always removed a scheduled Fiber after processing 1819 toRemove ~= i; 1820 } 1821 1822 // Clean up processed Fibers 1823 foreach_reverse (immutable i; toRemove) 1824 { 1825 import std.algorithm.mutation : SwapStrategy, remove; 1826 plugin.state.scheduledFibers = plugin.state.scheduledFibers 1827 .remove!(SwapStrategy.unstable)(i); 1828 } 1829 } 1830 1831 1832 // processReadyReplays 1833 /++ 1834 Handles the queue of ready-to-replay objects, re-postprocessing events from the 1835 current (main loop) context, outside of any plugin. 1836 1837 Params: 1838 instance = Reference to the current bot instance. 1839 plugin = The current [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1840 +/ 1841 void processReadyReplays(ref Kameloso instance, IRCPlugin plugin) 1842 { 1843 import lu.string : NomException; 1844 import std.utf : UTFException; 1845 import core.exception : UnicodeException; 1846 import core.thread : Fiber; 1847 1848 foreach (immutable i, replay; plugin.state.readyReplays) 1849 { 1850 version(WithPersistenceService) 1851 { 1852 // Postprocessing will reapply class, but not if there is already 1853 // a custom class (assuming channel cache hit) 1854 replay.event.sender.class_ = IRCUser.Class.unset; 1855 replay.event.target.class_ = IRCUser.Class.unset; 1856 } 1857 1858 try 1859 { 1860 foreach (postprocessor; instance.plugins) 1861 { 1862 postprocessor.postprocess(replay.event); 1863 } 1864 } 1865 catch (NomException e) 1866 { 1867 enum pattern = "NomException postprocessing %s.state.readyReplays[%d]: " ~ 1868 `tried to nom "<l>%s</>" with "<l>%s</>"`; 1869 logger.warningf(pattern, plugin.name, i, e.haystack, e.needle); 1870 printEventDebugDetails(replay.event, replay.event.raw); 1871 version(PrintStacktraces) logger.trace(e.info); 1872 continue; 1873 } 1874 catch (UTFException e) 1875 { 1876 enum pattern = "UTFException postprocessing %s.state.readyReplace[%d]: <l>%s"; 1877 logger.warningf(pattern, plugin.name, i, e.msg); 1878 version(PrintStacktraces) logger.trace(e.info); 1879 continue; 1880 } 1881 catch (UnicodeException e) 1882 { 1883 enum pattern = "UnicodeException postprocessing %s.state.readyReplace[%d]: <l>%s"; 1884 logger.warningf(pattern, plugin.name, i, e.msg); 1885 version(PrintStacktraces) logger.trace(e.info); 1886 continue; 1887 } 1888 catch (Exception e) 1889 { 1890 enum pattern = "Exception postprocessing %s.state.readyReplace[%d]: <l>%s"; 1891 logger.warningf(pattern, plugin.name, i, e.msg); 1892 printEventDebugDetails(replay.event, replay.event.raw); 1893 version(PrintStacktraces) logger.trace(e); 1894 continue; 1895 } 1896 1897 // If we're here no exceptions were thrown 1898 1899 try 1900 { 1901 replay.dg(replay); 1902 } 1903 catch (Exception e) 1904 { 1905 enum pattern = "Exception %s.state.readyReplays[%d].dg(): <l>%s"; 1906 logger.warningf(pattern, plugin.name, i, e.msg); 1907 printEventDebugDetails(replay.event, replay.event.raw); 1908 version(PrintStacktraces) logger.trace(e); 1909 } 1910 finally 1911 { 1912 destroy(replay.dg); 1913 } 1914 } 1915 1916 // All ready replays guaranteed exhausted 1917 plugin.state.readyReplays = null; 1918 } 1919 1920 1921 // processPendingReplay 1922 /++ 1923 Takes a queue of pending [kameloso.plugins.common.core.Replay|Replay] 1924 objects and issues WHOIS queries for each one, unless it has already been done 1925 recently (within [kameloso.constants.Timeout.whoisRetry|Timeout.whoisRetry] seconds). 1926 1927 Params: 1928 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 1929 plugin = The relevant [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 1930 +/ 1931 void processPendingReplays(ref Kameloso instance, IRCPlugin plugin) 1932 { 1933 import kameloso.constants : Timeout; 1934 import kameloso.messaging : Message, whois; 1935 import std.datetime.systime : Clock; 1936 1937 // Walk through replays and call WHOIS on those that haven't been 1938 // WHOISed in the last Timeout.whoisRetry seconds 1939 1940 immutable now = Clock.currTime.toUnixTime; 1941 1942 foreach (immutable nickname, replaysForNickname; plugin.state.pendingReplays) 1943 { 1944 version(TraceWhois) 1945 { 1946 import std.stdio : writef, writefln, writeln; 1947 1948 if (!instance.settings.headless) 1949 { 1950 import std.algorithm.iteration : map; 1951 1952 auto callerNames = replaysForNickname.map!(replay => replay.caller); 1953 enum pattern = "[TraceWhois] processReplays saw request to " ~ 1954 "WHOIS \"%s\" from: %-(%s, %)"; 1955 writef(pattern, nickname, callerNames); 1956 } 1957 } 1958 1959 immutable lastWhois = instance.previousWhoisTimestamps.get(nickname, 0L); 1960 1961 if ((now - lastWhois) > Timeout.whoisRetry) 1962 { 1963 version(TraceWhois) 1964 { 1965 if (!instance.settings.headless) 1966 { 1967 writeln(" ...and actually issuing."); 1968 } 1969 } 1970 1971 /*instance.outbuffer.put(OutgoingLine("WHOIS " ~ nickname, 1972 cast(Flag!"quiet")instance.settings.hideOutgoing)); 1973 instance.previousWhoisTimestamps[nickname] = now; 1974 propagateWhoisTimestamp(instance, nickname, now);*/ 1975 1976 enum properties = (Message.Property.forced | Message.Property.quiet); 1977 whois(plugin.state, nickname, properties); 1978 } 1979 else 1980 { 1981 version(TraceWhois) 1982 { 1983 if (!instance.settings.headless) 1984 { 1985 writefln(" ...but already issued %d seconds ago.", (now - lastWhois)); 1986 } 1987 } 1988 } 1989 1990 version(TraceWhois) 1991 { 1992 if (instance.settings.flush) stdout.flush(); 1993 } 1994 } 1995 } 1996 1997 1998 // processSpecialRequests 1999 /++ 2000 Iterates through a plugin's array of [kameloso.plugins.common.core.SpecialRequest|SpecialRequest]s. 2001 Depending on what their [kameloso.plugins.common.core.SpecialRequest.fiber|fiber] member 2002 (which is in actualy a [kameloso.thread.CarryingFiber|CarryingFiber]) can be 2003 cast to, it prepares a payload, assigns it to the 2004 [kameloso.thread.CarryingFiber|CarryingFiber], and calls it. 2005 2006 If plugins need support for new types of requests, they must be defined and 2007 hardcoded here. There's no way to let plugins process the requests themselves 2008 without letting them peek into [kameloso.kameloso.Kameloso|the Kameloso instance]. 2009 2010 The array is always cleared after iteration, so requests that yield must 2011 first re-queue themselves. 2012 2013 Params: 2014 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2015 plugin = The relevant [kameloso.plugins.common.core.IRCPlugin|IRCPlugin]. 2016 +/ 2017 void processSpecialRequests(ref Kameloso instance, IRCPlugin plugin) 2018 { 2019 import kameloso.thread : CarryingFiber; 2020 import std.typecons : Tuple; 2021 import core.thread : Fiber; 2022 2023 auto specialRequestsSnapshot = plugin.state.specialRequests; 2024 plugin.state.specialRequests = null; 2025 2026 top: 2027 foreach (request; specialRequestsSnapshot) 2028 { 2029 scope(exit) 2030 { 2031 if (request.fiber.state == Fiber.State.TERM) 2032 { 2033 // Clean up 2034 destroy(request.fiber); 2035 } 2036 2037 destroy(request); 2038 } 2039 2040 alias PeekCommandsPayload = Tuple!(IRCPlugin.CommandMetadata[string][string]); 2041 alias GetSettingPayload = Tuple!(string, string, string); 2042 alias SetSettingPayload = Tuple!(bool); 2043 2044 if (auto fiber = cast(CarryingFiber!(PeekCommandsPayload))(request.fiber)) 2045 { 2046 immutable channelName = request.context; 2047 2048 IRCPlugin.CommandMetadata[string][string] commandAA; 2049 2050 foreach (thisPlugin; instance.plugins) 2051 { 2052 if (channelName.length) 2053 { 2054 commandAA[thisPlugin.name] = thisPlugin.channelSpecificCommands(channelName); 2055 } 2056 else 2057 { 2058 commandAA[thisPlugin.name] = thisPlugin.commands; 2059 } 2060 } 2061 2062 fiber.payload[0] = commandAA; 2063 fiber.call(); 2064 continue; 2065 } 2066 else if (auto fiber = cast(CarryingFiber!(GetSettingPayload))(request.fiber)) 2067 { 2068 import lu.string : beginsWith, nom; 2069 import std.array : Appender; 2070 import std.algorithm.iteration : splitter; 2071 2072 immutable expression = request.context; 2073 string slice = expression; // mutable 2074 immutable pluginName = slice.nom!(Yes.inherit)('.'); 2075 alias setting = slice; 2076 2077 Appender!(char[]) sink; 2078 sink.reserve(256); // guesstimate 2079 2080 void apply() 2081 { 2082 if (setting.length) 2083 { 2084 import lu.string : strippedLeft; 2085 2086 foreach (const line; sink.data.splitter('\n')) 2087 { 2088 string lineslice = cast(string)line; // need a string for nom and strippedLeft... 2089 if (lineslice.beginsWith('#')) lineslice = lineslice[1..$]; 2090 const thisSetting = lineslice.nom!(Yes.inherit)(' '); 2091 2092 if (thisSetting != setting) continue; 2093 2094 const value = lineslice.strippedLeft; 2095 fiber.payload[0] = pluginName; 2096 fiber.payload[1] = setting; 2097 fiber.payload[2] = value; 2098 fiber.call(); 2099 return; 2100 } 2101 } 2102 else 2103 { 2104 import std.conv : to; 2105 2106 string[] allSettings; 2107 2108 foreach (const line; sink.data.splitter('\n')) 2109 { 2110 string lineslice = cast(string)line; // need a string for nom and strippedLeft... 2111 if (!lineslice.beginsWith('[')) allSettings ~= lineslice.nom!(Yes.inherit)(' '); 2112 } 2113 2114 fiber.payload[0] = pluginName; 2115 //fiber.payload[1] = string.init; 2116 fiber.payload[2] = allSettings.to!string; 2117 fiber.call(); 2118 return; 2119 } 2120 2121 // If we're here, no such setting was found 2122 fiber.payload[0] = pluginName; 2123 //fiber.payload[1] = string.init; 2124 //fiber.payload[2] = string.init; 2125 fiber.call(); 2126 return; 2127 } 2128 2129 switch (pluginName) 2130 { 2131 case "core": 2132 import lu.serialisation : serialise; 2133 sink.serialise(instance.settings); 2134 apply(); 2135 continue; 2136 2137 case "connection": 2138 // May leak secrets? certFile, privateKey etc... 2139 // Careful with how we make this functionality available 2140 import lu.serialisation : serialise; 2141 sink.serialise(instance.connSettings); 2142 apply(); 2143 continue; 2144 2145 default: 2146 foreach (thisPlugin; instance.plugins) 2147 { 2148 if (thisPlugin.name != pluginName) continue; 2149 thisPlugin.serialiseConfigInto(sink); 2150 apply(); 2151 continue top; 2152 } 2153 2154 // If we're here, no plugin was found 2155 //fiber.payload[0] = string.init; 2156 //fiber.payload[1] = string.init; 2157 //fiber.payload[2] = string.init; 2158 fiber.call(); 2159 continue; 2160 } 2161 } 2162 else if (auto fiber = cast(CarryingFiber!(SetSettingPayload))(request.fiber)) 2163 { 2164 import kameloso.plugins.common.misc : applyCustomSettings; 2165 2166 immutable expression = request.context; 2167 2168 // Borrow settings from the first plugin. It's taken by value 2169 immutable success = applyCustomSettings( 2170 instance.plugins, 2171 [ expression ], 2172 instance.plugins[0].state.settings); 2173 2174 fiber.payload[0] = success; 2175 fiber.call(); 2176 continue; 2177 } 2178 else 2179 { 2180 logger.error("Unknown special request type: " ~ typeof(request).stringof); 2181 } 2182 } 2183 2184 if (plugin.state.specialRequests.length) 2185 { 2186 // One or more new requests were added while processing these ones 2187 return processSpecialRequests(instance, plugin); 2188 } 2189 } 2190 2191 2192 // setupSignals 2193 /++ 2194 Registers some process signals to redirect to our own [signalHandler], so we 2195 can (for instance) catch Ctrl+C and gracefully shut down. 2196 2197 On Posix, additionally ignore `SIGPIPE` so that we can catch SSL errors and 2198 not just immediately terminate. 2199 +/ 2200 void setupSignals() nothrow @nogc 2201 { 2202 import core.stdc.signal : SIGINT, SIGTERM, signal; 2203 2204 signal(SIGINT, &signalHandler); 2205 signal(SIGTERM, &signalHandler); 2206 2207 version(Posix) 2208 { 2209 import core.sys.posix.signal : SIG_IGN, SIGHUP, SIGPIPE, SIGQUIT; 2210 2211 signal(SIGHUP, &signalHandler); 2212 signal(SIGQUIT, &signalHandler); 2213 signal(SIGPIPE, SIG_IGN); 2214 } 2215 } 2216 2217 2218 // resetSignals 2219 /++ 2220 Resets signal handlers to the system default. 2221 +/ 2222 void resetSignals() nothrow @nogc 2223 { 2224 import core.stdc.signal : SIG_DFL, SIGINT, SIGTERM, signal; 2225 2226 signal(SIGINT, SIG_DFL); 2227 signal(SIGTERM, SIG_DFL); 2228 2229 version(Posix) 2230 { 2231 import core.sys.posix.signal : SIGHUP, SIGQUIT; 2232 2233 signal(SIGHUP, SIG_DFL); 2234 signal(SIGQUIT, SIG_DFL); 2235 } 2236 } 2237 2238 2239 // tryGetopt 2240 /++ 2241 Attempt handling `getopt`, wrapped in try-catch blocks. 2242 2243 Params: 2244 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2245 2246 Returns: 2247 [lu.common.Next|Next].* depending on what action the calling site should take. 2248 +/ 2249 auto tryGetopt(ref Kameloso instance) 2250 { 2251 import kameloso.plugins.common.misc : IRCPluginSettingsException; 2252 import kameloso.config : handleGetopt; 2253 import kameloso.configreader : ConfigurationFileReadFailureException; 2254 import kameloso.string : doublyBackslashed; 2255 import lu.common : FileTypeMismatchException; 2256 import lu.serialisation : DeserialisationException; 2257 import std.conv : ConvException; 2258 import std.getopt : GetOptException; 2259 import std.process : ProcessException; 2260 2261 try 2262 { 2263 // Act on arguments getopt, pass return value to main 2264 return handleGetopt(instance); 2265 } 2266 catch (GetOptException e) 2267 { 2268 enum pattern = "Error parsing command-line arguments: <l>%s"; 2269 logger.errorf(pattern, e.msg); 2270 //version(PrintStacktraces) logger.trace(e.info); 2271 } 2272 catch (ConvException e) 2273 { 2274 enum pattern = "Error converting command-line arguments: <l>%s"; 2275 logger.errorf(pattern, e.msg); 2276 //version(PrintStacktraces) logger.trace(e.info); 2277 } 2278 catch (FileTypeMismatchException e) 2279 { 2280 enum pattern = "Specified configuration file <l>%s</> is not a file!"; 2281 logger.errorf(pattern, e.filename.doublyBackslashed); 2282 //version(PrintStacktraces) logger.trace(e.info); 2283 } 2284 catch (ConfigurationFileReadFailureException e) 2285 { 2286 enum pattern = "Error reading and decoding configuration file [<l>%s</>]: <l>%s"; 2287 logger.errorf(pattern, e.filename.doublyBackslashed, e.msg); 2288 version(PrintStacktraces) logger.trace(e.info); 2289 } 2290 catch (DeserialisationException e) 2291 { 2292 enum pattern = "Error parsing configuration file: <l>%s"; 2293 logger.errorf(pattern, e.msg); 2294 version(PrintStacktraces) logger.trace(e.info); 2295 } 2296 catch (ProcessException e) 2297 { 2298 enum pattern = "Failed to open <l>%s</> in an editor: <l>%s"; 2299 logger.errorf(pattern, instance.settings.configFile.doublyBackslashed, e.msg); 2300 version(PrintStacktraces) logger.trace(e.info); 2301 } 2302 catch (IRCPluginSettingsException e) 2303 { 2304 // Can be thrown from printSettings 2305 logger.error(e.msg); 2306 version(PrintStacktraces) logger.trace(e.info); 2307 } 2308 catch (Exception e) 2309 { 2310 enum pattern = "Unexpected exception: <l>%s"; 2311 logger.errorf(pattern, e.msg); 2312 version(PrintStacktraces) logger.trace(e); 2313 } 2314 2315 return Next.returnFailure; 2316 } 2317 2318 2319 // tryConnect 2320 /++ 2321 Tries to connect to the IPs in 2322 [kameloso.kameloso.Kameloso.conn.ips|Kameloso.conn.ips] by leveraging 2323 [kameloso.net.connectFiber|connectFiber], reacting on the 2324 [kameloso.net.ConnectionAttempt|ConnectionAttempt]s it yields to provide feedback 2325 to the user. 2326 2327 Params: 2328 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2329 2330 Returns: 2331 [lu.common.Next.continue_|Next.continue_] if connection succeeded, 2332 [lu.common.Next.returnFailure|Next.returnFailure] if connection failed 2333 and the program should exit. 2334 +/ 2335 auto tryConnect(ref Kameloso instance) 2336 { 2337 import kameloso.constants : ConnectionDefaultFloats, 2338 ConnectionDefaultIntegers, MagicErrorStrings, Timeout; 2339 import kameloso.net : ConnectionAttempt, connectFiber; 2340 import kameloso.thread : interruptibleSleep; 2341 import std.concurrency : Generator; 2342 2343 auto connector = new Generator!ConnectionAttempt(() => 2344 connectFiber(instance.conn, ConnectionDefaultIntegers.retries, *instance.abort)); 2345 2346 scope(exit) destroy(connector); 2347 2348 try 2349 { 2350 connector.call(); 2351 } 2352 catch (Exception e) 2353 { 2354 /+ 2355 We can only detect SSL context creation failure based on the string 2356 in the generic Exception thrown, sadly. 2357 +/ 2358 if (e.msg == MagicErrorStrings.sslContextCreationFailure) 2359 { 2360 enum message = "Connection error: <l>" ~ 2361 MagicErrorStrings.sslLibraryNotFoundRewritten ~ 2362 " <t>(is OpenSSL installed?)"; 2363 enum wikiMessage = cast(string)MagicErrorStrings.visitWikiOneliner; 2364 logger.error(message); 2365 logger.error(wikiMessage); 2366 2367 version(Windows) 2368 { 2369 enum getoptMessage = cast(string)MagicErrorStrings.getOpenSSLSuggestion; 2370 logger.error(getoptMessage); 2371 } 2372 } 2373 else 2374 { 2375 enum pattern = "Connection error: <l>%s"; 2376 logger.errorf(pattern, e.msg); 2377 } 2378 2379 return Next.returnFailure; 2380 } 2381 2382 uint incrementedRetryDelay = Timeout.connectionRetry; 2383 enum transientSSLFailureTolerance = 10; 2384 uint numTransientSSLFailures; 2385 2386 foreach (const attempt; connector) 2387 { 2388 import lu.string : beginsWith; 2389 import core.time : seconds; 2390 2391 if (*instance.abort) return Next.returnFailure; 2392 2393 immutable lastRetry = (attempt.retryNum+1 == ConnectionDefaultIntegers.retries); 2394 2395 enum unableToConnectString = "Unable to connect socket: "; 2396 immutable errorString = attempt.error.length ? 2397 (attempt.error.beginsWith(unableToConnectString) ? 2398 attempt.error[unableToConnectString.length..$] : 2399 attempt.error) : 2400 string.init; 2401 2402 void verboselyDelay() 2403 { 2404 enum pattern = "Retrying in <i>%d</> seconds..."; 2405 logger.logf(pattern, incrementedRetryDelay); 2406 interruptibleSleep(incrementedRetryDelay.seconds, *instance.abort); 2407 2408 import std.algorithm.comparison : min; 2409 incrementedRetryDelay = cast(uint)(incrementedRetryDelay * 2410 ConnectionDefaultFloats.delayIncrementMultiplier); 2411 incrementedRetryDelay = min(incrementedRetryDelay, Timeout.connectionDelayCap); 2412 } 2413 2414 void verboselyDelayToNextIP() 2415 { 2416 enum pattern = "Failed to connect to IP. Trying next IP in <i>%d</> seconds."; 2417 logger.logf(pattern, Timeout.connectionRetry); 2418 incrementedRetryDelay = Timeout.connectionRetry; 2419 interruptibleSleep(Timeout.connectionRetry.seconds, *instance.abort); 2420 } 2421 2422 with (ConnectionAttempt.State) 2423 final switch (attempt.state) 2424 { 2425 case unset: // should never happen 2426 assert(0, "connector yielded `unset` state"); 2427 2428 case preconnect: 2429 import lu.common : sharedDomains; 2430 import std.socket : AddressException, AddressFamily; 2431 2432 string resolvedHost; // mutable 2433 2434 if (!instance.settings.numericAddresses) 2435 { 2436 try 2437 { 2438 resolvedHost = attempt.ip.toHostNameString; 2439 } 2440 catch (AddressException e) 2441 { 2442 /* 2443 std.socket.AddressException@std/socket.d(1301): Could not get host name: Success 2444 ---------------- 2445 ??:? pure @safe bool std.exception.enforce!(bool).enforce(bool, lazy object.Throwable) [0x2cf5f0] 2446 ??:? const @trusted immutable(char)[] std.socket.Address.toHostString(bool) [0x4b2d7c6] 2447 */ 2448 // Just let the string be empty 2449 } 2450 2451 if (*instance.abort) return Next.returnFailure; 2452 } 2453 2454 immutable rtPattern = !resolvedHost.length && 2455 (attempt.ip.addressFamily == AddressFamily.INET6) ? 2456 "Connecting to [<i>%s</>]:<i>%s</> %s..." : 2457 "Connecting to <i>%s</>:<i>%s</> %s..."; 2458 2459 immutable ssl = instance.conn.ssl ? "(SSL) " : string.init; 2460 2461 immutable address = (!resolvedHost.length || 2462 (instance.parser.server.address == resolvedHost) || 2463 (sharedDomains(instance.parser.server.address, resolvedHost) < 2)) ? 2464 attempt.ip.toAddrString : 2465 resolvedHost; 2466 2467 logger.logf(rtPattern, address, attempt.ip.toPortString, ssl); 2468 continue; 2469 2470 case connected: 2471 logger.log("Connected!"); 2472 return Next.continue_; 2473 2474 case delayThenReconnect: 2475 version(Posix) 2476 { 2477 import core.stdc.errno : EINPROGRESS; 2478 enum errnoInProgress = EINPROGRESS; 2479 } 2480 else version(Windows) 2481 { 2482 import core.sys.windows.winsock2 : WSAEINPROGRESS; 2483 enum errnoInProgress = WSAEINPROGRESS; 2484 } 2485 else 2486 { 2487 static assert(0, "Unsupported platform, please file a bug."); 2488 } 2489 2490 if (attempt.errno == errnoInProgress) 2491 { 2492 logger.warning("Connection timed out."); 2493 } 2494 else if (attempt.errno == 0) 2495 { 2496 logger.warning("Connection failed."); 2497 } 2498 else 2499 { 2500 version(Posix) 2501 { 2502 import kameloso.common : errnoStrings; 2503 enum pattern = "Connection failed with <l>%s</>: <t>%s"; 2504 logger.warningf(pattern, errnoStrings[attempt.errno], errorString); 2505 } 2506 else version(Windows) 2507 { 2508 enum pattern = "Connection failed with error <l>%d</>: <t>%s"; 2509 logger.warningf(pattern, attempt.errno, errorString); 2510 } 2511 else 2512 { 2513 static assert(0, "Unsupported platform, please file a bug."); 2514 } 2515 } 2516 2517 if (*instance.abort) return Next.returnFailure; 2518 if (!lastRetry) verboselyDelay(); 2519 numTransientSSLFailures = 0; 2520 continue; 2521 2522 case delayThenNextIP: 2523 // Check abort before delaying and then again after 2524 if (*instance.abort) return Next.returnFailure; 2525 verboselyDelayToNextIP(); 2526 if (*instance.abort) return Next.returnFailure; 2527 numTransientSSLFailures = 0; 2528 continue; 2529 2530 /*case noMoreIPs: 2531 logger.warning("Could not connect to server!"); 2532 return Next.returnFailure;*/ 2533 2534 case ipv6Failure: 2535 version(Posix) 2536 { 2537 import kameloso.common : errnoStrings; 2538 enum pattern = "IPv6 connection failed with <l>%s</>: <t>%s"; 2539 logger.warningf(pattern, errnoStrings[attempt.errno], errorString); 2540 } 2541 else version(Windows) 2542 { 2543 enum pattern = "IPv6 connection failed with error <l>%d</>: <t>%s"; 2544 logger.warningf(pattern, attempt.errno, errorString); 2545 } 2546 else 2547 { 2548 static assert(0, "Unsupported platform, please file a bug."); 2549 } 2550 2551 if (*instance.abort) return Next.returnFailure; 2552 if (!lastRetry) goto case delayThenNextIP; 2553 numTransientSSLFailures = 0; 2554 continue; 2555 2556 case transientSSLFailure: 2557 import lu.string : contains; 2558 2559 // "Failed to establish SSL connection after successful connect (system lib)" 2560 // "Failed to establish SSL connection after successful connect" --> attempted SSL on non-SSL server 2561 2562 enum pattern = "Failed to connect: <l>%s"; 2563 logger.errorf(pattern, attempt.error); 2564 if (*instance.abort) return Next.returnFailure; 2565 2566 if ((numTransientSSLFailures++ < transientSSLFailureTolerance) && 2567 attempt.error.contains("(system lib)")) 2568 { 2569 // Random failure, just reconnect immediately 2570 // but only `transientSSLFailureTolerance` times 2571 } 2572 else 2573 { 2574 if (!lastRetry) verboselyDelay(); 2575 } 2576 continue; 2577 2578 case fatalSSLFailure: 2579 enum pattern = "Failed to connect: <l>%s"; 2580 logger.errorf(pattern, attempt.error); 2581 return Next.returnFailure; 2582 2583 case invalidConnectionError: 2584 case error: 2585 version(Posix) 2586 { 2587 import kameloso.common : errnoStrings; 2588 enum pattern = "Failed to connect: <l>%s</> (<l>%s</>)"; 2589 logger.errorf(pattern, errorString, errnoStrings[attempt.errno]); 2590 } 2591 else version(Windows) 2592 { 2593 enum pattern = "Failed to connect: <l>%s</> (<l>%d</>)"; 2594 logger.errorf(pattern, errorString, attempt.errno); 2595 } 2596 else 2597 { 2598 static assert(0, "Unsupported platform, please file a bug."); 2599 } 2600 2601 if (attempt.state == invalidConnectionError) 2602 { 2603 goto case delayThenNextIP; 2604 } 2605 else 2606 { 2607 return Next.returnFailure; 2608 } 2609 } 2610 } 2611 2612 return Next.returnFailure; 2613 } 2614 2615 2616 // tryResolve 2617 /++ 2618 Tries to resolve the address in 2619 [kameloso.kameloso.Kameloso.parser.server|Kameloso.parser.server] to IPs, by 2620 leveraging [kameloso.net.resolveFiber|resolveFiber], reacting on the 2621 [kameloso.net.ResolveAttempt|ResolveAttempt]s it yields to provide feedback 2622 to the user. 2623 2624 Params: 2625 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2626 firstConnect = Whether or not this is the first time we're attempting a connection. 2627 2628 Returns: 2629 [lu.common.Next.continue_|Next.continue_] if resolution succeeded, 2630 [lu.common.Next.returnFailure|Next.returnFailure] if it failed and the 2631 program should exit. 2632 +/ 2633 auto tryResolve(ref Kameloso instance, const Flag!"firstConnect" firstConnect) 2634 { 2635 import kameloso.constants : Timeout; 2636 import kameloso.net : ResolveAttempt, resolveFiber; 2637 import std.concurrency : Generator; 2638 2639 auto resolver = new Generator!ResolveAttempt(() => 2640 resolveFiber( 2641 instance.conn, 2642 instance.parser.server.address, 2643 instance.parser.server.port, 2644 instance.connSettings.ipv6, 2645 *instance.abort)); 2646 2647 scope(exit) destroy(resolver); 2648 2649 uint incrementedRetryDelay = Timeout.connectionRetry; 2650 enum incrementMultiplier = 1.2; 2651 2652 void delayOnNetworkDown() 2653 { 2654 import kameloso.thread : interruptibleSleep; 2655 import std.algorithm.comparison : min; 2656 import core.time : seconds; 2657 2658 enum pattern = "Network down? Retrying in <i>%d</> seconds."; 2659 logger.logf(pattern, incrementedRetryDelay); 2660 interruptibleSleep(incrementedRetryDelay.seconds, *instance.abort); 2661 if (*instance.abort) return; 2662 2663 enum delayCap = 10*60; // seconds 2664 incrementedRetryDelay = cast(uint)(incrementedRetryDelay * incrementMultiplier); 2665 incrementedRetryDelay = min(incrementedRetryDelay, delayCap); 2666 } 2667 2668 foreach (const attempt; resolver) 2669 { 2670 import lu.string : beginsWith; 2671 2672 if (*instance.abort) return Next.returnFailure; 2673 2674 enum getaddrinfoErrorString = "getaddrinfo error: "; 2675 immutable errorString = attempt.error.length ? 2676 (attempt.error.beginsWith(getaddrinfoErrorString) ? 2677 attempt.error[getaddrinfoErrorString.length..$] : 2678 attempt.error) : 2679 string.init; 2680 2681 with (ResolveAttempt.State) 2682 final switch (attempt.state) 2683 { 2684 case unset: 2685 // Should never happen 2686 assert(0, "resolver yielded `unset` state"); 2687 2688 case preresolve: 2689 // No message for this 2690 continue; 2691 2692 case success: 2693 import lu.string : plurality; 2694 enum pattern = "<i>%s</> resolved into <i>%d</> %s."; 2695 logger.logf( 2696 pattern, 2697 instance.parser.server.address, 2698 instance.conn.ips.length, 2699 instance.conn.ips.length.plurality("IP", "IPs")); 2700 return Next.continue_; 2701 2702 case exception: 2703 enum pattern = "Could not resolve server address: <l>%s</> <t>(%d)"; 2704 logger.warningf(pattern, errorString, attempt.errno); 2705 delayOnNetworkDown(); 2706 if (*instance.abort) return Next.returnFailure; 2707 continue; 2708 2709 case error: 2710 enum pattern = "Could not resolve server address: <l>%s</> <t>(%d)"; 2711 logger.errorf(pattern, errorString, attempt.errno); 2712 2713 if (firstConnect) 2714 { 2715 // First attempt and a failure; something's wrong, abort 2716 enum firstConnectPattern = "Failed to resolve host. Verify that you are " ~ 2717 "connected to the Internet and that the server address (<i>%s</>) is correct."; 2718 logger.logf(firstConnectPattern, instance.parser.server.address); 2719 return Next.returnFailure; 2720 } 2721 else 2722 { 2723 // Not the first attempt yet failure; transient error? retry 2724 delayOnNetworkDown(); 2725 if (*instance.abort) return Next.returnFailure; 2726 continue; 2727 } 2728 2729 case failure: 2730 logger.error("Failed to resolve host."); 2731 return Next.returnFailure; 2732 } 2733 } 2734 2735 return Next.returnFailure; 2736 } 2737 2738 2739 // postInstanceSetup 2740 /++ 2741 Sets up the program (terminal) environment. 2742 2743 Depending on your platform it may set any of thread name, terminal title and 2744 console codepages. 2745 2746 This is called very early during execution. 2747 2748 Params: 2749 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso] instance. 2750 +/ 2751 void postInstanceSetup(ref Kameloso instance) 2752 { 2753 import kameloso.constants : KamelosoInfo; 2754 import kameloso.terminal : isTerminal, setTitle; 2755 2756 version(Windows) 2757 { 2758 import kameloso.terminal : setConsoleModeAndCodepage; 2759 2760 // Set up the console to display text and colours properly. 2761 setConsoleModeAndCodepage(); 2762 } 2763 2764 version(Posix) 2765 { 2766 import kameloso.thread : setThreadName; 2767 setThreadName("kameloso"); 2768 } 2769 2770 if (isTerminal) 2771 { 2772 // TTY or whitelisted pseudo-TTY 2773 enum terminalTitle = "kameloso v" ~ cast(string)KamelosoInfo.version_; 2774 setTitle(terminalTitle); 2775 } 2776 } 2777 2778 2779 // setDefaultDirectories 2780 /++ 2781 Sets default directories in the passed [kameloso.pods.CoreSettings|CoreSettings]. 2782 2783 This is called during early execution. 2784 2785 Params: 2786 settings = A reference to some [kameloso.pods.CoreSettings|CoreSettings]. 2787 +/ 2788 void setDefaultDirectories(ref CoreSettings settings) 2789 { 2790 import kameloso.constants : KamelosoFilenames; 2791 import kameloso.platform : cbd = configurationBaseDirectory, rbd = resourceBaseDirectory; 2792 import std.path : buildNormalizedPath; 2793 2794 settings.configFile = buildNormalizedPath(cbd, "kameloso", KamelosoFilenames.configuration); 2795 settings.resourceDirectory = buildNormalizedPath(rbd, "kameloso"); 2796 } 2797 2798 2799 // verifySettings 2800 /++ 2801 Verifies some settings and returns whether the program should continue 2802 executing (or whether there were errors such that we should exit). 2803 2804 This is called after command-line arguments have been parsed. 2805 2806 Params: 2807 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2808 2809 Returns: 2810 [lu.common.Next.returnFailure|Next.returnFailure] if the program should exit, 2811 [lu.common.Next.continue_|Next.continue_] otherwise. 2812 +/ 2813 auto verifySettings(ref Kameloso instance) 2814 { 2815 if (!instance.settings.force) 2816 { 2817 import dialect.common : isValidNickname; 2818 2819 IRCServer conservativeServer; 2820 conservativeServer.maxNickLength = 25; // Twitch max, should be enough 2821 2822 if (!instance.parser.client.nickname.isValidNickname(conservativeServer)) 2823 { 2824 // No need to print the nickname, visible from printObjects preivously 2825 logger.error("Invalid nickname!"); 2826 return Next.returnFailure; 2827 } 2828 2829 if (!instance.settings.prefix.length) 2830 { 2831 logger.error("No prefix configured!"); 2832 return Next.returnFailure; 2833 } 2834 } 2835 2836 // No point having these checks be bypassable with --force 2837 if (instance.connSettings.messageRate <= 0) 2838 { 2839 logger.error("Message rate must be a number greater than zero!"); 2840 return Next.returnFailure; 2841 } 2842 else if (instance.connSettings.messageBurst <= 0) 2843 { 2844 logger.error("Message burst must be a number greater than zero!"); 2845 return Next.returnFailure; 2846 } 2847 2848 version(Posix) 2849 { 2850 import lu.string : contains; 2851 2852 // Workaround for Issue 19247: 2853 // Segmentation fault when resolving address with std.socket.getAddress inside a Fiber 2854 // the workaround being never resolve addresses that don't contain at least one dot 2855 immutable addressIsResolvable = instance.settings.force || 2856 instance.parser.server.address == "localhost" || 2857 instance.parser.server.address.contains('.') || 2858 instance.parser.server.address.contains(':'); 2859 } 2860 else version(Windows) 2861 { 2862 // On Windows this doesn't happen, so allow all addresses. 2863 enum addressIsResolvable = true; 2864 } 2865 else 2866 { 2867 static assert(0, "Unsupported platform, please file a bug."); 2868 } 2869 2870 if (!addressIsResolvable) 2871 { 2872 enum pattern = "Invalid address! [<l>%s</>]"; 2873 logger.errorf(pattern, instance.parser.server.address); 2874 return Next.returnFailure; 2875 } 2876 2877 return Next.continue_; 2878 } 2879 2880 2881 // resolvePaths 2882 /++ 2883 Resolves resource directory private key/certificate file paths semi-verbosely. 2884 2885 This is called after settings have been verified, before plugins are initialised. 2886 2887 Params: 2888 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2889 +/ 2890 void resolvePaths(ref Kameloso instance) 2891 { 2892 import kameloso.platform : rbd = resourceBaseDirectory; 2893 import std.file : exists; 2894 import std.path : absolutePath, buildNormalizedPath, dirName, expandTilde, isAbsolute; 2895 import std.range : only; 2896 2897 immutable defaultResourceDir = buildNormalizedPath(rbd, "kameloso"); 2898 2899 version(Posix) 2900 { 2901 instance.settings.resourceDirectory = instance.settings.resourceDirectory.expandTilde(); 2902 } 2903 2904 // Resolve and create the resource directory 2905 // Assume nothing has been entered if it is the default resource dir sans server etc 2906 if (instance.settings.resourceDirectory == defaultResourceDir) 2907 { 2908 version(Windows) 2909 { 2910 import std.string : replace; 2911 instance.settings.resourceDirectory = buildNormalizedPath( 2912 defaultResourceDir, 2913 "server", 2914 instance.parser.server.address.replace(':', '_')); 2915 } 2916 else version(Posix) 2917 { 2918 instance.settings.resourceDirectory = buildNormalizedPath( 2919 defaultResourceDir, 2920 "server", 2921 instance.parser.server.address); 2922 } 2923 else 2924 { 2925 static assert(0, "Unsupported platform, please file a bug."); 2926 } 2927 } 2928 2929 if (!instance.settings.resourceDirectory.exists) 2930 { 2931 import kameloso.string : doublyBackslashed; 2932 import std.file : mkdirRecurse; 2933 2934 mkdirRecurse(instance.settings.resourceDirectory); 2935 enum pattern = "Created resource directory <i>%s"; 2936 logger.logf(pattern, instance.settings.resourceDirectory.doublyBackslashed); 2937 } 2938 2939 instance.settings.configDirectory = instance.settings.configFile.dirName; 2940 2941 auto filerange = only( 2942 &instance.connSettings.caBundleFile, 2943 &instance.connSettings.privateKeyFile, 2944 &instance.connSettings.certFile); 2945 2946 foreach (/*const*/ file; filerange) 2947 { 2948 if (!file.length) continue; 2949 2950 *file = (*file).expandTilde; 2951 2952 if (!(*file).isAbsolute && !(*file).exists) 2953 { 2954 immutable fullPath = instance.settings.configDirectory.isAbsolute ? 2955 absolutePath(*file, instance.settings.configDirectory) : 2956 buildNormalizedPath(instance.settings.configDirectory, *file); 2957 2958 if (fullPath.exists) 2959 { 2960 *file = fullPath; 2961 } 2962 // else leave as-is 2963 } 2964 } 2965 } 2966 2967 2968 // startBot 2969 /++ 2970 Main connection logic. 2971 2972 This function *starts* the bot, after it has been sufficiently initialised. 2973 It resolves and connects to servers, then hands off execution to [mainLoop]. 2974 2975 Params: 2976 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 2977 attempt = out-reference [AttemptState] aggregate of state variables used when connecting. 2978 +/ 2979 void startBot(ref Kameloso instance, out AttemptState attempt) 2980 { 2981 import kameloso.plugins.common.misc : IRCPluginInitialisationException, 2982 pluginNameOfFilename, pluginFileBaseName; 2983 import kameloso.constants : ShellReturnValue; 2984 import kameloso.terminal : TerminalToken, isTerminal; 2985 import dialect.parsing : IRCParser; 2986 import std.algorithm.comparison : among; 2987 2988 // Save a backup snapshot of the client, for restoring upon reconnections 2989 IRCClient backupClient = instance.parser.client; 2990 2991 enum bellString = "" ~ cast(char)(TerminalToken.bell); 2992 immutable bell = isTerminal ? bellString : string.init; 2993 2994 outerloop: 2995 do 2996 { 2997 // *instance.abort is guaranteed to be false here. 2998 2999 instance.generateNewConnectionID(); 3000 attempt.silentExit = true; 3001 3002 if (!attempt.firstConnect) 3003 { 3004 import kameloso.constants : Timeout; 3005 import kameloso.thread : exhaustMessages, interruptibleSleep; 3006 import core.time : seconds; 3007 3008 version(TwitchSupport) 3009 { 3010 import std.algorithm.searching : endsWith; 3011 immutable lastConnectAttemptFizzled = 3012 instance.parser.server.address.endsWith(".twitch.tv") && 3013 !instance.flags.sawWelcome; 3014 } 3015 else 3016 { 3017 enum lastConnectAttemptFizzled = false; 3018 } 3019 3020 if ((!lastConnectAttemptFizzled && instance.settings.reexecToReconnect) || instance.flags.askedToReexec) 3021 { 3022 import kameloso.platform : ExecException, execvp; 3023 import std.process : ProcessException; 3024 3025 if (!instance.settings.headless) 3026 { 3027 if (instance.settings.exitSummary && instance.connectionHistory.length) 3028 { 3029 printSummary(instance); 3030 } 3031 3032 version(GCStatsOnExit) 3033 { 3034 import kameloso.common : printGCStats; 3035 printGCStats(); 3036 } 3037 3038 immutable message = instance.flags.askedToReexec ? 3039 "Re-executing as requested." : 3040 "Re-executing to reconnect as per settings."; 3041 logger.info(message); 3042 3043 version(Windows) 3044 { 3045 // Don't writeln on Windows, leave room for "Forked into PID" message 3046 } 3047 else 3048 { 3049 import std.stdio : stdout, writeln; 3050 writeln(); 3051 stdout.flush(); 3052 } 3053 } 3054 3055 try 3056 { 3057 import core.stdc.stdlib : exit; 3058 3059 auto pid = execvp(instance.args); 3060 // On Windows, if we're here, the call succeeded 3061 // Posix should never be here; it will either exec or throw 3062 3063 enum pattern = "Forked into PID <l>%d</>."; 3064 logger.infof(pattern, pid.processID); 3065 //resetConsoleModeAndCodepage(); // Don't, it will be called via atexit 3066 exit(0); 3067 } 3068 catch (ProcessException e) 3069 { 3070 enum pattern = "Failed to spawn a new process: <t>%s</>."; 3071 logger.errorf(pattern, e.msg); 3072 } 3073 catch (ExecException e) 3074 { 3075 enum pattern = "Failed to <l>execvp</> with an error value of <l>%d</>."; 3076 logger.errorf(pattern, e.retval); 3077 } 3078 catch (Exception e) 3079 { 3080 enum pattern = "Unexpected exception: <l>%s"; 3081 logger.errorf(pattern, e.msg); 3082 version(PrintStacktraces) logger.trace(e); 3083 } 3084 } 3085 3086 // Carry some values but otherwise restore the pristine client backup 3087 backupClient.nickname = instance.parser.client.nickname; 3088 //instance.parser.client = backupClient; // Initialised below 3089 3090 // Exhaust leftover queued messages 3091 exhaustMessages(); 3092 3093 // Clear outgoing messages 3094 instance.outbuffer.clear(); 3095 instance.backgroundBuffer.clear(); 3096 instance.priorityBuffer.clear(); 3097 instance.immediateBuffer.clear(); 3098 3099 version(TwitchSupport) 3100 { 3101 instance.fastbuffer.clear(); 3102 } 3103 3104 auto gracePeriodBeforeReconnect = Timeout.connectionRetry.seconds; // mutable 3105 3106 version(TwitchSupport) 3107 { 3108 if (lastConnectAttemptFizzled || instance.flags.askedToReconnect) 3109 { 3110 import core.time : msecs; 3111 3112 /+ 3113 We either saw an instant disconnect before even getting 3114 to RPL_WELCOME, or we're reconnecting. 3115 Quickly attempt again. 3116 +/ 3117 static immutable twitchRegistrationFailConnectionRetry = 3118 Timeout.twitchRegistrationFailConnectionRetryMsecs.msecs; 3119 gracePeriodBeforeReconnect = twitchRegistrationFailConnectionRetry; 3120 } 3121 } 3122 3123 if (!lastConnectAttemptFizzled && !instance.flags.askedToReconnect) 3124 { 3125 logger.log("One moment..."); 3126 } 3127 3128 interruptibleSleep(gracePeriodBeforeReconnect, *instance.abort); 3129 if (*instance.abort) break outerloop; 3130 3131 // Re-init plugins here so it isn't done on the first connect attempt 3132 instance.initPlugins(); 3133 3134 // Reset throttling, in case there were queued messages. 3135 instance.throttle.reset(); 3136 3137 // Clear WHOIS history 3138 instance.previousWhoisTimestamps = null; 3139 3140 // Reset the server but keep the address and port 3141 immutable addressSnapshot = instance.parser.server.address; 3142 immutable portSnapshot = instance.parser.server.port; 3143 instance.parser.server = typeof(instance.parser.server).init; // TODO: Add IRCServer constructor 3144 instance.parser.server.address = addressSnapshot; 3145 instance.parser.server.port = portSnapshot; 3146 3147 // Reset transient state flags 3148 instance.flags = typeof(instance.flags).init; 3149 } 3150 3151 scope(exit) 3152 { 3153 // Always teardown when exiting this loop (for whatever reason) 3154 instance.teardownPlugins(); 3155 } 3156 3157 // May as well check once here, in case something in initPlugins aborted or so. 3158 if (*instance.abort) break outerloop; 3159 3160 instance.conn.reset(); 3161 3162 // reset() sets the receive timeout to the enum default, so make sure to 3163 // update it to any custom value after each reset() call. 3164 instance.conn.receiveTimeout = instance.connSettings.receiveTimeout; 3165 3166 immutable actionAfterResolve = tryResolve( 3167 instance, 3168 cast(Flag!"firstConnect")(attempt.firstConnect)); 3169 if (*instance.abort) break outerloop; // tryResolve interruptibleSleep can abort 3170 3171 with (Next) 3172 final switch (actionAfterResolve) 3173 { 3174 case continue_: 3175 break; 3176 3177 case returnFailure: 3178 // No need to teardown; the scopeguard does it for us. 3179 attempt.retval = ShellReturnValue.resolutionFailure; 3180 break outerloop; 3181 3182 case returnSuccess: 3183 // Ditto 3184 attempt.retval = ShellReturnValue.success; 3185 break outerloop; 3186 3187 case retry: // should never happen 3188 case crash: // ditto 3189 import lu.conv : Enum; 3190 import std.conv : text; 3191 assert(0, text("`tryResolve` returned `", Enum!Next.toString(actionAfterResolve), "`")); 3192 } 3193 3194 immutable actionAfterConnect = tryConnect(instance); 3195 if (*instance.abort) break outerloop; // tryConnect interruptibleSleep can abort 3196 3197 with (Next) 3198 final switch (actionAfterConnect) 3199 { 3200 case continue_: 3201 break; 3202 3203 case returnFailure: 3204 // No need to saveOnExit, the scopeguard takes care of that 3205 attempt.retval = ShellReturnValue.connectionFailure; 3206 break outerloop; 3207 3208 case returnSuccess: // should never happen 3209 case retry: // ditto 3210 case crash: // ditto 3211 import lu.conv : Enum; 3212 import std.conv : text; 3213 assert(0, text("`tryConnect` returned `", Enum!Next.toString(actionAfterConnect), "`")); 3214 } 3215 3216 // Ensure initialised resources after resolve so we know we have a 3217 // valid server to create a directory for. 3218 try 3219 { 3220 instance.initPluginResources(); 3221 if (*instance.abort) break outerloop; 3222 } 3223 catch (IRCPluginInitialisationException e) 3224 { 3225 if (e.malformedFilename.length) 3226 { 3227 enum pattern = "The <l>%s</> plugin failed to load its resources; " ~ 3228 "<l>%s</> is malformed. (at <l>%s</>:<l>%d</>)%s"; 3229 logger.warningf( 3230 pattern, 3231 e.pluginName, 3232 e.malformedFilename, 3233 e.file.pluginFileBaseName, 3234 e.line, 3235 bell); 3236 } 3237 else 3238 { 3239 enum pattern = "The <l>%s</> plugin failed to load its resources; " ~ 3240 "<l>%s</> (at <l>%s</>:<l>%d</>)%s"; 3241 logger.warningf( 3242 pattern, 3243 e.pluginName, 3244 e.msg, 3245 e.file.pluginFileBaseName, 3246 e.line, 3247 bell); 3248 } 3249 3250 version(PrintStacktraces) logger.trace(e.info); 3251 attempt.retval = ShellReturnValue.pluginResourceLoadFailure; 3252 break outerloop; 3253 } 3254 catch (Exception e) 3255 { 3256 enum pattern = "An unexpected error occurred while initialising " ~ 3257 "plugin resources: <l>%s</> (at <l>%s</>:<l>%d</>)%s"; 3258 logger.warningf( 3259 pattern, 3260 e.msg, 3261 e.file.pluginFileBaseName, 3262 e.line, 3263 bell); 3264 3265 version(PrintStacktraces) logger.trace(e); 3266 attempt.retval = ShellReturnValue.pluginResourceLoadException; 3267 break outerloop; 3268 } 3269 3270 // Reinit with its own server. 3271 instance.parser = IRCParser(backupClient, instance.parser.server); 3272 3273 try 3274 { 3275 instance.setupPlugins(); 3276 if (*instance.abort) break outerloop; 3277 } 3278 catch (IRCPluginInitialisationException e) 3279 { 3280 if (e.malformedFilename.length) 3281 { 3282 enum pattern = "The <l>%s</> plugin failed to setup; " ~ 3283 "<l>%s</> is malformed. (at <l>%s</>:<l>%d</>)%s"; 3284 logger.warningf( 3285 pattern, 3286 e.pluginName, 3287 e.malformedFilename, 3288 e.file.pluginFileBaseName, 3289 e.line, 3290 bell); 3291 } 3292 else 3293 { 3294 enum pattern = "The <l>%s</> plugin failed to setup; " ~ 3295 "<l>%s</> (at <l>%s</>:<l>%d</>)%s"; 3296 logger.warningf( 3297 pattern, 3298 e.pluginName, 3299 e.msg, 3300 e.file.pluginFileBaseName, 3301 e.line, 3302 bell); 3303 } 3304 3305 version(PrintStacktraces) logger.trace(e.info); 3306 attempt.retval = ShellReturnValue.pluginSetupFailure; 3307 break outerloop; 3308 } 3309 catch (Exception e) 3310 { 3311 enum pattern = "An unexpected error occurred while setting up the <l>%s</> plugin: " ~ 3312 "<l>%s</> (at <l>%s</>:<l>%d</>)%s"; 3313 logger.warningf( 3314 pattern, 3315 e.file.pluginNameOfFilename, 3316 e.msg, 3317 e.file, 3318 e.line, 3319 bell); 3320 3321 version(PrintStacktraces) logger.trace(e); 3322 attempt.retval = ShellReturnValue.pluginSetupException; 3323 break outerloop; 3324 } 3325 3326 // Do verbose exits if mainLoop causes a return 3327 attempt.silentExit = false; 3328 3329 /+ 3330 If version Callgrind, do a callgrind dump before the main loop starts, 3331 and then once again on disconnect. That way the dump won't contain 3332 uninteresting profiling about resolving and connecting and such. 3333 +/ 3334 version(Callgrind) 3335 { 3336 void dumpCallgrind() 3337 { 3338 import lu.string : beginsWith; 3339 import std.conv : to; 3340 import std.process : execute, thisProcessID; 3341 import std.stdio : writeln; 3342 import std.string : chomp; 3343 3344 immutable dumpCommand = 3345 [ 3346 "callgrind_control", 3347 "-d", 3348 thisProcessID.to!string, 3349 ]; 3350 3351 logger.info("$ callgrind_control -d ", thisProcessID); 3352 immutable result = execute(dumpCommand); 3353 writeln(result.output.chomp); 3354 instance.callgrindRunning = !result.output.beginsWith("Error: Callgrind task with PID"); 3355 } 3356 3357 if (instance.callgrindRunning) 3358 { 3359 // Dump now and on scope exit 3360 dumpCallgrind(); 3361 } 3362 3363 scope(exit) if (instance.callgrindRunning) dumpCallgrind(); 3364 } 3365 3366 // Start the main loop 3367 instance.flags.askedToReconnect = false; 3368 attempt.next = instance.mainLoop(); 3369 attempt.firstConnect = false; 3370 } 3371 while ( 3372 !*instance.abort && 3373 attempt.next.among!(Next.continue_, Next.retry)); 3374 } 3375 3376 3377 // printEventDebugDetails 3378 /++ 3379 Print what we know about an event, from an error perspective. 3380 3381 Params: 3382 event = The [dialect.defs.IRCEvent|IRCEvent] in question. 3383 raw = The raw string that `event` was parsed from, as read from the IRC server. 3384 eventWasInitialised = Whether the [dialect.defs.IRCEvent|IRCEvent] was 3385 initialised or if it was only ever set to `void`. 3386 +/ 3387 void printEventDebugDetails( 3388 const ref IRCEvent event, 3389 const string raw, 3390 const Flag!"eventWasInitialised" eventWasInitialised = Yes.eventWasInitialised) 3391 { 3392 if (globalHeadless || !raw.length) return; 3393 3394 version(IncludeHeavyStuff) 3395 { 3396 enum onlyPrintRaw = false; 3397 } 3398 else 3399 { 3400 enum onlyPrintRaw = true; 3401 } 3402 3403 if (onlyPrintRaw || !eventWasInitialised || !event.raw.length) // == IRCEvent.init 3404 { 3405 enum pattern = `Offending line: "<l>%s</>"`; 3406 logger.warningf(pattern, raw); 3407 } 3408 else 3409 { 3410 version(IncludeHeavyStuff) 3411 { 3412 import kameloso.printing : printObject; 3413 import std.typecons : Flag, No, Yes; 3414 3415 // Offending line included in event, in raw 3416 printObject!(Yes.all)(event); 3417 3418 if (event.sender != IRCUser.init) 3419 { 3420 logger.trace("sender:"); 3421 printObject(event.sender); 3422 } 3423 3424 if (event.target != IRCUser.init) 3425 { 3426 logger.trace("target:"); 3427 printObject(event.target); 3428 } 3429 } 3430 } 3431 } 3432 3433 3434 // printSummary 3435 /++ 3436 Prints a summary of the connection(s) made and events parsed this execution. 3437 3438 Params: 3439 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3440 +/ 3441 void printSummary(const ref Kameloso instance) 3442 { 3443 import kameloso.time : timeSince; 3444 import core.time : Duration; 3445 3446 Duration totalTime; 3447 ulong totalBytesReceived; 3448 uint i; 3449 3450 logger.info("== Connection summary =="); 3451 3452 foreach (const entry; instance.connectionHistory) 3453 { 3454 import std.datetime.systime : SysTime; 3455 import std.format : format; 3456 import std.stdio : writefln; 3457 import core.time : hnsecs; 3458 3459 if (!entry.bytesReceived) continue; 3460 3461 enum onlyTimePattern = "%02d:%02d:%02d"; 3462 enum fullDatePattern = "%d-%02d-%02d " ~ onlyTimePattern; 3463 3464 auto start = SysTime.fromUnixTime(entry.startTime); 3465 immutable startString = fullDatePattern.format( 3466 start.year, 3467 start.month, 3468 start.day, 3469 start.hour, 3470 start.minute, 3471 start.second); 3472 3473 auto stop = SysTime.fromUnixTime(entry.stopTime); 3474 immutable stopString = (start.dayOfGregorianCal == stop.dayOfGregorianCal) ? 3475 onlyTimePattern.format( 3476 stop.hour, 3477 stop.minute, 3478 stop.second) : 3479 fullDatePattern.format( 3480 stop.year, 3481 stop.month, 3482 stop.day, 3483 stop.hour, 3484 stop.minute, 3485 stop.second); 3486 3487 start.fracSecs = 0.hnsecs; 3488 stop.fracSecs = 0.hnsecs; 3489 immutable duration = (stop - start); 3490 totalTime += duration; 3491 totalBytesReceived += entry.bytesReceived; 3492 3493 enum pattern = "%2d: %s, %d events parsed in %,d bytes (%s to %s)"; 3494 writefln( 3495 pattern, 3496 ++i, 3497 duration.timeSince!(7, 0)(Yes.abbreviate), 3498 entry.numEvents, 3499 entry.bytesReceived, 3500 startString, 3501 stopString); 3502 } 3503 3504 enum timeConnectedPattern = "Total time connected: <l>%s"; 3505 logger.infof(timeConnectedPattern, totalTime.timeSince!(7, 1)); 3506 enum receivedPattern = "Total received: <l>%,d</> bytes"; 3507 logger.infof(receivedPattern, totalBytesReceived); 3508 } 3509 3510 3511 // AttemptState 3512 /++ 3513 Aggregate of state values used in an execution of the program. 3514 +/ 3515 struct AttemptState 3516 { 3517 /// Enum denoting what we should do next loop in an execution attempt. 3518 Next next; 3519 3520 /++ 3521 Bool whether this is the first connection attempt or if we have 3522 connected at least once already. 3523 +/ 3524 bool firstConnect = true; 3525 3526 /// Whether or not "Exiting..." should be printed at program exit. 3527 bool silentExit; 3528 3529 /// Shell return value to exit with. 3530 int retval; 3531 } 3532 3533 3534 // syncGuestChannels 3535 /++ 3536 Syncs currently joined channels with [IRCBot.guestChannels|guestChannels], 3537 adding entries in the latter where the former is missing. 3538 3539 We can't just check the first plugin at `instance.plugins[0]` since there's 3540 no way to be certain it mixes in [kameloso.plugins.common.awareness.ChannelAwareness|ChannelAwareness]. 3541 3542 Used when saving to configuration file, to ensure the current state is saved. 3543 3544 Params: 3545 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3546 +/ 3547 void syncGuestChannels(ref Kameloso instance) 3548 { 3549 foreach (plugin; instance.plugins) 3550 { 3551 // Skip plugins that don't seem to mix in ChannelAwareness 3552 if (!plugin.state.channels.length) continue; 3553 3554 foreach (immutable channelName; plugin.state.channels.byKey) 3555 { 3556 import std.algorithm.searching : canFind; 3557 3558 if (!instance.bot.homeChannels.canFind(channelName) && 3559 !instance.bot.guestChannels.canFind(channelName)) 3560 { 3561 // We're in a channel that isn't tracked as home or guest 3562 // We're also saving, so save it as guest 3563 instance.bot.guestChannels ~= channelName; 3564 } 3565 } 3566 3567 // We only need the channels from one plugin, as we can be reasonably sure 3568 // every plugin that have channels have the same channels 3569 break; 3570 } 3571 } 3572 3573 3574 // getQuitMessageInFlight 3575 /++ 3576 Get any QUIT concurrency messages currently in the mailbox. Also catch Variants 3577 so as not to throw an exception on missed priority messages. 3578 3579 Params: 3580 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3581 3582 Returns: 3583 A [kameloso.thread.ThreadMessage|ThreadMessage] with its `content` message 3584 containing any quit reasons encountered. 3585 +/ 3586 auto getQuitMessageInFlight(ref Kameloso instance) 3587 { 3588 import kameloso.string : replaceTokens; 3589 import kameloso.thread : ThreadMessage; 3590 import std.concurrency : receiveTimeout; 3591 import std.variant : Variant; 3592 import core.time : Duration; 3593 3594 ThreadMessage returnMessage; 3595 bool receivedSomething; 3596 bool halt; 3597 3598 do 3599 { 3600 receivedSomething = receiveTimeout(Duration.zero, 3601 (ThreadMessage message) scope 3602 { 3603 if (message.type == ThreadMessage.Type.quit) 3604 { 3605 returnMessage = message; 3606 halt = true; 3607 } 3608 }, 3609 (Variant _) scope {}, 3610 ); 3611 } 3612 while (!halt && receivedSomething); 3613 3614 return returnMessage; 3615 } 3616 3617 3618 // echoQuitMessage 3619 /++ 3620 Echos the quit message to the local terminal, to fake it being sent verbosely 3621 to the server. It is sent, but later, bypassing the message Fiber which would 3622 otherwise do the echoing. 3623 3624 Params: 3625 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3626 reason = Quit reason. 3627 +/ 3628 void echoQuitMessage(ref Kameloso instance, const string reason) 3629 { 3630 bool printed; 3631 3632 version(Colours) 3633 { 3634 if (!instance.settings.monochrome) 3635 { 3636 import kameloso.irccolours : mapEffects; 3637 logger.trace("--> QUIT :", reason.mapEffects); 3638 printed = true; 3639 } 3640 } 3641 3642 if (!printed) 3643 { 3644 import kameloso.irccolours : stripEffects; 3645 logger.trace("--> QUIT :", reason.stripEffects); 3646 } 3647 } 3648 3649 3650 // propagateWhoisTimestamp 3651 /++ 3652 Propagates a single update to the the [kameloso.kameloso.Kameloso.previousWhoisTimestamps] 3653 associative array to all plugins. 3654 3655 Params: 3656 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3657 nickname = Nickname whose WHOIS timestamp to propagate. 3658 now = UNIX WHOIS timestamp. 3659 +/ 3660 void propagateWhoisTimestamp( 3661 ref Kameloso instance, 3662 const string nickname, 3663 const long now) pure 3664 { 3665 foreach (plugin; instance.plugins) 3666 { 3667 plugin.state.previousWhoisTimestamps[nickname] = now; 3668 } 3669 } 3670 3671 3672 // propagateWhoisTimestamps 3673 /++ 3674 Propagates the [kameloso.kameloso.Kameloso.previousWhoisTimestamps] 3675 associative array to all plugins. 3676 3677 Makes a copy of it before passing it onwards; this way, plugins cannot 3678 modify the original. 3679 3680 Params: 3681 instance = Reference to the current [kameloso.kameloso.Kameloso|Kameloso]. 3682 +/ 3683 void propagateWhoisTimestamps(ref Kameloso instance) pure 3684 { 3685 auto copy = instance.previousWhoisTimestamps.dup; // mutable 3686 3687 foreach (plugin; instance.plugins) 3688 { 3689 plugin.state.previousWhoisTimestamps = copy; 3690 } 3691 } 3692 3693 3694 public: 3695 3696 3697 // run 3698 /++ 3699 Entry point of the program. 3700 3701 This function is very long, but mostly because it's tricky to split up into 3702 free functions and have them convey "parent function should exit". 3703 3704 Params: 3705 args = Command-line arguments passed to the program. 3706 3707 Returns: 3708 `0` on success, non-`0` on failure. 3709 +/ 3710 auto run(string[] args) 3711 { 3712 import kameloso.plugins.common.misc : IRCPluginSettingsException; 3713 import kameloso.constants : ShellReturnValue; 3714 import kameloso.logger : KamelosoLogger; 3715 import kameloso.string : replaceTokens; 3716 import std.algorithm.comparison : among; 3717 import std.conv : ConvException; 3718 import std.exception : ErrnoException; 3719 static import kameloso.common; 3720 3721 // Set up the Kameloso instance. 3722 auto instance = Kameloso(args); 3723 postInstanceSetup(instance); 3724 3725 // Set pointers. 3726 kameloso.common.settings = &instance.settings; 3727 instance.abort = &globalAbort; 3728 3729 // Declare AttemptState instance. 3730 AttemptState attempt; 3731 3732 // Set up default directories in the settings. 3733 setDefaultDirectories(instance.settings); 3734 3735 // Initialise the logger immediately so it's always available. 3736 // handleGetopt re-inits later when we know the settings for monochrome and headless 3737 kameloso.common.logger = new KamelosoLogger(instance.settings); 3738 3739 // Set up signal handling so that we can gracefully catch Ctrl+C. 3740 setupSignals(); 3741 3742 scope(failure) 3743 { 3744 import kameloso.terminal : TerminalToken, isTerminal; 3745 3746 if (!instance.settings.headless) 3747 { 3748 enum bellString = "" ~ cast(char)(TerminalToken.bell); 3749 immutable bell = isTerminal ? bellString : string.init; 3750 logger.error("We just crashed!", bell); 3751 } 3752 3753 *instance.abort = true; 3754 resetSignals(); 3755 } 3756 3757 immutable actionAfterGetopt = tryGetopt(instance); 3758 globalHeadless = instance.settings.headless; 3759 3760 with (Next) 3761 final switch (actionAfterGetopt) 3762 { 3763 case continue_: 3764 break; 3765 3766 case returnSuccess: 3767 return ShellReturnValue.success; 3768 3769 case returnFailure: 3770 return ShellReturnValue.getoptFailure; 3771 3772 case retry: // should never happen 3773 case crash: // ditto 3774 import lu.conv : Enum; 3775 import std.conv : text; 3776 assert(0, text("`tryGetopt` returned `", Enum!Next.toString(actionAfterGetopt), "`")); 3777 } 3778 3779 if (!instance.settings.headless || instance.settings.force) 3780 { 3781 try 3782 { 3783 import kameloso.terminal : ensureAppropriateBuffering; 3784 3785 // Ensure stdout is buffered by line if we think it isn't being 3786 ensureAppropriateBuffering(); 3787 } 3788 catch (ErrnoException e) 3789 { 3790 import std.stdio : writeln; 3791 if (!instance.settings.headless) writeln("Failed to set stdout buffer mode/size! errno:", e.errno); 3792 if (!instance.settings.force) return ShellReturnValue.terminalSetupFailure; 3793 } 3794 catch (Exception e) 3795 { 3796 if (!instance.settings.headless) 3797 { 3798 import std.stdio : writeln; 3799 writeln("Failed to set stdout buffer mode/size!"); 3800 writeln(e); 3801 } 3802 3803 if (!instance.settings.force) return ShellReturnValue.terminalSetupFailure; 3804 } 3805 finally 3806 { 3807 if (instance.settings.flush) stdout.flush(); 3808 } 3809 } 3810 3811 // Apply some defaults to empty members, as stored in `kameloso.constants`. 3812 // It's done before in tryGetopt but do it again to ensure we don't have an empty nick etc 3813 // Skip if --force was passed. 3814 if (!instance.settings.force) 3815 { 3816 import kameloso.config : applyDefaults; 3817 applyDefaults(instance.parser.client, instance.parser.server, instance.bot); 3818 } 3819 3820 // Additionally if the port is an SSL-like port, assume SSL, 3821 // but only if the user isn't forcing settings 3822 if (!instance.connSettings.ssl && 3823 !instance.settings.force && 3824 instance.parser.server.port.among!(6697, 7000, 7001, 7029, 7070, 9999, 443)) 3825 { 3826 instance.connSettings.ssl = true; 3827 } 3828 3829 // Copy ssl setting to the Connection after the above 3830 instance.conn.ssl = instance.connSettings.ssl; 3831 3832 if (!instance.settings.headless) 3833 { 3834 import kameloso.common : printVersionInfo; 3835 import kameloso.printing : printObjects; 3836 import std.stdio : writeln; 3837 3838 printVersionInfo(); 3839 writeln(); 3840 if (instance.settings.flush) stdout.flush(); 3841 3842 // Print the current settings to show what's going on. 3843 IRCClient prettyClient = instance.parser.client; 3844 prettyClient.realName = replaceTokens(prettyClient.realName); 3845 printObjects(prettyClient, instance.bot, instance.parser.server); 3846 3847 if (!instance.bot.homeChannels.length && !instance.bot.admins.length) 3848 { 3849 import kameloso.config : giveBrightTerminalHint, notifyAboutIncompleteConfiguration; 3850 3851 giveBrightTerminalHint(); 3852 logger.trace(); 3853 notifyAboutIncompleteConfiguration(instance.settings.configFile, args[0]); 3854 } 3855 } 3856 3857 // Verify that settings are as they should be (nickname exists and not too long, etc) 3858 immutable actionAfterVerification = verifySettings(instance); 3859 3860 with (Next) 3861 final switch (actionAfterVerification) 3862 { 3863 case continue_: 3864 break; 3865 3866 case returnFailure: 3867 return ShellReturnValue.settingsVerificationFailure; 3868 3869 case retry: // should never happen 3870 case returnSuccess: // ditto 3871 case crash: // ditto 3872 import lu.conv : Enum; 3873 import std.conv : text; 3874 assert(0, text("`verifySettings` returned `", Enum!Next.toString(actionAfterVerification), "`")); 3875 } 3876 3877 // Resolve resource and private key/certificate paths. 3878 resolvePaths(instance); 3879 instance.conn.certFile = instance.connSettings.certFile; 3880 instance.conn.privateKeyFile = instance.connSettings.privateKeyFile; 3881 3882 // Save the original nickname *once*, outside the connection loop and before 3883 // initialising plugins (who will make a copy of it). Knowing this is useful 3884 // when authenticating. 3885 instance.parser.client.origNickname = instance.parser.client.nickname; 3886 3887 // Initialise plugins outside the loop once, for the error messages 3888 try 3889 { 3890 import std.file : exists; 3891 3892 instance.initPlugins(); 3893 3894 if (!instance.settings.headless && 3895 instance.missingConfigurationEntries.length && 3896 instance.settings.configFile.exists) 3897 { 3898 import kameloso.config : notifyAboutMissingSettings; 3899 3900 notifyAboutMissingSettings( 3901 instance.missingConfigurationEntries, 3902 args[0], 3903 instance.settings.configFile); 3904 } 3905 } 3906 catch (ConvException e) 3907 { 3908 // Configuration file/--set argument syntax error 3909 logger.error(e.msg); 3910 version(PrintStacktraces) logger.trace(e.info); 3911 if (!instance.settings.force) return ShellReturnValue.customConfigSyntaxFailure; 3912 } 3913 catch (IRCPluginSettingsException e) 3914 { 3915 // --set plugin/setting name error 3916 logger.error(e.msg); 3917 version(PrintStacktraces) logger.trace(e.info); 3918 if (!instance.settings.force) return ShellReturnValue.customConfigFailure; 3919 } 3920 3921 // Save the original nickname *once*, outside the connection loop. 3922 // It will change later and knowing this is useful when authenticating 3923 instance.parser.client.origNickname = instance.parser.client.nickname; 3924 3925 // Go! 3926 startBot(instance, attempt); 3927 3928 // If we're here, we should exit. The only question is in what way. 3929 3930 if (instance.conn.connected && !instance.flags.quitMessageSent) 3931 { 3932 // If not already sent, send a proper QUIT, optionally verbosely 3933 string reason; // mutable 3934 3935 if (!*instance.abort && !instance.settings.headless && !instance.settings.hideOutgoing) 3936 { 3937 const message = getQuitMessageInFlight(instance); 3938 reason = message.content.length ? 3939 message.content : 3940 instance.bot.quitReason; 3941 reason = reason.replaceTokens(instance.parser.client); 3942 echoQuitMessage(instance, reason); 3943 } 3944 3945 if (!reason.length) 3946 { 3947 reason = instance.bot.quitReason.replaceTokens(instance.parser.client); 3948 } 3949 3950 instance.conn.sendline("QUIT :" ~ reason); 3951 } 3952 3953 // Save if we're exiting and configuration says we should. 3954 if (instance.settings.saveOnExit) 3955 { 3956 try 3957 { 3958 import kameloso.config : writeConfigurationFile; 3959 syncGuestChannels(instance); 3960 writeConfigurationFile(instance, instance.settings.configFile); 3961 } 3962 catch (Exception e) 3963 { 3964 import kameloso.string : doublyBackslashed; 3965 enum pattern = "Caught Exception when saving settings: " ~ 3966 "<l>%s</> (at <l>%s</>:<l>%d</>)"; 3967 logger.warningf(pattern, e.msg, e.file.doublyBackslashed, e.line); 3968 version(PrintStacktraces) logger.trace(e); 3969 } 3970 } 3971 3972 // Print connection summary 3973 if (!instance.settings.headless) 3974 { 3975 if (instance.settings.exitSummary && instance.connectionHistory.length) 3976 { 3977 printSummary(instance); 3978 } 3979 3980 version(GCStatsOnExit) 3981 { 3982 import kameloso.common : printGCStats; 3983 printGCStats(); 3984 } 3985 3986 if (*instance.abort) 3987 { 3988 logger.error("Aborting..."); 3989 } 3990 else if (!attempt.silentExit) 3991 { 3992 logger.info("Exiting..."); 3993 } 3994 } 3995 3996 if (*instance.abort) 3997 { 3998 // Ctrl+C 3999 version(Posix) 4000 { 4001 if (signalRaised > 0) attempt.retval = (128 + signalRaised); 4002 } 4003 4004 if (attempt.retval == 0) 4005 { 4006 // Pass through any specific values, set to failure if unset 4007 attempt.retval = ShellReturnValue.failure; 4008 } 4009 } 4010 4011 return attempt.retval; 4012 }