qwertyquerty/pypresence

`InvalidID` is raised after Discord restart

FrzMtrsprt opened this issue · 14 comments

When trying to update after Discord has restarted, a BrokenPipe exception will raise a InvaldID exception.

And if I catch the InvalidID exception and try to reconnect with Presence.connect(), RuntimeError: There is no current event loop in thread 'Thread-1 (_target)'. is raised at loop = asyncio.get_event_loop(), as described in #208.

Below is a script to reproduce the error:

import time
from threading import Event, Thread
from pypresence import InvalidID, Presence

client_id = 'CLIENT_ID_HERE'
presence = Presence(client_id)
presence.connect()

class RepeatedTimer:
    def __init__(self, interval, function):
        self.interval = interval
        self.function = function
        self.start = time.time()
        self.event = Event()
        self.thread = Thread(target=self._target)
        self.thread.start()
    def _target(self):
        while not self.event.wait(self._time):
            self.function()
    @property
    def _time(self):
        return self.interval - ((time.time() - self.start) % self.interval)

def update():
    try:
        print("Updating")
        presence.update(state="test")  # InvalidID
    except InvalidID:
        print("Reconnecting")
        presence.connect()  # RuntimeError: There is no current event loop in thread 'Thread-1 (_target)'.

RepeatedTimer(15, update)

If Discord was restarted during the 15 seconds interval, the InvalidID exception will be raised at the next update, and connect() will fail to get the current event loop.

Which platform @FrzMtrsprt

Which platform @FrzMtrsprt

Windows

Try modify the script utils.py as shown here

Try modify the script utils.py as shown here

It works, and connect() is now able to create a new event loop.
But should we use another exception for the broken pipe after Discord restart, instead of InvalidID?

Send the full error on the console see if i can do something

Send the full error on the console see if i can do something

Exception has occurred: InvalidID
Client ID is Invalid
  File "...\pypresence\baseclient.py", line 81, in read_output
    preamble = await self.sock_reader.read(8)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
BrokenPipeError: [WinError 232] The pipe has been ended.

During handling of the above exception, another exception occurred:

  File "...\pypresence\baseclient.py", line 85, in read_output
    raise InvalidID
  File "...\pypresence\presence.py", line 34, in update
    return self.loop.run_until_complete(self.read_output())
  File "...\main.py", line 227, in update
    RPC.update(pid=pid,
  File "...\main.py", line 47, in _target
    self.function()
pypresence.exceptions.InvalidID: Client ID is Invalid

The best way to do this is by checking if the pipe is opened. After capturing the InvalidID error add a function or these lines that checks if discord is running if true reconnect something like this.

import psutil

def update():
    try:
        print("Updating")
        presence.update(state="test")  # InvalidID
    except InvalidID:
        print("Reconnecting")
        try:
            presence.connect()
        except BrokenPipeError:
            port_range = range(6463,6473) #range of ports the rpc may use
            for port in port_range:
                if port in ([i.laddr.port for i in psutil.net_connections()]): #check if either one of the port is open
                    presence.connect() #if open we reconnect

Maybe I didn't describe the error clearly, but presence.connect() didn't throw BrokenPipeError, presence.update() did.
It was thrown in the following code:

# baseclient.py
# BaseClient.read_output(self)
try:
    preamble = await self.sock_reader.read(8) # BrokenPipeError was thrown here
    status_code, length = struct.unpack('<II', preamble[:8])
    data = await self.sock_reader.read(length)
except BrokenPipeError:
    raise InvalidID

The situation now (after applying the changes from your PR) is, doing presence.update() after a discord restart will raise an InvalidID exception. If I catch it and do a presence.connect(), everything will go back to normal, but I don't think that is the intended behaviour.

The intended behaviour should be, throwing a dedicated exception (for example ConnectionLost) telling the user to reconnect, or just reconnect automatically within presence.update()

In my opinion, scanning for rpc ports should be the work of pypresence, not the user. If pypresence is not able to decide whether it's an invalid ID or a broken connection, at least make the exception name less confusing.

Yeah the reconnecting should be done on the pypresence module, to be brief the pypresence modules is not that clean and misses some things but if the solution works that should hold on for a while

So I tested everything and the issue is caused when updating as you said, the fix is simply putting a time delay befor updating.
Atleast this worked for me

def update():
    if start_presence(): #Before we send an update we check if discord is running
        print("Updating")
        presence.update(state="test")  
   

def start_presence():
    time.sleep(2) #add a delay here 
    for i in range(6463,6473): #same function as the psutil I used earlier this doesnt require extra installs
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(0.01)
        conn = s.connect_ex(('localhost', i))
        if (conn == 0):
            return(True)
        else:
            return(False)

Thanks for the suggection, but I'll stick to catching the InvalidID for the time being, because my program is quite time crucial and can't afford a 2 second sleep. Thanks for helping out anyway!

The time delay is not that important you can remove it, provided when the error occurs close the rpc then check if discord is open before reconnecting cause it may cause errors then reconnect

Try modify the script utils.py as shown here

It works, and connect() is now able to create a new event loop. But should we use another exception for the broken pipe after Discord restart, instead of InvalidID?

@FrzMtrsprt Could you please confirm if 1022b07 fixes this?

If you're deploying pypresence in an application my suggestion would be to create your own connectDiscord() function that calls .connect() and catches DiscordNotFound, InvalidPipe, ConnectionTimeout. Whether you then want to wait an amount of time before retrying or instantly attempt to reconnect is completely up to you.

Yes, everything works fine in the lastest commit.