Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/controller/c/g_image.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBIW_WINDOWS_UTF8
#include <stb_image.h>
#include <stb_image_write.h>
#include "stb_image_write.h"

#include <webots/types.h>

Expand Down
23 changes: 18 additions & 5 deletions src/webots/gui/WbTcpServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,24 +130,37 @@ void WbTcpServer::create(int port) {
// - a websocket on "/"
// - texture images on the other urls. e.g. "/textures/dir/image.[jpg|png|hdr]"

// See if a server is already running on port by trying to connect to it.
// This is needed because in some environments QTcpServer::listen() uses a socket that is configured to reuse a port [1]
// and Qt does not provide a way to configure the socket before calling listen() [2].
// [1] https://doc.qt.io/qt-6/qabstractsocket.html#BindFlag-enum
// [2] https://stackoverflow.com/questions/47268023/how-to-set-so-reuseaddr-on-the-socket-used-by-qtcpserver
// =====================================================
// TEMPORARY PATCH FOR macOS 27
// -----------------------------------------------------
// Webots R2025a performs a pre-check by connecting to
// localhost:port and assumes the port is already in use
// if the connection succeeds.
//
// On macOS 27 this check appears to incorrectly report
// every port as occupied.
//
// We disable the pre-check and rely on the real
// QTcpServer::listen() call below.
// =====================================================

/*
QTcpSocket socket;
socket.connectToHost("localhost", port);
if (socket.waitForConnected(100)) {
socket.disconnectFromHost();
throw tr("Port %1 is already in use").arg(port);
}
*/
Comment on lines +147 to +154

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix this in a different way. The test should be performed on other platforms than macOS. Also, on macOS, we should find another way to check if the port is already in use.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review.

I agree that simply disabling the check is probably not the best long-term solution and that a platform-specific or more robust implementation would be preferable.

My intention with this patch was mainly to identify the source of the issue and verify that the failure originates from the current localhost connection test.

On macOS 27 Beta, commenting out this pre-check consistently allows Webots to start and operate normally, while the original code reports every tested port as already in use, even when the ports are confirmed to be free.

From my testing, this suggests that the problem is likely located in the current localhost probe rather than in QTcpServer::listen() itself.

I completely agree that the final fix should preserve the port availability check and remain compatible with other platforms.

In the meantime, this workaround may still be useful for affected users running macOS 27 Beta, as it provides a temporary way to bypass what is currently a startup-blocking issue until a more complete solution is implemented.

If it would be useful, I can perform additional tests on macOS 27 Beta and help evaluate alternative approaches for checking port availability on macOS.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a platform-specific approach be acceptable?

For example, we could keep the existing localhost pre-check on non-macOS platforms, but skip it on macOS and rely directly on QTcpServer::listen() there. This would preserve the current behavior on Linux/Windows while avoiding the false-positive localhost connection behavior observed on macOS 27 Beta.

Conceptually:

#ifndef APPLE
QTcpSocket socket;
socket.connectToHost("localhost", port);
if (socket.waitForConnected(100)) {
socket.disconnectFromHost();
throw tr("Port %1 is already in use").arg(port);
}
#endif

mTcpServer = new QTcpServer();
if (!mTcpServer->listen(QHostAddress::Any, port))
throw tr("Cannot set the server in listen mode: %1").arg(mTcpServer->errorString());

This would make the workaround macOS-specific instead of removing the pre-check globally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was investigating this, and I'm now wondering if the check is even necessary. The comment references QAbstractSocket::BindMode. Notably the only flag that that claims to set is SO_REUSEADDR, which looks like it still blocks connections on ports with active servers. The real worry is setting SO_REUSEPORT, which would allow that behavior. So, just going by that, we should be fine. Nevertheless, I had some doubts, so here's my in-depth analysis:

  • The default bind behavior only sets the AddressReusable socket engine option on UNIX [1]
  • Same goes for QTcpServer [2]
  • This option typically only corresponds to SO_REUSEADDR [3] (called here)
  • On Mac, this can sometimes set SO_REUSEPORT, but only on UDP, which doesn't matter here [4]

On Windows, there are SO_REUSEADDR and SO_EXCLUSIVEADDRUSE. If the former is set, we can get multiple servers on the same socket. If the latter is set (admin required), it prevents use of the former. [5]

  • The default bind behavior can set AddressReusable and/or BindExclusively based on the flags. [6]
  • These correspond to the two windows flags [7] (called here)
  • QTcpServer sets neither explicitly, so if we run by the BindMode defaults (see first link), we'd expect ShareAddress, which corresponds to both flags being unset. (see [6])

All of that is to say, just looking at the code, I don't think Qt sets any options which would cause the behavior this check is trying to prevent, so we could try starting the TCP server first and checking the error code. If it fails with an in-use conflict, return the "in use" error. Otherwise, fall back to the generic version. Of course, we should probably check that behavior before merging it in.

Also, is it just me, or does create have a memory leak on the second throw whereby mWebSocketServer and mTcpServer are never deleted? start() looks like it just recursively calls create on the same WbTcpServer object, so the destructor is never called to delete the intermittent members.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I am still curious as to why the current behavior is failing on mac though 👀 )

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that makes sense. My original intention was mainly to identify the source of the startup failure and provide a temporary workaround. Using the result of QTcpServer::listen() as the source of truth seems cleaner than relying on a localhost connection probe. From my testing on macOS 27 Beta, removing the pre-check consistently restores normal startup behavior, which would also be consistent with the idea that the check itself may no longer be necessary.

@omichel omichel Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is it just me, or does create have a memory leak on the second throw whereby mWebSocketServer and mTcpServer are never deleted? start() looks like it just recursively calls create on the same WbTcpServer object, so the destructor is never called to delete the intermittent members.

Yes, I believe you are right. The minimal fix is to clean up before throwing (and ideally parent mTcpServer too for symmetry):

mWebSocketServer = new QWebSocketServer("Webots Streaming Server", QWebSocketServer::NonSecureMode, this);
mTcpServer = new QTcpServer(this);
if (!mTcpServer->listen(QHostAddress::Any, port)) {
  const QString error = mTcpServer->errorString();
  delete mTcpServer;       mTcpServer = NULL;
  delete mWebSocketServer; mWebSocketServer = NULL;
  throw tr("Cannot set the server in listen mode: %1").arg(error);
}

This also keeps isActive() (mWebSocketServer != NULL) honest after a failed attempt.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense.

I hadn't noticed the cleanup issue, but I agree that deleting the server objects before throwing would avoid leaving the object in a partially initialized state and would keep isActive() consistent after a failed startup attempt.

It also seems compatible with using QTcpServer::listen() as the primary source of truth for port availability.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to update the PR in this direction?

I can replace the current workaround with a cleaner implementation that removes the localhost pre-check, relies on QTcpServer::listen() as the source of truth, and cleans up mTcpServer / mWebSocketServer before throwing if listen() fails.

I’m actually an MSc student rather than a regular contributor to the Webots codebase, so I may not be fully familiar with all the design considerations here. If this direction is consistent with the intended architecture, I’d be happy to implement and test it on macOS 27 Beta lol

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; that would be awesome! Please go-ahead make those changes!


// Reference to let live QTcpSocket and QWebSocketServer on the same port using `QWebSocketServer::handleConnection()`:
// - https://bugreports.qt.io/browse/QTBUG-54276
mWebSocketServer = new QWebSocketServer("Webots Streaming Server", QWebSocketServer::NonSecureMode, this);
mTcpServer = new QTcpServer();

if (!mTcpServer->listen(QHostAddress::Any, port))
throw tr("Cannot set the server in listen mode: %1").arg(mTcpServer->errorString());

connect(mWebSocketServer, &QWebSocketServer::newConnection, this, &WbTcpServer::onNewWebSocketConnection);
connect(mTcpServer, &QTcpServer::newConnection, this, &WbTcpServer::onNewTcpConnection);
connect(WbSimulationState::instance(), &WbSimulationState::controllerReadRequestsCompleted, this,
Expand Down
Loading