8000 Fix Webhook not working on Windows with Python 3.8+ (#2067) · Konano/python-telegram-bot@bb34c79 · GitHub
[go: up one dir, main page]

Skip to content

Commit bb34c79

Browse files
Bibo-Joshin5y
andauthored
Fix Webhook not working on Windows with Python 3.8+ (python-telegram-bot#2067)
* Fix start_webhook NotImplementedError on windows with python3.8 * Fine-tune and add tests. * Minor fixes * typos * Make Codacy happy Co-authored-by: n5y <41209360+n5y@users.noreply.github.com>
1 parent a0720b9 commit bb34c79

File tree

3 files changed

+200
-93
lines changed
  • tests
  • 3 files changed

    +200
    -93
    lines changed

    telegram/ext/updater.py

    Lines changed: 33 additions & 7 deletions
    Original file line numberDiff line numberDiff line change
    @@ -258,11 +258,15 @@ def start_polling(self,
    258258
    # Create & start threads
    259259
    self.job_queue.start()
    260260
    dispatcher_ready = Event()
    261+
    polling_ready = Event()
    261262
    self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
    262263
    self._init_thread(self._start_polling, "updater", poll_interval, timeout,
    263-
    read_latency, bootstrap_retries, clean, allowed_updates)
    264+
    read_latency, bootstrap_retries, clean, allowed_updates,
    265+
    ready=polling_ready)
    264266

    267+
    self.logger.debug('Waiting for Dispatcher and polling to start')
    265268
    dispatcher_ready.wait()
    269+
    polling_ready.wait()
    266270

    267271
    # Return the update queue so the main thread can insert updates
    268272
    return self.update_queue
    @@ -276,14 +280,24 @@ def start_webhook(self,
    276280
    clean=False,
    277281
    bootstrap_retries=0,
    278282
    webhook_url=None,
    279-
    allowed_updates=None):
    283+
    allowed_updates=None,
    284+
    force_event_loop=False):
    280285
    """
    281286
    Starts a small http server to listen for updates via webhook. If cert
    282287
    and key are not provided, the webhook will be started directly on
    283288
    http://listen:port/url_path, so SSL can be handled by another
    284289
    application. Else, the webhook will be started on
    285290
    https://listen:port/url_path
    286291
    292+
    Note:
    293+
    Due to an incompatibility of the Tornado library PTB uses for the webhook with Python
    294+
    3.8+ on Windows machines, PTB will attempt to set the event loop to
    295+
    :attr:`asyncio.SelectorEventLoop` and raise an exception, if an incompatible event loop
    296+
    has already been specified. See this `thread`_ for more details. To suppress the
    297+
    exception, set :attr:`force_event_loop` to :obj:`True`.
    298+
    299+
    .. _thread: https://github.com/tornadoweb/tornado/issues/2608
    300+
    287301
    Args:
    288302
    listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
    289303
    port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``.
    @@ -303,6 +317,8 @@ def start_webhook(self,
    303317
    NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`.
    304318
    allowed_updates (List[:obj:`str`], optional): Passed to
    305319
    :attr:`telegram.Bot.set_webhook`.
    320+
    force_event_loop (:obj:`bool`, optional): Force using the current event loop. See above
    321+
    note for details. Defaults to :obj:`False`
    306322
    307323
    Returns:
    308324
    :obj:`Queue`: The update queue that can be filled from the main thread.
    @@ -313,16 +329,23 @@ def start_webhook(self,
    313329
    self.running = True
    314330

    315331
    # Create & start threads
    332+
    webhook_ready = Event()
    333+
    dispatcher_ready = Event()
    316334
    self.job_queue.start()
    317-
    self._init_thread(self.dispatcher.start, "dispatcher"),
    335+
    self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
    318336
    self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert,
    319-
    key, bootstrap_retries, clean, webhook_url, allowed_updates)
    337+
    key, bootstrap_retries, clean, webhook_url, allowed_updates,
    338+
    ready=webhook_ready, force_event_loop=force_event_loop)
    339+
    340+
    self.logger.debug('Waiting for Dispatcher and Webhook to start')
    341+
    webhook_ready.wait()
    342+
    dispatcher_ready.wait()
    320343

    321344
    # Return the update queue so the main thread can insert updates
    322345
    return self.update_queue
    323346

    324347
    def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean,
    325-
    allowed_updates): # pragma: no cover
    348+
    allowed_updates, ready=None): # pragma: no cover
    326349
    # Thread target of thread 'updater'. Runs in background, pulls
    327350
    # updates from Telegram and inserts them in the update queue of the
    328351
    # Dispatcher.
    @@ -354,6 +377,9 @@ def polling_onerr_cb(exc):
    354377
    # broadcast it
    355378
    self.update_queue.put(exc)
    356379

    380+
    if ready is not None:
    381+
    ready.set()
    382+
    357383
    self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates',
    358384
    poll_interval)
    359385

    @@ -410,7 +436,7 @@ def _increase_poll_interval(current_interval):
    410436
    return current_interval
    411437

    412438
    def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean,
    413-
    webhook_url, allowed_updates):
    439+
    webhook_url, allowed_updates, ready=None, force_event_loop=False):
    414440
    self.logger.debug('Updater thread started (webhook)')
    415441
    use_ssl = cert is not None and key is not None
    416442
    if not url_path.startswith('/'):
    @@ -448,7 +474,7 @@ def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, c
    448474
    self.logger.warning("cleaning updates is not supported if "
    449475
    "SSL-termination happens elsewhere; skipping")
    450476

    451-
    self.httpd.serve_forever()
    477+
    self.httpd.serve_forever(force_event_loop=force_event_loop, ready=ready)
    452478

    453479
    @staticmethod
    454480
    def _gen_webhook_url(listen, port, url_path):

    telegram/utils/webhookhandler.py

    Lines changed: 47 additions & 33 deletions
    Original file line numberDiff line numberDiff line change
    @@ -16,10 +16,13 @@
    1616
    #
    1717
    # You should have received a copy of the GNU Lesser Public License
    1818
    # along with this program. If not, see [http://www.gnu.org/licenses/].
    19+
    import asyncio
    20+
    import os
    1921
    import sys
    2022
    import logging
    2123
    from telegram import Update
    2224
    from threading import Lock
    25+
    2326
    try:
    2427
    import ujson as json
    2528
    except ImportError:
    @@ -41,13 +44,17 @@ def __init__(self, listen, port, webhook_app, ssl_ctx):
    4144
    self.server_lock = Lock()
    4245
    self.shutdown_lock = Lock()
    4346

    44-
    def serve_forever(self):
    47+
    def serve_forever(self, force_event_loop=False, ready=None):
    4548
    with self.server_lock:
    46-
    IOLoop().make_current()
    4749
    self.is_running = True
    4850
    self.logger.debug('Webhook Server started.')
    49-
    self.http_server.listen(self.port, address=self.listen)
    51+
    self._ensure_event_loop(force_event_loop=force_event_loop)
    5052
    self.loop = IOLoop.current()
    53+
    self.http_server.listen(self.port, address=self.listen)
    54+
    55+
    if ready is not None:
    56+
    ready.set()
    57+
    5158
    self.loop.start()
    5259
    self.logger.debug('Webhook Server stopped.')
    5360
    self.is_running = False
    @@ -65,6 +72,42 @@ def handle_error(self, request, client_address):
    6572
    self.logger.debug('Exception happened during processing of request from %s',
    6673
    client_address, exc_info=True)
    6774

    75+
    def _ensure_event_loop(self, force_event_loop=False):
    76+
    """If there's no asyncio event loop set for the current thread - create one."""
    77+
    try:
    78+
    loop = asyncio.get_event_loop()
    79+
    if (not force_event_loop and os.name == 'nt' and sys.version_info >= (3, 8)
    80+
    and isinstance(loop, asyncio.ProactorEventLoop)):
    81+
    raise TypeError('`ProactorEventLoop` is incompatible with '
    82+
    'Tornado. Please switch to `SelectorEventLoop`.')
    83+
    except RuntimeError:
    84+
    # Python 3.8 changed default asyncio event loop implementation on windows
    85+
    # from SelectorEventLoop to ProactorEventLoop. At the time of this writing
    86+
    # Tornado doesn't support ProactorEventLoop and suggests that end users
    87+
    # change asyncio event loop policy to WindowsSelectorEventLoopPolicy.
    88+
    # https://github.com/tornadoweb/tornado/issues/2608
    89+
    # To avoid changing the global event loop policy, we manually construct
    90< 10000 /code>+
    # a SelectorEventLoop instance instead of using asyncio.new_event_loop().
    91+
    # Note that the fix is not applied in the main thread, as that can break
    92+
    # user code in even more ways than changing the global event loop policy can,
    93+
    # and because Updater always starts its webhook server in a separate thread.
    94+
    # Ideally, we would want to check that Tornado actually raises the expected
    95+
    # NotImplementedError, but it's not possible to cleanly recover from that
    96+
    # exception in current Tornado version.
    97+
    if (os.name == 'nt'
    98+
    and sys.version_info >= (3, 8)
    99+
    # OS+version check makes hasattr check redundant, but just to be sure
    100+
    and hasattr(asyncio, 'WindowsProactorEventLoopPolicy')
    101+
    and (isinstance(
    102+
    asyncio.get_event_loop_policy(),
    103+
    asyncio.WindowsProactorEventLoopPolicy))): # pylint: disable=E1101
    104+
    self.logger.debug(
    105+
    'Applying Tornado asyncio event loop fix for Python 3.8+ on Windows')
    106+
    loop = asyncio.SelectorEventLoop()
    107+
    else:
    108+
    loop = asyncio.new_event_loop()
    109+
    asyncio.set_event_loop(loop)
    110+
    68111

    69112
    class WebhookAppClass(tornado.web.Application):
    70113

    @@ -74,7 +117,7 @@ def __init__(self, webhook_path, bot, update_queue, default_quote=None):
    74117
    handlers = [
    75118
    (r"{}/?".format(webhook_path), WebhookHandler,
    76119
    self.shared_objects)
    77-
    ] # noqa
    120+
    ] # noqa
    78121
    tornado.web.Application.__init__(self, handlers)
    79122

    80123
    def log_request(self, handler):
    @@ -88,35 +131,6 @@ class WebhookHandler(tornado.web.RequestHandler):
    88131
    def __init__(self, application, request, **kwargs):
    89132
    super().__init__(application, request, **kwargs)
    90133
    self.logger = logging.getLogger(__name__)
    91-
    self._init_asyncio_patch()
    92-
    93-
    def _init_asyncio_patch(self):
    94-
    """set default asyncio policy to be compatible with tornado
    95-
    Tornado 6 (at least) is not compatible with the default
    96-
    asyncio implementation on Windows
    97-
    Pick the older SelectorEventLoopPolicy on Windows
    98-
    if the known-incompatible default policy is in use.
    99-
    do this as early as possible to make it a low priority and overrideable
    100-
    ref: https://github.com/tornadoweb/tornado/issues/2608
    101-
    TODO: if/when tornado supports the defaults in asyncio,
    102-
    remove and bump tornado requirement for py38
    103-
    Copied from https://github.com/ipython/ipykernel/pull/456/
    104-
    """
    105-
    if sys.platform.startswith("win") and sys.version_info >= (3, 8):
    106-
    import asyncio
    107-
    try:
    108-
    from asyncio import (
    109-
    WindowsProactorEventLoopPolicy,
    110-
    WindowsSelectorEventLoopPolicy,
    111-
    )
    112-
    except ImportError:
    113-
    pass
    114-
    # not affected
    115-
    else:
    116-
    if isinstance(asyncio.get_event_loop_policy(), WindowsProactorEventLoopPolicy):
    117-
    # WindowsProactorEventLoopPolicy is not compatible with tornado 6
    118-
    # fallback to the pre-3.8 default of Selector
    119-
    asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
    120134

    121135
    def initialize(self, bot, update_queue, default_quote=None):
    122136
    self.bot = bot

    0 commit comments

    Comments
     (0)
    0