Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

on_close() doesn't get called after calling close() #612

Open
Jedore opened this issue Apr 2, 2020 · 19 comments
Open

on_close() doesn't get called after calling close() #612

Jedore opened this issue Apr 2, 2020 · 19 comments

Comments

@Jedore
Copy link

Jedore commented Apr 2, 2020

code just like:

class MyWS(object):
    def __init__(self):
        self._ws = None
    def on_close(self):
        print("closed")

    def run(self):
        try:
            # websocket.enableTrace(True)
            self._ws = websocket.WebSocketApp(*******)
            self._ws.on_close = self.on_close
            self._ws.run_forever()
        except Exception:
            pass

After calling run() above and connection successful, I call self._ws.close() somewhere, But on_close() func doesn't get called. What's wrong?
In _app.py:

    def close(self, **kwargs):
        """
        close websocket connection.
        """
        self.keep_running = False
        if self.sock:
            self.sock.close(**kwargs)
            self.sock = None

As above, After calling self._ws.close(), self.keep_running is set to False, Then in _app.py:

class Dispatcher:
    def __init__(self, app, ping_timeout):
        self.app = app
        self.ping_timeout = ping_timeout

    def read(self, sock, read_callback, check_callback):
        while self.app.keep_running:
            r, w, e = select.select(
                    (self.app.sock.sock, ), (), (), self.ping_timeout)
            if r:
                if not read_callback():
                    break
            check_callback()

the while loop in read() func will be over according to self.keep_running. After dispatcher.read(), nothing is going to be done in run_forever() func.

Is there some suggestions? Thks.

@zyang01
Copy link

zyang01 commented Apr 4, 2020

Same issue after upgrading to 0.57.0. Went back to 0.40.0 and all worked fine again.

@Jedore
Copy link
Author

Jedore commented Apr 5, 2020

Same issue after upgrading to 0.57.0. Went back to 0.40.0 and all worked fine again.

Yeah, I used 0.57.0.

@romeubertho
Copy link

The same issue with me, I had to downgrade to 0.54.0

@urbanonymous
Copy link

Lost few hours on this, using 0.47 now haha

@TomsonRiviera
Copy link

I downgraded to 0.52.0 and it worked, not working on above versions.

@engn33r
Copy link
Collaborator

engn33r commented Mar 5, 2021

Looks like this might be the same issue as #452, maybe #508 and #534 too. Would be great to see a PR if anyone has one. I might not get to this issue for a bit.

@lalondesteve
Copy link

lalondesteve commented Mar 5, 2021

It seems it has evolved, in 0.58 I have this error on KeyboardInterrupt when assigning any function to on_close:

def on_close(): print("closed")
ws = websocket.WebSocketApp("ws://echo.websocket.org/", on_close=on_close)
ws.run_forever()

KeyboardInterrupt

error from callback <function on_close at 0x10a6bbf70>: on_close() takes 0 positional arguments but 1 was given
File "***/venv/lib/python3.9/site-packages/websocket/_app.py", line 388, in _callback
    callback(self, *args)
False

@engn33r
Copy link
Collaborator

engn33r commented Mar 7, 2021

@lalondesteve Thanks for posting a nice simple example. I was able to recreate the error you got and the issue is that your on_close() function needs to accept the WebSocketApp as an argument. The following fixes your error:

def on_close(ws): # This line has the fix
  print("closed")

ws = websocket.WebSocketApp("ws://echo.websocket.org/", on_close=on_close)
ws.run_forever()

I think the original issue here is different though. If I understand the original issue correctly, the following code snippet recreates the original issue where the word "closed" is never printed out, even though the connection does get closed:

import websocket

websocket.enableTrace(True)

def on_open(wsapp):
    wsapp.send("hi")

def on_message(wsapp, message):
    print(message)
    wsapp.close()

def on_close(wsapp): # new
  print("closed")

wsapp = websocket.WebSocketApp("ws://echo.websocket.org", on_open=on_open, on_message=on_message, on_close=on_close)
wsapp.run_forever()

@lalondesteve
Copy link

lalondesteve commented Mar 7, 2021

You're right, I'm an idiot! Thanks for this very nice answer.

FYI, in my tests after your fix, the "closed" is printing with KeyboardInterrupt and using Thread

>>> import websocket
>>> from threading import Thread
>>>
>>> def on_close(wsapp):
...     print("closed for real")
>>>
>>> wsapp = websocket.WebSocketApp("ws://echo.websocket.org", on_close=on_close)
>>> t = Thread(target=wsapp.run_forever)
>>> t.start()
>>> wsapp.close()
closed for real

@engn33r
Copy link
Collaborator

engn33r commented Mar 7, 2021

A follow-up on the original issue of this thread: it looks like the echoapp_client.py included under the /examples directory of this repository uses close() to call the on_close() function, and on_close() is indeed called when I tested it. I don't have time right now, but if someone is able to review this example and figure out a fix to their code, I would be interested to hear what the difference is between echoapp_client.py and the code where you experience this issue. There should be a way to make this work if the example is working!

Edit: plus lalondesteve helpfully confirmed that he called on_close() using close() in the above comment ^ Maybe this issue is related to threading, since both examples where on_close() works contain from threading import Thread?

@Jedore
Copy link
Author

Jedore commented Mar 8, 2021

@engn33r Thanks firstly, I will try to recreate this error in a few days.

@engn33r
Copy link
Collaborator

engn33r commented Mar 30, 2021

I looked at the code example from @lalondesteve and discovered that the on_close() function in his comment was only called because of the explicit wsapp.close() call that happened outside of another function. When I tried to use wsapp.close() inside a custom on_message() function as seen in the code example I posted previously, I was still not able to trigger the on_close() function from within on_message(). However, after looking more carefully at the echoapp_client.py code, I was able to create two pieces of similar example code, one working and one not working, which I have posted to the threading documentation page for illustration purposes. If someone has another example of using threading that they could share, please submit a PR to update the documentation, since this project could use more threading documentation.

One weird behavior I encountered with the working threading code example posted on the threading documentation page was that it doesn't usually work for ws://echo.websocket.org. I encountered strange behavior where about 1 in 5 tries connecting to ws://echo.websocket.org did result in calling on_close(), but most of the time on_close() was not called. I also observed this variation in behavior on some other WebSockets servers. This might be the same issue as #452 or #534. At first I wondered if some servers send different data on close, but I examined the communications using Wireshark and didn't see any noticeable differences between the connections that triggered on_close() and those that didn't. I suspect there is something deeper happening here that I don't understand yet, perhaps related to the local network stack, buffers, socket handling, etc. Can unclosed WebSockets leave artifacts on the system that can interfere with other connections?

Since there is new example code on the threading page that should provide a solution to the simplest case of this issue (ignoring the variable behavior that I observed for certain endpoints), I am closing this issue for now. We can reopen discussions if there are still loose ends on this topic or if issues with the variable behavior are encountered and problematic.

@Goggy-one
Copy link

I was able to find out something else about this issue.
The working code example from @engn33r only works because of the print(massage) before the ws.close().
For some reason there must always be a print() inside on_message() and the thread before the close(), otherwise on_close will not be triggered.

I must admit that this is the first time I have encountered a problem that can be solved by a print. XD

Here are the examples:
working on_message():

def on_message(ws, message):
    def run(*args):
        print('test')
        ws.close()
        print("Message received...")

    threading.Thread(target=run).start()
  1. not working on_message() :
def on_message(ws, message):
    def run(*args):
        ws.close()
        print("Message received...")

    threading.Thread(target=run).start()


  1. not working on_message() :
def on_message(ws, message):
    print('test')
    def run(*args):
        test = 'test'
        ws.close()
        print(test)
        print("Message received...")

    threading.Thread(target=run).start()

Maybe this will help someone who had the same problems as me and didn't put a print() between def run(*args): and ws.close().

@wormsik
Copy link

wormsik commented Apr 17, 2023

@engn33r I also spend some time debugging this issue and it seems to me that it is a matter of timing. This is what I found:

  • Running close() will set keep_running to False
  • Setting keep_running to False should trigger teardown() method from which calls on_close() callback
  • However, dispatchers are also implemented in a way they also checks for keep_running boolean

So whenever you call close() synchronously (or seemingly synchronously) inside on_message callback, keep_running is set to False and since dispatcher also checks for it, there will never be any new read() method call from dispatcher and thus no teardown() call which would call on_close() callback.

Whenever you defer close call (and from my tests it seems that the thread needs to be somehow delayed - sleep, print, whatever), the original on_message handler would be able to end, dispatcher would already be waiting for new data and closing the socket (in my guess) would make read() method to be executed for the last time and on_close() callback is correctly emitted.

My current solution is to call close using a timer (it seems to works every repeat):

import websocket
import threading

websocket.enableTrace(True)

timer = None

def on_message(ws, message):
    print(message)
    timer = threading.Timer(2, ws.close)
    timer.start()

def on_close(ws, close_status_code, close_msg):
    print(">>>>>>CLOSED")

wsapp = websocket.WebSocketApp("ws://echo.websocket.events", on_message=on_message, on_close=on_close)

wsapp.run_forever()
if timer:
    # Just to avoid unnecessary waiting in case server closes the connection by itself
    timer.cancel()

I guess this might be fully solved by not using keep_running boolean in dispatcher or having dispatcher to call teardown() method explicitly when outside of the loop. However, I don't still fully know websockets and this library, so I might be completely wrong and I would be happy for any feedback and possible problems with my solutions I cannot foresee.

@engn33r
Copy link
Collaborator

engn33r commented Apr 18, 2023

I will reopen this issue because it may not be solved completely based on these reports.

@AdamMiltonBarker
Copy link

I also have issues with this, on_close is never called no matter what.

@QuinnDamerell
Copy link
Contributor

FYI for everyone here. I might have fixed this bug while actually hunting another issue, as you can see in the PR above. I would love to hear if this bug is resokved in 1.6.0!

@engn33r engn33r pinned this issue Jun 17, 2023
@wormsik
Copy link

wormsik commented Jun 18, 2023

Thank you very much @QuinnDamerell and @engn33r ! I plan to test your fix in the following weeks and I'll let you know.

@guessrrRUS
Copy link

guessrrRUS commented Jun 23, 2023

Thank you very much for fixing it @QuinnDamerell !
I had this issue in my code without knowing why the on_close() doesn't set an important event. It drove me almost nuts, since I hadn't found anything relevant when I first searched for the problem back almost a month ago. Recently tried again, found this and now my code works properly! Big thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests