28#include <condition_variable>
34using namespace Qt::StringLiterals;
47#include <QFontDatabase>
51#include <QNetworkInterface>
52#include <QCommandLineParser>
65QAtomicInt IS_RUNNING = 1;
70std::condition_variable REQUEST_WAIT_CONDITION;
71std::mutex REQUEST_QUEUE_MUTEX;
72std::mutex SERVER_MUTEX;
76 QPointer<QTcpSocket> clientConnection;
78 std::chrono::steady_clock::time_point startTime;
79 QgsBufferServerRequest request;
80 QgsBufferServerResponse response;
84QQueue<RequestContext *> REQUEST_QUEUE;
86const QMap<int, QString> knownStatuses {
88 { 201, u
"Created"_s },
89 { 202, u
"Accepted"_s },
90 { 204, u
"No Content"_s },
91 { 301, u
"Moved Permanently"_s },
92 { 302, u
"Moved Temporarily"_s },
93 { 304, u
"Not Modified"_s },
94 { 400, u
"Bad Request"_s },
95 { 401, u
"Unauthorized"_s },
96 { 403, u
"Forbidden"_s },
97 { 404, u
"Not Found"_s },
98 { 500, u
"Internal Server Error"_s },
99 { 501, u
"Not Implemented"_s },
100 { 502, u
"Bad Gateway"_s },
101 { 503, u
"Service Unavailable"_s }
107class HttpException :
public std::exception
113 HttpException(
const QString &message )
114 : mMessage( message )
120 QString message()
const {
return mMessage; }
127class TcpServerWorker :
public QObject
132 TcpServerWorker(
const QString &ipAddress,
int port )
134 QHostAddress address { QHostAddress::AnyIPv4 };
135 address.setAddress( ipAddress );
137 if ( !mTcpServer.listen( address, port ) )
139 std::cerr << tr(
"Unable to start the server: %1." ).arg( mTcpServer.errorString() ).toStdString() << std::endl;
143 const int port { mTcpServer.serverPort() };
145 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
147 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
153 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection,
this, [
this, ipAddress, port] {
154 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
156 mConnectionCounter++;
160 QString *incomingData =
new QString();
163 QObject *context {
new QObject };
166 auto connectionDeleter = [
this, clientConnection, incomingData]() {
167 clientConnection->deleteLater();
168 mConnectionCounter--;
173 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
176 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
178 qDebug() <<
"Socket error #" << socketError;
179 }, Qt::QueuedConnection );
183 QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
185 while ( clientConnection->bytesAvailable() > 0 )
187 incomingData->append( clientConnection->readAll() );
193 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
194 if ( firstLinePos == -1 )
196 throw HttpException( u
"HTTP error finding protocol header"_s );
199 const QString firstLine { incomingData->left( firstLinePos ) };
200 const QStringList firstLinePieces { firstLine.split(
' ' ) };
201 if ( firstLinePieces.size() != 3 )
203 throw HttpException( u
"HTTP error splitting protocol header"_s );
206 const QString methodString { firstLinePieces.at( 0 ) };
209 if ( methodString ==
"GET" )
213 else if ( methodString ==
"POST" )
217 else if ( methodString ==
"HEAD" )
221 else if ( methodString ==
"PUT" )
225 else if ( methodString ==
"PATCH" )
229 else if ( methodString ==
"DELETE" )
233 else if ( methodString ==
"OPTIONS" )
239 throw HttpException( u
"HTTP error unsupported method: %1"_s.arg( methodString ) );
243 const QString protocol { firstLinePieces.at( 2 ) };
244 if ( protocol !=
"HTTP/1.0"_L1 && protocol !=
"HTTP/1.1"_L1 )
246 throw HttpException( u
"HTTP error unsupported protocol: %1"_s.arg( protocol ) );
251 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
253 if ( endHeadersPos == -1 )
255 throw HttpException( u
"HTTP error finding headers"_s );
258 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
260 for (
const auto &headerLine : httpHeaders )
262 const auto headerColonPos { headerLine.indexOf(
':' ) };
263 if ( headerColonPos > 0 )
265 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
269 const auto headersSize { endHeadersPos + 4 };
272 if ( headers.contains( u
"Content-Length"_s ) )
275 const int contentLength { headers.value( u
"Content-Length"_s ).toInt( &ok ) };
276 if ( ok && contentLength > incomingData->length() - headersSize )
287 QString url { qgetenv(
"REQUEST_URI" ) };
292 const QString path { firstLinePieces.at( 1 ) };
294 if ( headers.contains( u
"Host"_s ) )
296 url = u
"http://%1%2"_s.arg( headers.value( u
"Host"_s ), path );
300 url = u
"http://%1:%2%3"_s.arg( ipAddress ).arg( port ).arg( path );
305 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
307 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
309 auto requestContext =
new RequestContext {
311 firstLinePieces.join(
' ' ),
312 std::chrono::steady_clock::now(),
313 { url, method, headers, &data },
316 REQUEST_QUEUE_MUTEX.lock();
317 REQUEST_QUEUE.enqueue( requestContext );
318 REQUEST_QUEUE_MUTEX.unlock();
319 REQUEST_WAIT_CONDITION.notify_one();
322 catch ( HttpException &ex )
324 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
327 clientConnection->write( u
"HTTP/1.0 %1 %2\r\n"_s.arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
328 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
329 clientConnection->write(
"\r\n" );
330 clientConnection->write( ex.message().toUtf8() );
332 std::cout << u
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m"_s.arg( clientConnection->peerAddress().toString(), QDateTime::currentDateTime().toString(), ex.message() ).toStdString() << std::endl;
334 clientConnection->disconnectFromHost();
342 ~TcpServerWorker()
override { mTcpServer.close(); }
344 bool isListening()
const {
return mIsListening; }
349 void responseReady( RequestContext *requestContext )
351 std::unique_ptr<RequestContext> request { requestContext };
352 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
354 const auto &response { request->response };
355 const auto &clientConnection { request->clientConnection };
357 if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
359 std::cout <<
"Connection reset by peer" << std::endl;
364 if ( -1 == clientConnection->write( u
"HTTP/1.0 %1 %2\r\n"_s.arg( response.statusCode() ).arg( knownStatuses.value( response.statusCode(), u
"Unknown response code"_s ) ).toUtf8() ) )
366 std::cout <<
"Cannot write to output socket" << std::endl;
367 clientConnection->disconnectFromHost();
371 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
372 const auto responseHeaders { response.fullHeaders() };
373 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
375 for (
const QString &headerValue : std::as_const( it.value() ) )
376 clientConnection->write( u
"%1: %2\r\n"_s.arg( it.key(), headerValue ).toUtf8() );
378 clientConnection->write(
"\r\n" );
379 const QByteArray body { response.body() };
380 clientConnection->write( body );
384 << u
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m"_s
386 clientConnection->peerAddress().toString(),
387 QDateTime::currentDateTime().toString(),
388 QString::number( body.size() ),
389 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
391 QString::number( response.statusCode() )
397 clientConnection->disconnectFromHost();
401 QTcpServer mTcpServer;
402 qlonglong mConnectionCounter = 0;
403 bool mIsListening =
false;
407class TcpServerThread :
public QThread
412 TcpServerThread(
const QString &ipAddress,
const int port )
413 : mIpAddress( ipAddress )
417 void emitResponseReady( RequestContext *requestContext )
419 if ( requestContext->clientConnection )
420 emit responseReady( requestContext );
425 const TcpServerWorker worker( mIpAddress, mPort );
426 if ( !worker.isListening() )
433 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
440 void responseReady( RequestContext *requestContext );
449class QueueMonitorThread :
public QThread
458 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
459 REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
464 emit requestReady( REQUEST_QUEUE.dequeue() );
471 void requestReady( RequestContext *requestContext );
475 void stop() { mIsRunning =
false; }
478 bool mIsRunning =
true;
481int main(
int argc,
char *argv[] )
492 const QString display { qgetenv(
"DISPLAY" ) };
493 bool withDisplay =
true;
494 if ( display.isEmpty() )
497 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
501 const QgsApplication app( argc, argv, withDisplay, QString(), u
"QGIS Development Server"_s );
503 if (
const char *globalSettingsFile = getenv(
"QGIS_GLOBAL_SETTINGS_FILE" ) )
505 const QString path = QString::fromLocal8Bit( globalSettingsFile );
512 QCoreApplication::setApplicationName(
"QGIS Development Server" );
513 QCoreApplication::setApplicationVersion( VERSION );
518 "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
519 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
528 QFontDatabase fontDB;
532 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
534 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
536 if ( serverPort.isEmpty() )
538 serverPort = u
"8000"_s;
541 if ( ipAddress.isEmpty() )
543 ipAddress = u
"127.0.0.1"_s;
546 QCommandLineParser parser;
547 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
548 parser.addHelpOption();
550 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
551 parser.addOption( versionOption );
553 parser.addPositionalArgument(
556 "Address and port (default: \"localhost:8000\")\n"
557 "address and port can also be specified with the environment\n"
558 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT."
562 const QCommandLineOption logLevelOption(
565 "Log level (default: 0)\n"
573 parser.addOption( logLevelOption );
575 const QCommandLineOption projectOption(
578 "Path to a QGIS project file (*.qgs or *.qgz),\n"
579 "if specified it will override the query string MAP argument\n"
580 "and the QGIS_PROJECT_FILE environment variable."
585 parser.addOption( projectOption );
587 parser.process( app );
589 if ( parser.isSet( versionOption ) )
595 const QStringList args = parser.positionalArguments();
597 if ( args.size() == 1 )
599 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
600 if ( addressAndPort.size() == 2 )
602 ipAddress = addressAndPort.at( 0 );
604 serverPort = addressAndPort.at( 1 );
608 const QString logLevel = parser.value( logLevelOption );
609 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
610 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
611 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
615 if ( !parser.value( projectOption ).isEmpty() )
618 const QString projectFilePath { parser.value( projectOption ) };
622 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
626 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
634#ifdef HAVE_SERVER_PYTHON_PLUGINS
639 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
641 bool isTcpError =
false;
642 TcpServerThread::connect(
644 &TcpServerThread::serverError,
654 QueueMonitorThread queueMonitorThread;
655 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
656 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
658 server.
handleRequest( requestContext->request, requestContext->response );
659 SERVER_MUTEX.unlock();
663 delete requestContext;
664 SERVER_MUTEX.unlock();
667 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
668 tcpServerThread.emitResponseReady( requestContext );
670 delete requestContext;
676 auto exitHandler = [](
int signal ) {
677 std::cout << u
"Signal %1 received: quitting"_s.arg( signal ).toStdString() << std::endl;
682 signal( SIGTERM, exitHandler );
683 signal( SIGABRT, exitHandler );
684 signal( SIGINT, exitHandler );
685 signal( SIGPIPE, [](
int ) { std::cerr << u
"Signal SIGPIPE received: ignoring"_s.toStdString() << std::endl; } );
689 tcpServerThread.start();
690 queueMonitorThread.start();
692 QgsApplication::exec();
694 tcpServerThread.exit();
695 tcpServerThread.wait();
696 queueMonitorThread.stop();
697 REQUEST_WAIT_CONDITION.notify_all();
698 queueMonitorThread.wait();
701 return isTcpError ? 1 : 0;
704#include "qgis_mapserver.moc"
@ DontLoad3DViews
Skip loading 3D views.
@ DontStoreOriginalStyles
Skip the initial XML style storage for layers. Useful for minimising project load times in non-intera...
@ DontUpgradeAnnotations
Don't upgrade old annotation items to QgsAnnotationItem.
@ DontLoadLayouts
Don't load print layouts. Improves project read time if layouts are not required, and allows projects...
@ DontResolveLayers
Don't resolve layer paths (i.e. don't load any layer content). Dramatically improves project read tim...
@ Warning
Warning message.
Extends QApplication to provide access to QGIS specific resources such as theme paths,...
static void exitQgis()
deletes provider registry and map layer registry
static const char * QGIS_ORGANIZATION_DOMAIN
static const char * QGIS_ORGANIZATION_NAME
static QString allVersions()
Display all versions in the standard output stream.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
static QgsProject * instance()
Returns the QgsProject singleton instance.
Method
HTTP Method (or equivalent) used for the request.
QMap< QString, QString > Headers
A server which provides OGC web services.
void handleRequest(QgsServerRequest &request, QgsServerResponse &response, const QgsProject *project=nullptr)
Handles the request.
static bool setGlobalSettingsPath(const QString &path)
Sets the path to the global settings INI file and loads all keys into an in-memory hash for use as de...
static bool setGlobalSettingsPath(const QString &path)
Sets the Global Settings QSettings storage file.
int main(int argc, char *argv[])