Repository: tornadoweb/tornado

Issue:
Periodic pings shouldn't be sent after socket close
Hi, I'm using tornado websockets with periodic pings (https://github.com/tornadoweb/tornado/issues/1640).
When sending large amount of data before closing the socket from server-side, I noticed the error on client side in Chrome: _Ping received after close_.
As far as I understood, the error happens, when client receives the frames in the following order:
> data1, data2, ..., dataN, close, ping

The problem here is that periodic ping can be sent even after calling `socket.close()`: this method is only about sending a close frame. However periodic pings check real connection state, which will be closed only after client confirms the socket close frame.
I guess, periodic ping should be stopped immediately on `socket.close()` or at least check for socket state, instead of real connection.
As I local workaround I added a call `web_socket.ws_connection.connection.ping_callback.stop()` before close and don't get this error anymore

Attached is a sample tornado server with "overloaded" websocket and an html page for connecting to the socket. This can be not 100% reproducible, depending on ping occurences.
[ping_bug.zip](https://github.com/tornadoweb/tornado/files/2207374/ping_bug.zip)

Tornado version: 5, but I believe it applies to other versions as well


Discussion:

Resolving patch:
diff --git a/tornado/websocket.py b/tornado/websocket.py
--- a/tornado/websocket.py
+++ b/tornado/websocket.py
@@ -248,7 +248,7 @@ def write_message(self, message, binary=False):
            Consistently raises `WebSocketClosedError`. Previously could
            sometimes raise `.StreamClosedError`.
         """
-        if self.ws_connection is None:
+        if self.ws_connection is None or self.ws_connection.is_closing():
             raise WebSocketClosedError()
         if isinstance(message, dict):
             message = tornado.escape.json_encode(message)
@@ -355,7 +355,7 @@ def ping(self, data=b''):
 
         """
         data = utf8(data)
-        if self.ws_connection is None:
+        if self.ws_connection is None or self.ws_connection.is_closing():
             raise WebSocketClosedError()
         self.ws_connection.write_ping(data)
 
@@ -1046,6 +1046,17 @@ def close(self, code=None, reason=None):
             self._waiting = self.stream.io_loop.add_timeout(
                 self.stream.io_loop.time() + 5, self._abort)
 
+    def is_closing(self):
+        """Return true if this connection is closing.
+
+        The connection is considered closing if either side has
+        initiated its closing handshake or if the stream has been
+        shut down uncleanly.
+        """
+        return (self.stream.closed() or
+                self.client_terminated or
+                self.server_terminated)
+
     @property
     def ping_interval(self):
         interval = self.handler.ping_interval
@@ -1073,7 +1084,7 @@ def periodic_ping(self):
 
         Called periodically if the websocket_ping_interval is set and non-zero.
         """
-        if self.stream.closed() and self.ping_callback is not None:
+        if self.is_closing() and self.ping_callback is not None:
             self.ping_callback.stop()
             return
 


Test patch: