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 }