public static void SetStopHandler([NotNull] Action handler)
        {
            onCtrlBreak = sig =>
            {
                handler();
                return(true);
            };
            var stopSignal = CreateStopSignal();

            RegisterStopSignalCallback(stopSignal, handler);
            WinApi.SetConsoleCtrlHandler(onCtrlBreak, true);
        }
        private async Task <bool> Init(string configPath, string agentsPath, string commandsPath)
        {
            this.configPath   = configPath;
            this.agentsPath   = agentsPath;
            this.commandsPath = commandsPath;

            // create discord client
            Discord                  = new DiscordSocketClient();
            Discord.JoinedGuild     += Discord_JoinedGuild;
            Discord.LeftGuild       += Discord_LeftGuild;
            Discord.MessageReceived += Discord_MessageReceived;
            Discord.Log             += Log;
            Discord.Ready           += Discord_Ready;

            commandService      = new CommandService();
            commandService.Log += Log;
            try {
                CommandsAssembly = Assembly.LoadFile(Path.GetFullPath(commandsPath));
                await commandService.AddModulesAsync(CommandsAssembly);
            } catch (Exception ex) {
                Log(LogSeverity.Error, "Failed to load commands assembly at \"" + commandsPath + "\"", ex);
                return(false);
            }

            // install commands
            ModifyServices(x => x
                           .AddSingleton(Discord)
                           .AddSingleton(commandService)
                           );

            // load config
            if (!ReloadConfig())
            {
                return(false);
            }

            // export embedded bot agent as reference
            if (!Directory.Exists(agentsPath))
            {
                Directory.CreateDirectory(agentsPath);
            }
            File.WriteAllBytes(Path.Combine(agentsPath, "tomoko.json"), Resources.tomoko);

            // load agent
            if (!ReloadAgent())
            {
                return(false);
            }

            // capture console exit
            consoleCtrlHandler = new WinApi.HandlerRoutine(consoleCtrl);
            WinApi.SetConsoleCtrlHandler(consoleCtrlHandler, true);

            // init record device
            if (Config.speakEnabled)
            {
                recordDevice = -1;
                if (Config.speakRecordingDevice != null)
                {
                    var devices = Bass.BASS_RecordGetDeviceInfos();
                    for (int i = 0; i < devices.Length; i++)
                    {
                        if (devices[i].name == Config.speakRecordingDevice)
                        {
                            recordDevice = i;
                            break;
                        }
                    }
                    if (recordDevice < 0)
                    {
                        IEnumerable <string> devicesList = devices.Select(d => d.name);
                        Log(LogSeverity.Error, "Recording device \"" + Config.speakRecordingDevice + "\" not found.\nAvailable recording devices:\n * " + string.Join("\n * ", devicesList) + "\n");
                        return(false);
                    }
                }

                if (!Bass.BASS_RecordInit(recordDevice))
                {
                    Log(LogSeverity.Error, "Failed to init recording device: " + Bass.BASS_ErrorGetCode());
                    return(false);
                }
                recordProc = new RECORDPROC(recordDevice_audioReceived);
            }

            // init playback device
            if (Config.listenEnabled)
            {
                playbackDevice = -1;
                if (Config.listenPlaybackDevice != null)
                {
                    var devices = Bass.BASS_GetDeviceInfos();
                    for (int i = 0; i < devices.Length; i++)
                    {
                        if (devices[i].name == Config.listenPlaybackDevice)
                        {
                            playbackDevice = i;
                            break;
                        }
                    }
                    if (playbackDevice < 0)
                    {
                        IEnumerable <string> devicesList = devices.Select(d => d.name);
                        Log(LogSeverity.Error, "Playback device \"" + Config.listenPlaybackDevice + "\" not found.\nAvailable playback devices:\n * " + string.Join("\n * ", devicesList) + "\n");
                        return(false);
                    }
                }

                if (!Bass.BASS_Init(playbackDevice, SAMPLE_RATE, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero, null))
                {
                    Log(LogSeverity.Error, "Failed to init playback device: " + Bass.BASS_ErrorGetCode());
                    return(false);
                }
                playProc = new STREAMPROC(playbackDevice_audioRequested);
            }

            return(true);
        }