From fba4dcbd52cf4dadf418950111b0f8d95aeec960 Mon Sep 17 00:00:00 2001 From: Matteo Nastasi Date: Sun, 13 Sep 2026 12:39:10 +0200 Subject: [PATCH] the displaced stream was closed without a word When a new read stream arrives for a session that already has one, the daemon replaces the old with the new: that is the intended behaviour, it is what lets you carry on from where you were after a browser restart. The displaced client, though, was told nothing: its socket was simply closed. It reopened it, displacing the new one in turn, which reopened in turn: two windows open on the same session bounced each other forever, each with a red indicator and no explanation. Now the replaced stream is dismissed: it stops the streaming and shows a notice. It is not sent back to the login, because when the two windows share the same session the entry page would let it straight back in, starting the bouncing again. The message travels over the transport of the OLD connection, which may differ from the one of the new, and is followed by an orderly close (stream_bye in user.phh). In the normal case - the same client reopening after having given the connection up for lost - it ends up on a socket nobody reads any more and does no harm. Checked with two streams on the same session, in both combinations: the first receives xstm.stop() and the notice, as text over xhr and framed (0x81) over websocket, and is not sent back to the login. A complete game confirms the normal path is intact. NOTE: the case of a login by a user already present was handled before, by the "ghost" mechanism of add_user(): the new access inherits the old user, seat at the table included, and the displaced session is recorded in ghost_sess with reason ANOT, which index.php turns into "La tua sessione e' stata assegnata ad un altro browser". What was missing is precisely the piece added here: if the displaced client was reading a stream, it never reached index.php and never saw that message. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014M1jiEq9cHdE5SE5j6vFuE --- web/Obj/brisk.phh | 23 +++++++++++++++++++++-- web/Obj/user.phh | 12 ++++++++++++ web/briskin5/Obj/briskin5.phh | 13 +++++++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/web/Obj/brisk.phh b/web/Obj/brisk.phh index b556210..71ed615 100644 --- a/web/Obj/brisk.phh +++ b/web/Obj/brisk.phh @@ -119,6 +119,8 @@ $mlang_brisk = array( 'btn_backstand'=> array( 'it' => 'torna in piedi', 'en' => '
You are being idle for ' ), 'tabtout_b'=> array( 'it' => ' minuti.

Quindi ritorni tra i Giocatori in piedi.', 'en' => ' minutes.

Then you return with the standing players.'), + 'sessmoved' => array( 'it' => '
Questa sessione è stata ripresa da un'altra finestra o da un altro dispositivo.
Questa pagina non è più collegata.

', + 'en' => '
This session has been resumed from another window or device.
This page is no longer connected.

'), 'tickmust' => array( 'it' => '
Per attivare il messaggio di segnalazione del tavolo occorre essere seduti.

', 'en' => '
To activate the signalling message of the table it\'s necessary to be sitting

'), 'tickjust' => array( 'it' => '
Il messaggio di segnalazione del tavolo è già attivato.

', @@ -2745,6 +2747,7 @@ class Brisk function request_mgr(&$s_a_p, $header, &$header_out, &$new_socket, $path, $addr, $get, $post, $cookie) { GLOBAL $G_ban_list, $G_black_list, $G_cloud_smasher; + GLOBAL $mlang_brisk, $G_lang; // printf("NEW_SOCKET (root): %d PATH [%s]\n", intval($new_socket), $path); @@ -2825,8 +2828,24 @@ class Brisk $this->sess_cur_set($user->sess); // close a previous opened index_read_ifra socket, if exists if (($prev = $user->rd_socket_get()) != NULL) { - $s_a_p->socks_unset($user->rd_socket_get()); - fclose($user->rd_socket_get()); + /* The previous stream is being replaced by this one. Up to + now its socket was closed without a word, and the client on + the other side reopened it, displacing the new one in turn: + two windows open on the same session bounced each other + forever, with a red indicator and no explanation. Now it is + dismissed: it stops the stream and shows a notice. It is + not sent back to the login, because the session is valid + and it would come back in, starting the bouncing again. + In the normal case - the same client reopening after having + given the connection up for lost - the message ends up on a + socket nobody reads any more, and does no harm. */ + $bye = $user->stream_bye('xstm.stop(); ' + .show_notify($mlang_brisk['sessmoved'][$G_lang], 0, "chiudi", 400, 120)); + if ($bye != "") { + @fwrite($prev, $bye, mb_strlen($bye, "ASCII")); + } + $s_a_p->socks_unset($prev); + fclose($prev); // printf("CLOSE AND OPEN AGAIN ON IFRA2\n"); $user->rd_socket_set(NULL); } diff --git a/web/Obj/user.phh b/web/Obj/user.phh index 4c13325..71f682b 100644 --- a/web/Obj/user.phh +++ b/web/Obj/user.phh @@ -894,6 +894,18 @@ function stream_keepalive($with_ping) return ($this->rd_transp->chunk( $this->rd_scristp++, ($with_ping ? "act_ping();" : NULL))); } +/* Farewell for the stream about to be replaced by another one: the message + is framed according to the transport of the OLD connection, which may differ + from the one of the new, and is followed by an orderly close. If the old + connection is already dead the write fails with no consequence. */ +function stream_bye($js) +{ + if ($this->rd_transp == NULL) { + return (""); + } + return ($this->chunked_content($this->rd_transp->chunk(0, $js)).$this->stream_close()); +} + function stream_close() { $clo = $this->rd_transp->close(); diff --git a/web/briskin5/Obj/briskin5.phh b/web/briskin5/Obj/briskin5.phh index 86bd77d..190c88d 100644 --- a/web/briskin5/Obj/briskin5.phh +++ b/web/briskin5/Obj/briskin5.phh @@ -1463,6 +1463,7 @@ class Bin5 { static function request_mgr(&$s_a_p, $header, &$header_out, &$new_socket, $path, $addr, $get, $post, $cookie) { GLOBAL $G_ban_list, $G_black_list; + GLOBAL $mlang_brisk, $G_lang; // printf("NEW_SOCKET (root): %d\n", intval($new_socket)); @@ -1548,8 +1549,16 @@ class Bin5 { // close a previous opened index_read_ifra socket, if exists if (($prev = $user->rd_socket_get()) != NULL) { - $s_a_p->socks_unset($user->rd_socket_get()); - fclose($user->rd_socket_get()); + /* as in the room: the stream being replaced is told what + happened to it, otherwise it reopens and displaces the + new one in turn, forever. */ + $bye = $user->stream_bye('xstm.stop(); ' + .show_notify($mlang_brisk['sessmoved'][$G_lang], 0, "chiudi", 400, 120)); + if ($bye != "") { + @fwrite($prev, $bye, mb_strlen($bye, "ASCII")); + } + $s_a_p->socks_unset($prev); + fclose($prev); // printf("CLOSE AND OPEN AGAIN ON IFRA2\n"); $user->rd_socket_set(NULL); } -- 2.47.3