Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C-Python asyncio: running discord.py in a thread

I have to launch discord.py in a separate thread since I can't block my main thread.
It is a game server C/Python 3.7 (ubuntu 18)

C code:

int pysDiscord_Init;
...
PyObject *psv_discord;

psv_discord = Python_LoadModule("sv_discord");
if (psv_discord != NULL) {
  pysDiscord_Init = Python_RegisterFunction(psv_discord, "sv_discord", "init");
  Python_Execute(pysDiscord_Init, "");
}

sv_discord.py

import discord
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor
import multiprocessing

TOKEN = '12345'

client = discord.Client()

def init():
    print("Initializing Discord...")
    print("current_thread: %s" % threading.current_thread())
    t = threading.Thread(target=client.run, args=(TOKEN,))
    t.start()

or

def init():
    print("Initializing Discord...")
    print("current_thread: %s" % threading.current_thread())
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    asyncio.get_child_watcher().attach_loop(loop)
    pool = ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
    task = loop.run_in_executor(pool, client.run, TOKEN)
    loop.run_until_complete(task)

set_wakeup_fd exception:

...
Initializing Discord...
current_thread: <_MainThread(MainThread, started 4150019840)>

Exception in thread Thread-1:
 Traceback (most recent call last):
 File "./build/Lib/asyncio/unix_events.py", line 92, in add_signal_handler
 ValueError: set_wakeup_fd only works in main thread

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
 File "./build/Lib/threading.py", line 917, in _bootstrap_inner
 File "./build/Lib/threading.py", line 865, in run
 File "./../source/discord.py-rewrite/discord/client.py", line 550, in run
 File "./build/Lib/asyncio/unix_events.py", line 94, in add_signal_handler
RuntimeError: set_wakeup_fd only works in main thread

I should mention that I tried the same code on the python (without the C code) and it works.
This error tells me about the main thread. But I don't create sv_discord inside the new thread, and as you can see from the log, it is the "Main" thread inside init() method. I don't understand this.

like image 344
IgorZ Avatar asked Mar 06 '19 19:03

IgorZ


2 Answers

Answering my own question:

I should thank this source asyncio-you-are-a-complex-beast where I finally found a solution.
The final working code looks like this:

import discord
import asyncio
from threading import Thread


client = discord.Client()


def init():
    loop = asyncio.get_event_loop()
    loop.create_task(client.start(TOKEN))
    Thread(target=loop.run_forever).start()


@client.event
async def on_message(message):
    if message.author == client.user:
        return

    print("on_message content: %s, channel: %s" % (message.content, message.channel))
    await message.channel.send('Hello!')


@client.event
    async def on_ready():
    print("Discord bot logged in as: %s, %s" % (client.user.name, client.user.id))

My main mistake was that for the game I compiled and used the latest rewrite version while inside the system over the pip I got 0.16.12 and read the documentation for 0.16.12 while I had to look at discord.py.rewrite (for example inside on_message I used wrong client.send_message while I had to use message.channel.send)

like image 191
IgorZ Avatar answered Sep 21 '22 04:09

IgorZ


I had a similar edge case (not really the intended use of asyncio but what the hell) where I needed to Thread the discord.py instance.

I ended up modifying your solution with the following:

class discordHost(discord.Client):
    async def on_ready(self):
        print(f'{DThread.discord_client.user} has connected.')

class Threader(Thread):
    def __init__(self):
        Thread.__init__(self)
        self.loop = asyncio.get_event_loop()
        self.start()

    async def starter(self):
        self.discord_client = discordHost()
        await self.discord_client.start(DISCORD_TOKEN)

    def run(self):
        self.name = 'Discord.py'

        self.loop.create_task(self.starter())
        self.loop.run_forever()

if not 'Discord.py' in [t.name for t in tenumerate()]:
    DThread = Threader()

This does more or less the same thing, but I've subclassed Thread and subclassed discord.Client to more easily work with the outcome of the two.

The only caveat here, is that asyncio.get_event_loop() has to be called before Thread.start(), otherwise it gets confused and losses it's context of the main thread.

like image 28
Torxed Avatar answered Sep 20 '22 04:09

Torxed