@@ -29,6 +29,12 @@ import qualified Data.Aeson as Aeson
29
29
import qualified IHP.Log.Types as Log
30
30
import qualified IHP.Log as Log
31
31
32
+ import Control.Concurrent.Chan
33
+ import Control.Concurrent
34
+ import System.Timeout
35
+ import Data.Function (fix )
36
+ import qualified Network.WebSockets.Connection as WebSocket
37
+
32
38
class WSApp state where
33
39
initialState :: state
34
40
@@ -47,7 +53,15 @@ startWSApp connection = do
47
53
let ? state = state
48
54
let ? connection = connection
49
55
50
- result <- Exception. try ((Websocket. withPingThread connection 30 (onPing @ state ) (run @ state )) `Exception.finally` onClose @ state )
56
+ let runWithPongChan pongChan = do
57
+ let connectionOnPong = writeChan pongChan ()
58
+ let ? connection = connection
59
+ { WebSocket. connectionOptions = (get # connectionOptions connection) { WebSocket. connectionOnPong }
60
+ }
61
+ in
62
+ run @ state
63
+
64
+ result <- Exception. try ((withPinger connection runWithPongChan) `Exception.finally` onClose @ state )
51
65
case result of
52
66
Left (e@ Exception. SomeException {}) ->
53
67
case Exception. fromException e of
@@ -90,3 +104,47 @@ instance Websocket.WebSocketsData UUID where
90
104
fromDataMessage (Websocket. Binary byteString) = UUID. fromLazyASCIIBytes byteString |> Maybe. fromJust
91
105
fromLazyByteString byteString = UUID. fromLazyASCIIBytes byteString |> Maybe. fromJust
92
106
toLazyByteString = UUID. toLazyASCIIBytes
107
+
108
+ data PongTimeout
109
+ = PongTimeout
110
+ deriving (Show )
111
+
112
+ instance Exception PongTimeout
113
+
114
+ pingWaitTime :: Int
115
+ pingWaitTime = 30
116
+
117
+
118
+ -- | Pings the client every 30 seconds and expects a pong response within 10 secons. If no pong response
119
+ -- is received within 10 seconds, it will kill the connection.
120
+ --
121
+ -- We cannot use the withPingThread of the websockets package as this doesn't deal with pong messages. So
122
+ -- open connection will stay around forever.
123
+ --
124
+ -- This implementation is based on https://github.com/jaspervdj/websockets/issues/159#issuecomment-552776502
125
+ withPinger conn action = do
126
+ pongChan <- newChan
127
+ mainAsync <- async $ action pongChan
128
+ pingerAsync <- async $ runPinger conn pongChan
129
+
130
+ waitEitherCatch mainAsync pingerAsync >>= \ case
131
+ -- If the application async died for any reason, kill the pinger async
132
+ Left result -> do
133
+ cancel pingerAsync
134
+ case result of
135
+ Left exception -> throw exception
136
+ Right result -> pure ()
137
+ -- The pinger thread should never throw an exception. If it does, kill the app thread
138
+ Right (Left exception) -> do
139
+ cancel mainAsync
140
+ throw exception
141
+ -- The pinger thread exited due to a pong timeout. Tell the app thread about it.
142
+ Right (Right () ) -> cancelWith mainAsync PongTimeout
143
+
144
+ runPinger conn pongChan = fix $ \ loop -> do
145
+ Websocket. sendPing conn (mempty :: ByteString )
146
+ threadDelay pingWaitTime
147
+ -- See if we got a pong in that time
148
+ timeout 1000000 (readChan pongChan) >>= \ case
149
+ Just () -> loop
150
+ Nothing -> return ()
0 commit comments