30#include <condition_variable>
41#include <QFontDatabase>
45#include <QNetworkInterface>
46#include <QCommandLineParser>
59QAtomicInt IS_RUNNING = 1;
64std::condition_variable REQUEST_WAIT_CONDITION;
65std::mutex REQUEST_QUEUE_MUTEX;
66std::mutex SERVER_MUTEX;
70 QPointer<QTcpSocket> clientConnection;
72 std::chrono::steady_clock::time_point startTime;
78QQueue<RequestContext *> REQUEST_QUEUE;
80const QMap<int, QString> knownStatuses
82 { 200, QStringLiteral(
"OK" ) },
83 { 201, QStringLiteral(
"Created" ) },
84 { 202, QStringLiteral(
"Accepted" ) },
85 { 204, QStringLiteral(
"No Content" ) },
86 { 301, QStringLiteral(
"Moved Permanently" ) },
87 { 302, QStringLiteral(
"Moved Temporarily" ) },
88 { 304, QStringLiteral(
"Not Modified" ) },
89 { 400, QStringLiteral(
"Bad Request" ) },
90 { 401, QStringLiteral(
"Unauthorized" ) },
91 { 403, QStringLiteral(
"Forbidden" ) },
92 { 404, QStringLiteral(
"Not Found" ) },
93 { 500, QStringLiteral(
"Internal Server Error" ) },
94 { 501, QStringLiteral(
"Not Implemented" ) },
95 { 502, QStringLiteral(
"Bad Gateway" ) },
96 { 503, QStringLiteral(
"Service Unavailable" ) }
102class HttpException:
public std::exception
110 HttpException(
const QString &message )
111 : mMessage( message )
130class TcpServerWorker:
public QObject
136 TcpServerWorker(
const QString &ipAddress,
int port )
138 QHostAddress address { QHostAddress::AnyIPv4 };
139 address.setAddress( ipAddress );
141 if ( ! mTcpServer.listen( address, port ) )
143 std::cerr << tr(
"Unable to start the server: %1." )
144 .arg( mTcpServer.errorString() ).toStdString() << std::endl;
148 const int port { mTcpServer.serverPort() };
150 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
152 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
158 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection,
this, [ = ]
160 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
162 mConnectionCounter++;
166 QString *incomingData =
new QString();
169 QObject *context {
new QObject };
172 auto connectionDeleter = [ = ]()
174 clientConnection->deleteLater();
175 mConnectionCounter--;
180 QTcpSocket::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
183 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
185 qDebug() <<
"Socket error #" << socketError;
186 }, Qt::QueuedConnection );
190 QTcpSocket::connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
193 while ( clientConnection->bytesAvailable() > 0 )
195 incomingData->append( clientConnection->readAll() );
201 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
202 if ( firstLinePos == -1 )
204 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
207 const QString firstLine { incomingData->left( firstLinePos ) };
208 const QStringList firstLinePieces { firstLine.split(
' ' ) };
209 if ( firstLinePieces.size() != 3 )
211 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
214 const QString methodString { firstLinePieces.at( 0 ) };
217 if ( methodString ==
"GET" )
219 method = QgsServerRequest::Method::GetMethod;
221 else if ( methodString ==
"POST" )
223 method = QgsServerRequest::Method::PostMethod;
225 else if ( methodString ==
"HEAD" )
227 method = QgsServerRequest::Method::HeadMethod;
229 else if ( methodString ==
"PUT" )
231 method = QgsServerRequest::Method::PutMethod;
233 else if ( methodString ==
"PATCH" )
235 method = QgsServerRequest::Method::PatchMethod;
237 else if ( methodString ==
"DELETE" )
239 method = QgsServerRequest::Method::DeleteMethod;
243 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
247 const QString protocol { firstLinePieces.at( 2 )};
248 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
250 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
255 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
257 if ( endHeadersPos == -1 )
259 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
262 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
264 for (
const auto &headerLine : httpHeaders )
266 const auto headerColonPos { headerLine.indexOf(
':' ) };
267 if ( headerColonPos > 0 )
269 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
273 const auto headersSize { endHeadersPos + 4 };
276 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
279 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
280 if ( ok && contentLength > incomingData->length() - headersSize )
291 QString url { qgetenv(
"REQUEST_URI" ) };
296 const QString path { firstLinePieces.at( 1 )};
298 if ( headers.contains( QStringLiteral(
"Host" ) ) )
300 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
304 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
309 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
311 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
313 auto requestContext =
new RequestContext
316 firstLinePieces.join(
' ' ),
317 std::chrono::steady_clock::now(),
318 { url, method, headers, &data },
321 REQUEST_QUEUE_MUTEX.lock();
322 REQUEST_QUEUE.enqueue( requestContext );
323 REQUEST_QUEUE_MUTEX.unlock();
324 REQUEST_WAIT_CONDITION.notify_one();
327 catch ( HttpException &ex )
329 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
332 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
333 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
334 clientConnection->write(
"\r\n" );
335 clientConnection->write( ex.message().toUtf8() );
337 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
338 .arg( clientConnection->peerAddress().toString() )
339 .arg( QDateTime::currentDateTime().toString() )
340 .arg( ex.message() ).toStdString() << std::endl;
342 clientConnection->disconnectFromHost();
355 bool isListening()
const
363 void responseReady( RequestContext *requestContext )
365 std::unique_ptr<RequestContext> request { requestContext };
366 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
368 const auto &response { request->response };
369 const auto &clientConnection { request->clientConnection };
371 if ( ! clientConnection ||
372 clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
374 std::cout <<
"Connection reset by peer" << std::endl;
379 if ( -1 == clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( response.statusCode() ).arg( knownStatuses.value( response.statusCode(), QStringLiteral(
"Unknown response code" ) ) ).toUtf8() ) )
381 std::cout <<
"Cannot write to output socket" << std::endl;
382 clientConnection->disconnectFromHost();
386 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
387 const auto responseHeaders { response.headers() };
388 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
390 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
392 clientConnection->write(
"\r\n" );
393 const QByteArray body { response.body() };
394 clientConnection->write( body );
397 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
398 .arg( clientConnection->peerAddress().toString(),
399 QDateTime::currentDateTime().toString(),
400 QString::number( body.size() ),
401 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
403 QString::number( response.statusCode() ) )
408 clientConnection->disconnectFromHost();
413 QTcpServer mTcpServer;
414 qlonglong mConnectionCounter = 0;
415 bool mIsListening =
false;
420class TcpServerThread:
public QThread
426 TcpServerThread(
const QString &ipAddress,
const int port )
427 : mIpAddress( ipAddress )
432 void emitResponseReady( RequestContext *requestContext )
434 if ( requestContext->clientConnection )
435 emit responseReady( requestContext );
440 const TcpServerWorker worker( mIpAddress, mPort );
441 if ( ! worker.isListening() )
448 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
455 void responseReady( RequestContext *requestContext );
465class QueueMonitorThread:
public QThread
475 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
476 REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] {
return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
481 emit requestReady( REQUEST_QUEUE.dequeue() );
488 void requestReady( RequestContext *requestContext );
499 bool mIsRunning =
true;
503int main(
int argc,
char *argv[] )
514 const QString display { qgetenv(
"DISPLAY" ) };
515 bool withDisplay =
true;
516 if ( display.isEmpty() )
519 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
523 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
527 QCoreApplication::setApplicationName(
"QGIS Development Server" );
528 QCoreApplication::setApplicationVersion( VERSION );
532 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
533 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
"Server", Qgis::MessageLevel::Warning );
539 QFontDatabase fontDB;
543 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
545 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
547 if ( serverPort.isEmpty() )
549 serverPort = QStringLiteral(
"8000" );
552 if ( ipAddress.isEmpty() )
554 ipAddress = QStringLiteral(
"localhost" );
557 QCommandLineParser parser;
558 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
559 parser.addHelpOption();
561 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
562 parser.addOption( versionOption );
564 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ),
565 QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
566 "address and port can also be specified with the environment\n"
567 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral(
"[address:port]" ) );
568 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
571 "2: CRITICAL" ),
"logLevel",
"0" );
572 parser.addOption( logLevelOption );
574 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
575 "if specified it will override the query string MAP argument\n"
576 "and the QGIS_PROJECT_FILE environment variable." ),
"projectPath",
"" );
577 parser.addOption( projectOption );
579 parser.process( app );
581 if ( parser.isSet( versionOption ) )
587 const QStringList args = parser.positionalArguments();
589 if ( args.size() == 1 )
591 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
592 if ( addressAndPort.size() == 2 )
594 ipAddress = addressAndPort.at( 0 );
596 serverPort = addressAndPort.at( 1 );
600 const QString logLevel = parser.value( logLevelOption );
601 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
602 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
603 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
607 if ( ! parser.value( projectOption ).isEmpty( ) )
610 const QString projectFilePath { parser.value( projectOption ) };
617 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
621 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
629#ifdef HAVE_SERVER_PYTHON_PLUGINS
634 TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
636 bool isTcpError =
false;
637 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
641 }, Qt::QueuedConnection );
644 QueueMonitorThread queueMonitorThread;
645 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
647 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
649 server.handleRequest( requestContext->request, requestContext->response );
650 SERVER_MUTEX.unlock();
654 delete requestContext;
655 SERVER_MUTEX.unlock();
658 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
659 tcpServerThread.emitResponseReady( requestContext );
661 delete requestContext;
667 auto exitHandler = [ ](
int signal )
669 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
674 signal( SIGTERM, exitHandler );
675 signal( SIGABRT, exitHandler );
676 signal( SIGINT, exitHandler );
677 signal( SIGPIPE, [ ](
int )
679 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
684 tcpServerThread.start();
685 queueMonitorThread.start();
687 QgsApplication::exec();
689 tcpServerThread.exit();
690 tcpServerThread.wait();
691 queueMonitorThread.stop();
692 REQUEST_WAIT_CONDITION.notify_all();
693 queueMonitorThread.wait();
696 return isTcpError ? 1 : 0;
699#include "qgis_mapserver.moc"
@ DontLoad3DViews
Skip loading 3D views (since QGIS 3.26)
@ DontStoreOriginalStyles
Skip the initial XML style storage for layers. Useful for minimising project load times in non-intera...
@ 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...
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
Class defining request with data.
Class defining buffered response.
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)
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
The QgsServer class provides OGC web services.
int main(int argc, char *argv[])