@@ -45,8 +45,9 @@ class Session{
4545
4646 public const STATE_CONNECTING = 0 ;
4747 public const STATE_CONNECTED = 1 ;
48- public const STATE_DISCONNECTING = 2 ;
49- public const STATE_DISCONNECTED = 3 ;
48+ public const STATE_DISCONNECT_PENDING = 2 ;
49+ public const STATE_DISCONNECT_NOTIFIED = 3 ;
50+ public const STATE_DISCONNECTED = 4 ;
5051
5152 public const MIN_MTU_SIZE = 400 ;
5253
@@ -149,7 +150,10 @@ public function isTemporal() : bool{
149150 }
150151
151152 public function isConnected () : bool {
152- return $ this ->state !== self ::STATE_DISCONNECTING and $ this ->state !== self ::STATE_DISCONNECTED ;
153+ return
154+ $ this ->state !== self ::STATE_DISCONNECT_PENDING and
155+ $ this ->state !== self ::STATE_DISCONNECT_NOTIFIED and
156+ $ this ->state !== self ::STATE_DISCONNECTED ;
153157 }
154158
155159 public function update (float $ time ) : void {
@@ -159,12 +163,18 @@ public function update(float $time) : void{
159163 return ;
160164 }
161165
162- if ($ this ->state === self ::STATE_DISCONNECTING ){
166+ if ($ this ->state === self ::STATE_DISCONNECT_PENDING || $ this -> state === self :: STATE_DISCONNECT_NOTIFIED ){
163167 //by this point we already told the event listener that the session is closing, so we don't need to do it again
164168 if (!$ this ->sendLayer ->needsUpdate () and !$ this ->recvLayer ->needsUpdate ()){
165- $ this ->state = self ::STATE_DISCONNECTED ;
166- $ this ->logger ->debug ("Client cleanly disconnected, marking session for destruction " );
167- return ;
169+ if ($ this ->state === self ::STATE_DISCONNECT_PENDING ){
170+ $ this ->queueConnectedPacket (new DisconnectionNotification (), PacketReliability::RELIABLE_ORDERED , 0 , true );
171+ $ this ->state = self ::STATE_DISCONNECT_NOTIFIED ;
172+ $ this ->logger ->debug ("All pending traffic flushed, sent disconnect notification " );
173+ }else {
174+ $ this ->state = self ::STATE_DISCONNECTED ;
175+ $ this ->logger ->debug ("Client cleanly disconnected, marking session for destruction " );
176+ return ;
177+ }
168178 }elseif ($ this ->disconnectionTime + 10 < $ time ){
169179 $ this ->state = self ::STATE_DISCONNECTED ;
170180 $ this ->logger ->debug ("Timeout during graceful disconnect, forcibly closing session " );
@@ -238,7 +248,7 @@ private function handleEncapsulatedPacketRoute(EncapsulatedPacket $packet) : voi
238248 }
239249 }
240250 }elseif ($ id === DisconnectionNotification::$ ID ){
241- $ this ->initiateDisconnect ( " client disconnect " );
251+ $ this ->onClientDisconnect ( );
242252 }elseif ($ id === ConnectedPing::$ ID ){
243253 $ dataPacket = new ConnectedPing ();
244254 $ dataPacket ->decode (new PacketSerializer ($ packet ->buffer ));
@@ -290,9 +300,8 @@ public function handlePacket(Packet $packet) : void{
290300 */
291301 public function initiateDisconnect (string $ reason ) : void {
292302 if ($ this ->isConnected ()){
293- $ this ->state = self ::STATE_DISCONNECTING ;
303+ $ this ->state = self ::STATE_DISCONNECT_PENDING ;
294304 $ this ->disconnectionTime = microtime (true );
295- $ this ->queueConnectedPacket (new DisconnectionNotification (), PacketReliability::RELIABLE_ORDERED , 0 , true );
296305 $ this ->server ->getEventListener ()->onClientDisconnect ($ this ->internalId , $ reason );
297306 $ this ->logger ->debug ("Requesting graceful disconnect because \"$ reason \"" );
298307 }
@@ -307,6 +316,20 @@ public function forciblyDisconnect(string $reason) : void{
307316 $ this ->logger ->debug ("Forcibly disconnecting session due to \"$ reason \"" );
308317 }
309318
319+ private function onClientDisconnect () : void {
320+ //the client will expect an ACK for this; make sure it gets sent, because after forcible termination
321+ //there won't be any session ticks to update it
322+ $ this ->recvLayer ->update ();
323+
324+ if ($ this ->isConnected ()){
325+ //the client might have disconnected after the server sent a disconnect notification, but before the client
326+ //received it - in this case, we don't want to notify the event handler twice
327+ $ this ->server ->getEventListener ()->onClientDisconnect ($ this ->internalId , "client disconnect " );
328+ }
329+ $ this ->state = self ::STATE_DISCONNECTED ;
330+ $ this ->logger ->debug ("Terminating session due to client disconnect " );
331+ }
332+
310333 /**
311334 * Returns whether the session is ready to be destroyed (either properly cleaned up or forcibly terminated)
312335 */
0 commit comments