def _check_if_alive(self) -> None: if not self._is_alive: raise errors.ComponentStateConflictError( "Component cannot be used while it's not alive") if self._is_closing: raise errors.ComponentStateConflictError( "Component cannot be used while it's closing")
async def join(self) -> None: """Wait for the process to halt before continuing.""" if not self._close_event: raise errors.ComponentStateConflictError( "Cannot wait for an inactive interaction server to join") await self._close_event.wait()
def start(self) -> None: """Start this voice component.""" if self._is_alive: raise errors.ComponentStateConflictError( "Cannot start a voice component which is already running") self._is_alive = True self._app.event_manager.subscribe(voice_events.VoiceEvent, self._on_voice_event)
async def close(self) -> None: if not self._close_event: raise errors.ComponentStateConflictError("Cannot close an inactive interaction server") if self._is_closing: await self.join() return self._is_closing = True close_event = self._close_event await self._server.close() await self._rest.close() close_event.set() self._close_event = None
async def close(self) -> None: """Gracefully close the server and any open connections.""" if not self._server or not self._close_event: raise errors.ComponentStateConflictError("Cannot close an inactive interaction server") if self._is_closing: await self.join() return self._is_closing = True self._application_fetch_lock = None # This shutdown then cleanup ordering matters. await self._server.shutdown() await self._server.cleanup() self._close_event.set() self._close_event = None self._server = None
async def start(self) -> None: if self._run_task is not None: raise errors.ComponentStateConflictError( "Cannot run more than one instance of one shard concurrently") run_task = asyncio.create_task(self._run(), name=f"run shard {self._shard_id}") self._run_task = run_task waiter = asyncio.create_task( self._handshake_completed.wait(), name=f"wait for shard {self._shard_id} to start") done, _ = await asyncio.wait((waiter, run_task), return_when=asyncio.FIRST_COMPLETED) waiter.cancel() if done and waiter not in done: # This might throw an error, or it might not, depending on what we do with it. # This occurs if the run task finished before the handshake completion event, # which implies the shard died before it could become ready/resume... self._run_task = None run_task.result() raise asyncio.CancelledError( f"shard {self._shard_id} was closed before it could start successfully" )
async def start( self, backlog: int = 128, enable_signal_handlers: bool = True, host: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, port: typing.Optional[int] = None, path: typing.Optional[str] = None, reuse_address: typing.Optional[bool] = None, reuse_port: typing.Optional[bool] = None, socket: typing.Optional[socket_.socket] = None, shutdown_timeout: float = 60.0, ssl_context: typing.Optional[ssl.SSLContext] = None, ) -> None: """Start the bot and wait for the internal server to startup then return. Other Parameters ---------------- backlog : builtins.int The number of unaccepted connections that the system will allow before refusing new connections. enable_signal_handlers : builtins.bool Defaults to `builtins.True`. If on a __non-Windows__ OS with builtin support for kernel-level POSIX signals, then setting this to `builtins.True` will allow treating keyboard interrupts and other OS signals to safely shut down the application as calls to shut down the application properly rather than just killing the process in a dirty state immediately. You should leave this disabled unless you plan to implement your own signal handling yourself. host : typing.Optional[typing.Union[builtins.str, aiohttp.web.HostSequence]] TCP/IP host or a sequence of hosts for the HTTP server. port : typing.Optional[builtins.int] TCP/IP port for the HTTP server. path : typing.Optional[builtins.str] File system path for HTTP server unix domain socket. reuse_address : typing.Optional[builtins.bool] Tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire. reuse_port : typing.Optional[builtins.bool] Tells the kernel to allow this endpoint to be bound to the same port as other existing endpoints are also bound to. socket : typing.Optional[socket.socket] A pre-existing socket object to accept connections on. shutdown_timeout : builtins.float A delay to wait for graceful server shutdown before forcefully disconnecting all open client sockets. This defaults to 60 seconds. ssl_context : typing.Optional[ssl.SSLContext] SSL context for HTTPS servers. !!! note For more information on the other parameters such as defaults see AIOHTTP's documentation. """ if self._server: raise errors.ComponentStateConflictError("Cannot start an already active interaction server") self._close_event = asyncio.Event() self._is_closing = False aio_app = aiohttp.web.Application() aio_app.add_routes([aiohttp.web.post("/", self.aiohttp_hook)]) self._server = aiohttp.web_runner.AppRunner(aio_app, handle_signals=enable_signal_handlers, access_log=_LOGGER) await self._server.setup() sites: typing.List[aiohttp.web.BaseSite] = [] if host is not None: if isinstance(host, str): host = [host] for h in host: sites.append( aiohttp.web.TCPSite( self._server, h, port, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, backlog=backlog, reuse_address=reuse_address, reuse_port=reuse_port, ) ) elif path is None and socket is None or port is None: sites.append( aiohttp.web.TCPSite( self._server, port=port, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, backlog=backlog, reuse_address=reuse_address, reuse_port=reuse_port, ) ) if path is not None: sites.append( aiohttp.web.UnixSite( self._server, path, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, backlog=backlog ) ) if socket is not None: sites.append( aiohttp.web.SockSite( self._server, socket, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, backlog=backlog ) ) for site in sites: _LOGGER.info("Starting site on %s", site.name) await site.start()
async def start( self, backlog: int = 128, check_for_updates: bool = True, enable_signal_handlers: typing.Optional[bool] = None, host: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, port: typing.Optional[int] = None, path: typing.Optional[str] = None, reuse_address: typing.Optional[bool] = None, reuse_port: typing.Optional[bool] = None, socket: typing.Optional[socket_.socket] = None, shutdown_timeout: float = 60.0, ssl_context: typing.Optional[ssl.SSLContext] = None, ) -> None: """Start the bot and wait for the internal server to startup then return. Other Parameters ---------------- backlog : builtins.int The number of unaccepted connections that the system will allow before refusing new connections. check_for_updates : builtins.bool Defaults to `builtins.True`. If `builtins.True`, will check for newer versions of `hikari` on PyPI and notify if available. enable_signal_handlers : typing.Optional[builtins.bool] Defaults to `builtins.True` if this is started in the main thread. If on a __non-Windows__ OS with builtin support for kernel-level POSIX signals, then setting this to `builtins.True` will allow treating keyboard interrupts and other OS signals to safely shut down the application as calls to shut down the application properly rather than just killing the process in a dirty state immediately. You should leave this enabled unless you plan to implement your own signal handling yourself. host : typing.Optional[typing.Union[builtins.str, aiohttp.web.HostSequence]] TCP/IP host or a sequence of hosts for the HTTP server. port : typing.Optional[builtins.int] TCP/IP port for the HTTP server. path : typing.Optional[builtins.str] File system path for HTTP server unix domain socket. reuse_address : typing.Optional[builtins.bool] Tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire. reuse_port : typing.Optional[builtins.bool] Tells the kernel to allow this endpoint to be bound to the same port as other existing endpoints are also bound to. socket : typing.Optional[socket.socket] A pre-existing socket object to accept connections on. shutdown_timeout : builtins.float A delay to wait for graceful server shutdown before forcefully disconnecting all open client sockets. This defaults to 60 seconds. ssl_context : typing.Optional[ssl.SSLContext] SSL context for HTTPS servers. !!! note For more information on the other parameters such as defaults see AIOHTTP's documentation. """ if self.is_alive: raise errors.ComponentStateConflictError("Cannot start an already active interaction server") self._is_closing = False self._close_event = asyncio.Event() if check_for_updates: asyncio.create_task( ux.check_for_updates(self._http_settings, self._proxy_settings), name="check for package updates", ) self._rest.start() await self._server.start( backlog=backlog, enable_signal_handlers=enable_signal_handlers, host=host, port=port, path=path, reuse_address=reuse_address, reuse_port=reuse_port, socket=socket, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, )
def run( self, asyncio_debug: bool = False, backlog: int = 128, check_for_updates: bool = True, close_loop: bool = True, close_passed_executor: bool = False, coroutine_tracking_depth: typing.Optional[int] = None, enable_signal_handlers: typing.Optional[bool] = None, host: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, path: typing.Optional[str] = None, port: typing.Optional[int] = None, reuse_address: typing.Optional[bool] = None, reuse_port: typing.Optional[bool] = None, shutdown_timeout: float = 60.0, socket: typing.Optional[socket_.socket] = None, ssl_context: typing.Optional[ssl.SSLContext] = None, ) -> None: """Open this REST server and block until it closes. Other Parameters ---------------- asyncio_debug : builtins.bool Defaults to `builtins.False`. If `builtins.True`, then debugging is enabled for the asyncio event loop in use. backlog : builtins.int The number of unaccepted connections that the system will allow before refusing new connections. check_for_updates : builtins.bool Defaults to `builtins.True`. If `builtins.True`, will check for newer versions of `hikari` on PyPI and notify if available. close_loop : builtins.bool Defaults to `builtins.True`. If `builtins.True`, then once the bot enters a state where all components have shut down permanently during application shutdown, then all asyncgens and background tasks will be destroyed, and the event loop will be shut down. This will wait until all `hikari`-owned `aiohttp` connectors have had time to attempt to shut down correctly (around 250ms), and on Python 3.9 and newer, will also shut down the default event loop executor too. close_passed_executor : builtins.bool Defaults to `builtins.False`. If `builtins.True`, any custom `concurrent.futures.Executor` passed to the constructor will be shut down when the application terminates. This does not affect the default executor associated with the event loop, and will not do anything if you do not provide a custom executor to the constructor. coroutine_tracking_depth : typing.Optional[builtins.int] Defaults to `builtins.None`. If an integer value and supported by the interpreter, then this many nested coroutine calls will be tracked with their call origin state. This allows you to determine where non-awaited coroutines may originate from, but generally you do not want to leave this enabled for performance reasons. enable_signal_handlers : typing.Optional[builtins.bool] Defaults to `builtins.True` if this is started in the main thread. If on a __non-Windows__ OS with builtin support for kernel-level POSIX signals, then setting this to `builtins.True` will allow treating keyboard interrupts and other OS signals to safely shut down the application as calls to shut down the application properly rather than just killing the process in a dirty state immediately. You should leave this enabled unless you plan to implement your own signal handling yourself. host : typing.Optional[typing.Union[builtins.str, aiohttp.web.HostSequence]] TCP/IP host or a sequence of hosts for the HTTP server. port : typing.Optional[builtins.int] TCP/IP port for the HTTP server. path : typing.Optional[builtins.str] File system path for HTTP server unix domain socket. reuse_address : typing.Optional[builtins.bool] Tells the kernel to reuse a local socket in TIME_WAIT state, without waiting for its natural timeout to expire. reuse_port : typing.Optional[builtins.bool] Tells the kernel to allow this endpoint to be bound to the same port as other existing endpoints are also bound to. socket : typing.Optional[socket.socket] A pre-existing socket object to accept connections on. shutdown_timeout : builtins.float A delay to wait for graceful server shutdown before forcefully disconnecting all open client sockets. This defaults to 60 seconds. ssl_context : typing.Optional[ssl.SSLContext] SSL context for HTTPS servers. """ if self.is_alive: raise errors.ComponentStateConflictError("Cannot start a bot that's already active") loop = aio.get_or_make_loop() if asyncio_debug: loop.set_debug(True) if coroutine_tracking_depth is not None: try: # Provisionally defined in CPython, may be removed without notice. sys.set_coroutine_origin_tracking_depth(coroutine_tracking_depth) except AttributeError: _LOGGER.log(ux.TRACE, "cannot set coroutine tracking depth for sys, no functionality exists for this") try: loop.run_until_complete( self.start( backlog=backlog, check_for_updates=check_for_updates, enable_signal_handlers=enable_signal_handlers, host=host, port=port, path=path, reuse_address=reuse_address, reuse_port=reuse_port, socket=socket, shutdown_timeout=shutdown_timeout, ssl_context=ssl_context, ) ) loop.run_until_complete(self.join()) finally: if close_passed_executor and self._executor: self._executor.shutdown(wait=True) self._executor = None if close_loop: loop.close()
async def join(self) -> None: if not self._close_event: raise errors.ComponentStateConflictError("Cannot wait for an inactive bot to join") await self._close_event.wait()
def error(self): return errors.ComponentStateConflictError("some reason")
def _check_if_alive(self) -> None: if not self.is_alive: raise errors.ComponentStateConflictError( f"shard {self._shard_id} is not running so it cannot be interacted with" )
def _get_ws(self) -> _GatewayTransport: if not self._ws: raise errors.ComponentStateConflictError("Shard is shutting down") return self._ws
async def start( self, *, activity: typing.Optional[presences.Activity] = None, afk: bool = False, check_for_updates: bool = True, idle_since: typing.Optional[datetime.datetime] = None, ignore_session_start_limit: bool = False, large_threshold: int = 250, shard_ids: typing.Optional[typing.AbstractSet[int]] = None, shard_count: typing.Optional[int] = None, status: presences.Status = presences.Status.ONLINE, ) -> None: """Start the bot, wait for all shards to become ready, and then return. Other Parameters ---------------- activity : typing.Optional[hikari.presences.Activity] The initial activity to display in the bot user presence, or `builtins.None` (default) to not show any. afk : builtins.bool The initial AFK state to display in the bot user presence, or `builtins.False` (default) to not show any. check_for_updates : builtins.bool Defaults to `builtins.True`. If `builtins.True`, will check for newer versions of `hikari` on PyPI and notify if available. idle_since : typing.Optional[datetime.datetime] The `datetime.datetime` the user should be marked as being idle since, or `builtins.None` (default) to not show this. ignore_session_start_limit : builtins.bool Defaults to `builtins.False`. If `builtins.False`, then attempting to start more sessions than you are allowed in a 24 hour window will throw a `hikari.errors.GatewayError` rather than going ahead and hitting the IDENTIFY limit, which may result in your token being reset. Setting to `builtins.True` disables this behavior. large_threshold : builtins.int Threshold for members in a guild before it is treated as being "large" and no longer sending member details in the `GUILD CREATE` event. Defaults to `250`. shard_ids : typing.Optional[typing.AbstractSet[builtins.int]] The shard IDs to create shards for. If not `builtins.None`, then a non-`None` `shard_count` must ALSO be provided. Defaults to `builtins.None`, which means the Discord-recommended count is used for your application instead. shard_count : typing.Optional[builtins.int] The number of shards to use in the entire distributed application. Defaults to `builtins.None` which results in the count being determined dynamically on startup. status : hikari.presences.Status The initial status to show for the user presence on startup. Defaults to `hikari.presences.Status.ONLINE`. Raises ------ hikari.errors.ComponentStateConflictError If bot is already running. builtins.TypeError If `shard_ids` is passed without `shard_count`. """ if self._is_alive: raise errors.ComponentStateConflictError("bot is already running") if shard_ids is not None and shard_count is None: raise TypeError("'shard_ids' must be passed with 'shard_count'") self._validate_activity(activity) # Dispatch the update checker, the sharding requirements checker, and dispatch # the starting event together to save a little time on startup. start_time = time.monotonic() if check_for_updates: asyncio.create_task( ux.check_for_updates(self._http_settings, self._proxy_settings), name="check for package updates", ) self._rest.start() await self._event_manager.dispatch(self._event_factory.deserialize_starting_event()) requirements = await self._rest.fetch_gateway_bot_info() if shard_count is None: shard_count = requirements.shard_count if shard_ids is None: shard_ids = set(range(shard_count)) if requirements.session_start_limit.remaining < len(shard_ids) and not ignore_session_start_limit: _LOGGER.critical( "would have started %s session%s, but you only have %s session%s remaining until %s. Starting more " "sessions than you are allowed to start may result in your token being reset. To skip this message, " "use bot.run(..., ignore_session_start_limit=True) or bot.start(..., ignore_session_start_limit=True)", len(shard_ids), "s" if len(shard_ids) != 1 else "", requirements.session_start_limit.remaining, "s" if requirements.session_start_limit.remaining != 1 else "", requirements.session_start_limit.reset_at, ) raise errors.GatewayError("Attempted to start more sessions than were allowed in the given time-window") self._is_alive = True # This needs to be started before shards. self._voice.start() self._closing_event = asyncio.Event() _LOGGER.info( "you can start %s session%s before the next window which starts at %s; planning to start %s session%s... ", requirements.session_start_limit.remaining, "s" if requirements.session_start_limit.remaining != 1 else "", requirements.session_start_limit.reset_at, len(shard_ids), "s" if len(shard_ids) != 1 else "", ) for window_start in range(0, shard_count, requirements.session_start_limit.max_concurrency): window = [ candidate_shard_id for candidate_shard_id in range( window_start, window_start + requirements.session_start_limit.max_concurrency ) if candidate_shard_id in shard_ids ] if not window: continue if self._shards: close_waiter = asyncio.create_task(self._closing_event.wait()) shard_joiners = [s.join() for s in self._shards.values()] try: # Attempt to wait for all started shards, for 5 seconds, along with the close # waiter. # If the close flag is set (i.e. user invoked bot.close), or one or more shards # die in this time, we shut down immediately. # If we time out, the joining tasks get discarded and we spin up the next # block of shards, if applicable. _LOGGER.info("the next startup window is in 5 seconds, please wait...") await aio.first_completed(aio.all_of(*shard_joiners, timeout=5), close_waiter) if not close_waiter.cancelled(): _LOGGER.info("requested to shut down during startup of shards") else: _LOGGER.critical("one or more shards shut down unexpectedly during bot startup") return except asyncio.TimeoutError: # If any shards stopped silently, we should close. if any(not s.is_alive for s in self._shards.values()): _LOGGER.warning("one of the shards has been manually shut down (no error), will now shut down") await self.close() return # new window starts. except Exception as ex: _LOGGER.critical("an exception occurred in one of the started shards during bot startup: %r", ex) raise await aio.all_of( *( self._start_one_shard( activity=activity, afk=afk, idle_since=idle_since, status=status, large_threshold=large_threshold, shard_id=candidate_shard_id, shard_count=shard_count, url=requirements.url, closing_event=self._closing_event, ) for candidate_shard_id in window if candidate_shard_id in shard_ids ) ) await self._event_manager.dispatch(self._event_factory.deserialize_started_event()) _LOGGER.info("started successfully in approx %.2f seconds", time.monotonic() - start_time)
def run( self, *, activity: typing.Optional[presences.Activity] = None, afk: bool = False, asyncio_debug: typing.Optional[bool] = None, check_for_updates: bool = True, close_passed_executor: bool = False, close_loop: bool = True, coroutine_tracking_depth: typing.Optional[int] = None, enable_signal_handlers: bool = True, idle_since: typing.Optional[datetime.datetime] = None, ignore_session_start_limit: bool = False, large_threshold: int = 250, propagate_interrupts: bool = False, status: presences.Status = presences.Status.ONLINE, shard_ids: typing.Optional[typing.AbstractSet[int]] = None, shard_count: typing.Optional[int] = None, ) -> None: """Start the bot, wait for all shards to become ready, and then return. Other Parameters ---------------- activity : typing.Optional[hikari.presences.Activity] The initial activity to display in the bot user presence, or `builtins.None` (default) to not show any. afk : builtins.bool The initial AFK state to display in the bot user presence, or `builtins.False` (default) to not show any. asyncio_debug : builtins.bool Defaults to `builtins.False`. If `builtins.True`, then debugging is enabled for the asyncio event loop in use. check_for_updates : builtins.bool Defaults to `builtins.True`. If `builtins.True`, will check for newer versions of `hikari` on PyPI and notify if available. close_passed_executor : builtins.bool Defaults to `builtins.False`. If `builtins.True`, any custom `concurrent.futures.Executor` passed to the constructor will be shut down when the application terminates. This does not affect the default executor associated with the event loop, and will not do anything if you do not provide a custom executor to the constructor. close_loop : builtins.bool Defaults to `builtins.True`. If `builtins.True`, then once the bot enters a state where all components have shut down permanently during application shutdown, then all asyngens and background tasks will be destroyed, and the event loop will be shut down. This will wait until all `hikari`-owned `aiohttp` connectors have had time to attempt to shut down correctly (around 250ms), and on Python 3.9 and newer, will also shut down the default event loop executor too. coroutine_tracking_depth : typing.Optional[builtins.int] Defaults to `builtins.None`. If an integer value and supported by the interpreter, then this many nested coroutine calls will be tracked with their call origin state. This allows you to determine where non-awaited coroutines may originate from, but generally you do not want to leave this enabled for performance reasons. enable_signal_handlers : builtins.bool Defaults to `builtins.True`. If on a __non-Windows__ OS with builtin support for kernel-level POSIX signals, then setting this to `builtins.True` will allow treating keyboard interrupts and other OS signals to safely shut down the application as calls to shut down the application properly rather than just killing the process in a dirty state immediately. You should leave this disabled unless you plan to implement your own signal handling yourself. idle_since : typing.Optional[datetime.datetime] The `datetime.datetime` the user should be marked as being idle since, or `builtins.None` (default) to not show this. ignore_session_start_limit : builtins.bool Defaults to `builtins.False`. If `builtins.False`, then attempting to start more sessions than you are allowed in a 24 hour window will throw a `hikari.errors.GatewayError` rather than going ahead and hitting the IDENTIFY limit, which may result in your token being reset. Setting to `builtins.True` disables this behavior. large_threshold : builtins.int Threshold for members in a guild before it is treated as being "large" and no longer sending member details in the `GUILD CREATE` event. Defaults to `250`. propagate_interrupts : builtins.bool Defaults to `builtins.False`. If set to `builtins.True`, then any internal `hikari.errors.HikariInterrupt` that is raises as a result of catching an OS level signal will result in the exception being rethrown once the application has closed. This can allow you to use hikari signal handlers and still be able to determine what kind of interrupt the application received after it closes. When `builtins.False`, nothing is raised and the call will terminate cleanly and silently where possible instead. shard_ids : typing.Optional[typing.AbstractSet[builtins.int]] The shard IDs to create shards for. If not `builtins.None`, then a non-`None` `shard_count` must ALSO be provided. Defaults to `builtins.None`, which means the Discord-recommended count is used for your application instead. shard_count : typing.Optional[builtins.int] The number of shards to use in the entire distributed application. Defaults to `builtins.None` which results in the count being determined dynamically on startup. status : hikari.presences.Status The initial status to show for the user presence on startup. Defaults to `hikari.presences.Status.ONLINE`. Raises ------ hikari.errors.ComponentStateConflictError If bot is already running. builtins.TypeError If `shard_ids` is passed without `shard_count`. """ if self._is_alive: raise errors.ComponentStateConflictError("bot is already running") if shard_ids is not None and shard_count is None: raise TypeError("'shard_ids' must be passed with 'shard_count'") loop = aio.get_or_make_loop() signals = ("SIGINT", "SIGTERM") if asyncio_debug: loop.set_debug(True) if coroutine_tracking_depth is not None: try: # Provisionally defined in CPython, may be removed without notice. sys.set_coroutine_origin_tracking_depth(coroutine_tracking_depth) # type: ignore[attr-defined] except AttributeError: _LOGGER.log(ux.TRACE, "cannot set coroutine tracking depth for sys, no functionality exists for this") # Throwing this in the handler will lead to lots of fun OS specific shenanigans. So, lets just # cache it for later, I guess. interrupt: typing.Optional[errors.HikariInterrupt] = None loop_thread_id = threading.get_native_id() def handle_os_interrupt(signum: int, frame: types.FrameType) -> None: # If we use a POSIX system, then raising an exception in here works perfectly and shuts the loop down # with an exception, which is good. # Windows, however, is special on this front. On Windows, the exception is caught by whatever was # currently running on the event loop at the time, which is annoying for us, as this could be fired into # the task for an event dispatch, for example, which is a guarded call that is never waited for by design. # We can't always safely intercept this either, as Windows does not allow us to use asyncio loop # signal listeners (since Windows doesn't have kernel-level signals, only emulated system calls # for a remote few standard C signal types). Thus, the best solution here is to set the close bit # instead, which will let the bot start to clean itself up as if the user closed it manually via a call # to `bot.close()`. nonlocal interrupt signame = signal.strsignal(signum) assert signame is not None # Will always be True interrupt = errors.HikariInterrupt(signum, signame) # The loop may or may not be running, depending on the state of the application when this occurs. # Signals on POSIX only occur on the main thread usually, too, so we need to ensure this is # threadsafe if we want the user's application to still shut down if on a separate thread. # We log native thread IDs purely for debugging purposes. if _LOGGER.isEnabledFor(ux.TRACE): _LOGGER.log( ux.TRACE, "interrupt %s occurred on thread %s, bot on thread %s will be notified to shut down shortly\n" "Stacktrace for developer sanity:\n%s", signum, threading.get_native_id(), loop_thread_id, "".join(traceback.format_stack(frame)), ) asyncio.run_coroutine_threadsafe(self._set_close_flag(signame, signum), loop) if enable_signal_handlers: for sig in signals: try: signum = getattr(signal, sig) signal.signal(signum, handle_os_interrupt) except AttributeError: _LOGGER.log(ux.TRACE, "signal %s is not implemented on your platform", sig) try: loop.run_until_complete( self.start( activity=activity, afk=afk, check_for_updates=check_for_updates, idle_since=idle_since, ignore_session_start_limit=ignore_session_start_limit, large_threshold=large_threshold, shard_ids=shard_ids, shard_count=shard_count, status=status, ) ) loop.run_until_complete(self.join()) finally: try: loop.run_until_complete(self.close()) if close_passed_executor and self._executor is not None: _LOGGER.debug("shutting down executor %s", self._executor) self._executor.shutdown(wait=True) self._executor = None finally: if enable_signal_handlers: for sig in signals: try: signum = getattr(signal, sig) signal.signal(signum, signal.SIG_DFL) except AttributeError: # Signal not implemented probably. We should have logged this earlier. pass if close_loop: self._destroy_loop(loop) _LOGGER.info("successfully terminated") if propagate_interrupts and interrupt is not None: raise interrupt