Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix qgis server fcgi response destructor #59632

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 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
6 changes: 4 additions & 2 deletions scripts/cppcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ case $SCRIPT_DIR in
;;
esac

SRC_DIR=${1:-../src}

LOG_FILE=/tmp/cppcheck_qgis.txt

rm -f ${LOG_FILE}
echo "Checking ${SCRIPT_DIR}/../src ..."
echo "Checking ${SCRIPT_DIR}/${SRC_DIR} ..."

# qgsgcptransformer.cpp causes an effective hang on newer cppcheck!

Expand Down Expand Up @@ -51,7 +53,7 @@ cppcheck --library=qt.cfg --inline-suppr \
-DBUILTIN_UNREACHABLE="__builtin_unreachable();" \
-i src/analysis/georeferencing/qgsgcptransformer.cpp \
-j $(nproc) \
${SCRIPT_DIR}/../src \
${SCRIPT_DIR}/${SRC_DIR} \
>>${LOG_FILE} 2>&1 &

PID=$!
Expand Down
104 changes: 80 additions & 24 deletions src/server/qgsfcgiserverresponse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@

#include "qgslogger.h"

#include <mutex>
#include <chrono>

#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

Expand Down Expand Up @@ -54,39 +58,52 @@ typedef struct QgsFCGXStreamData
} QgsFCGXStreamData;
#endif

QgsSocketMonitoringThread::QgsSocketMonitoringThread( bool *isResponseFinished, QgsFeedback *feedback )
: mIsResponseFinished( isResponseFinished )
, mFeedback( feedback )
// to be able to use 333ms expression as a duration
using namespace std::chrono_literals;

// used to synchronize socket monitoring thread and fcgi response
std::timed_mutex gSocketMonitoringMutex;
benoitdm-oslandia marked this conversation as resolved.
Show resolved Hide resolved


// QgsSocketMonitoringThread constructor
QgsSocketMonitoringThread::QgsSocketMonitoringThread( std::shared_ptr<QgsFeedback> feedback )
: mFeedback( feedback )
, mIpcFd( -1 )
{
setObjectName( "FCGI socket monitor" );
Q_ASSERT( mIsResponseFinished );
Q_ASSERT( mFeedback );

mIsResponseFinished.store( false );

#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
if ( FCGI_stdout && FCGI_stdout->fcgx_stream && FCGI_stdout->fcgx_stream->data )
{
QgsFCGXStreamData *stream = static_cast<QgsFCGXStreamData *>( FCGI_stdin->fcgx_stream->data );
QgsFCGXStreamData *stream = static_cast<QgsFCGXStreamData *>( FCGI_stdout->fcgx_stream->data );
if ( stream && stream->reqDataPtr )
{
mIpcFd = stream->reqDataPtr->ipcFd;
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "FCGI_stdin stream data is null! Socket monitoring disable." ),
QgsMessageLog::logMessage( QStringLiteral( "FCGI_stdout stream data is null! Socket monitoring disabled." ),
QStringLiteral( "FCGIServer" ),
Qgis::MessageLevel::Warning );
}
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "FCGI_stdin is null! Socket monitoring disable." ),
QgsMessageLog::logMessage( QStringLiteral( "FCGI_stdout is null! Socket monitoring disabled." ),
QStringLiteral( "FCGIServer" ),
Qgis::MessageLevel::Warning );
}
#endif
}

// Informs the thread to quit
void QgsSocketMonitoringThread::setResponseFinished( bool responseFinished )
{
mIsResponseFinished.store( responseFinished );
}

void QgsSocketMonitoringThread::run( )
{
if ( mIpcFd < 0 )
Expand All @@ -98,33 +115,59 @@ void QgsSocketMonitoringThread::run( )
}

#if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
#ifdef QGISDEBUG
const pid_t threadId = gettid();
#endif

mIsResponseFinished.store( false );
char c;
while ( !*mIsResponseFinished )
while ( !mIsResponseFinished.load() )
{
const ssize_t x = recv( mIpcFd, &c, 1, MSG_PEEK | MSG_DONTWAIT ); // see https://stackoverflow.com/a/12402596
if ( x < 0 )
if ( x != 0 )
{
// Ie. we are still connected but we have an 'error' as there is nothing to read
QgsDebugMsgLevel( QStringLiteral( "FCGIServer: remote socket still connected. errno: %1" ).arg( errno ), 5 );
QgsDebugMsgLevel( QStringLiteral( "FCGIServer %1: remote socket still connected. errno: %2, x: %3" )
.arg( threadId ).arg( errno ).arg( x ), 5 );
}
else
{
// socket closed, nothing can be read
QgsDebugMsgLevel( QStringLiteral( "FCGIServer: remote socket has been closed! errno: %1" ).arg( errno ), 2 );
mFeedback->cancel();
break;
// double check...
ssize_t x2 = 0;
for ( int i = 0; !mIsResponseFinished.load() && x2 == 0 && i < 10; i++ )
{
x2 = recv( mIpcFd, &c, 1, MSG_PEEK | MSG_DONTWAIT ); // see https://stackoverflow.com/a/12402596
std::this_thread::sleep_for( 50ms );
Copy link
Contributor

Choose a reason for hiding this comment

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

Why waiting ? Is it require for synchronization?

}
if ( x2 == 0 )
{
// socket closed, nothing can be read
QgsDebugMsgLevel( QStringLiteral( "FCGIServer %1: remote socket has been closed! errno: %2, x: %3" )
.arg( threadId ).arg( errno ).arg( x2 ), 2 );
mFeedback->cancel();
break;
}
else
{
QgsDebugMsgLevel( QStringLiteral( "FCGIServer::run %1: remote socket is not closed in fact! errno: %2, x: %3" )
.arg( threadId ).arg( errno ).arg( x2 ), 1 );
}

}

QThread::msleep( 333L );
// If lock is acquired this means the response has finished and we will exit the while loop
// else we will wait max for 333ms.
if ( gSocketMonitoringMutex.try_lock_for( 333ms ) )
gSocketMonitoringMutex.unlock();
}

if ( *mIsResponseFinished )
if ( mIsResponseFinished.load() )
{
QgsDebugMsgLevel( QStringLiteral( "FCGIServer: socket monitoring quits normally." ), 2 );
QgsDebugMsgLevel( QStringLiteral( "FCGIServer::run %1: socket monitoring quits normally." ).arg( threadId ), 2 );
}
else
{
QgsDebugMsgLevel( QStringLiteral( "FCGIServer: socket monitoring quits: no more socket." ), 2 );
QgsDebugMsgLevel( QStringLiteral( "FCGIServer::run %1: socket monitoring quits: no more socket." ).arg( threadId ), 2 );
}
#endif
}
Expand All @@ -141,15 +184,28 @@ QgsFcgiServerResponse::QgsFcgiServerResponse( QgsServerRequest::Method method )
mBuffer.open( QIODevice::ReadWrite );
setDefaultHeaders();

mSocketMonitoringThread = std::make_unique<QgsSocketMonitoringThread>( &mFinished, mFeedback.get() );
mSocketMonitoringThread->start();
mSocketMonitoringThread = std::make_unique<QgsSocketMonitoringThread>( mFeedback );

// Lock the thread mutex: every try_lock will take 333ms
gSocketMonitoringMutex.lock();

// Start the monitoring thread
mThread = std::thread( &QgsSocketMonitoringThread::run, mSocketMonitoringThread.get() );
}

QgsFcgiServerResponse::~QgsFcgiServerResponse()
{
mFinished = true;
mSocketMonitoringThread->exit();
mSocketMonitoringThread->wait();

// Inform the thread to quit asap
mSocketMonitoringThread->setResponseFinished( mFinished );

// Release the mutex so the try_lock in the thread will not wait anymore and
// the thread will end its loop as we have set 'setResponseFinished' to true
gSocketMonitoringMutex.unlock();

// Just to be sure
mThread.join();
}

void QgsFcgiServerResponse::removeHeader( const QString &key )
Expand Down Expand Up @@ -296,5 +352,5 @@ void QgsFcgiServerResponse::truncate()

void QgsFcgiServerResponse::setDefaultHeaders()
{
setHeader( QStringLiteral( "Server" ), QStringLiteral( " QGIS FCGI server - QGIS version %1" ).arg( Qgis::version() ) );
mHeaders.insert( QStringLiteral( "Server" ), QStringLiteral( " QGIS FCGI server - QGIS version %1" ).arg( Qgis::version() ) );
}
38 changes: 25 additions & 13 deletions src/server/qgsfcgiserverresponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,38 @@
#include "qgsserverresponse.h"

#include <QBuffer>
#include <QThread>
#include <thread>

/**
* \ingroup server
* \class QgsSocketMonitoringThread
* \brief Thread used to monitor the fcgi socket
* \since QGIS 3.36
*/
class QgsSocketMonitoringThread: public QThread
class QgsSocketMonitoringThread
{
Q_OBJECT

public:

/**
* \brief QgsSocketMonitoringThread
* \param isResponseFinished
* \param feedback
* Constructor for QgsSocketMonitoringThread
* \param feedback used to cancel rendering jobs when socket timedout
*/
QgsSocketMonitoringThread( std::shared_ptr<QgsFeedback> feedback );

/**
* main thread function
*/
QgsSocketMonitoringThread( bool *isResponseFinished, QgsFeedback *feedback );
void run( );
void run();

/**
* Controls thread life
* \param responseFinished stop the thread if true
*/
void setResponseFinished( bool responseFinished );

private:
bool *mIsResponseFinished = nullptr;
QgsFeedback *mFeedback = nullptr;
std::atomic_bool mIsResponseFinished;
std::shared_ptr<QgsFeedback> mFeedback;
int mIpcFd = -1;
};

Expand All @@ -68,7 +75,8 @@ class SERVER_EXPORT QgsFcgiServerResponse: public QgsServerResponse
* \param method The HTTP method (Get by default)
*/
QgsFcgiServerResponse( QgsServerRequest::Method method = QgsServerRequest::GetMethod );
virtual ~QgsFcgiServerResponse();

virtual ~QgsFcgiServerResponse() override;

void setHeader( const QString &key, const QString &value ) override;

Expand Down Expand Up @@ -117,8 +125,12 @@ class SERVER_EXPORT QgsFcgiServerResponse: public QgsServerResponse
QgsServerRequest::Method mMethod;
int mStatusCode = 0;

// encapsulate thread data
std::unique_ptr<QgsSocketMonitoringThread> mSocketMonitoringThread;
std::unique_ptr<QgsFeedback> mFeedback;
// real thread object. Used to join.
std::thread mThread;
// Used to cancel rendering jobs
std::shared_ptr<QgsFeedback> mFeedback;
};

#endif
Loading