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 {
81 { 200, QStringLiteral(
"OK" ) },
82 { 201, QStringLiteral(
"Created" ) },
83 { 202, QStringLiteral(
"Accepted" ) },
84 { 204, QStringLiteral(
"No Content" ) },
85 { 301, QStringLiteral(
"Moved Permanently" ) },
86 { 302, QStringLiteral(
"Moved Temporarily" ) },
87 { 304, QStringLiteral(
"Not Modified" ) },
88 { 400, QStringLiteral(
"Bad Request" ) },
89 { 401, QStringLiteral(
"Unauthorized" ) },
90 { 403, QStringLiteral(
"Forbidden" ) },
91 { 404, QStringLiteral(
"Not Found" ) },
92 { 500, QStringLiteral(
"Internal Server Error" ) },
93 { 501, QStringLiteral(
"Not Implemented" ) },
94 { 502, QStringLiteral(
"Bad Gateway" ) },
95 { 503, QStringLiteral(
"Service Unavailable" ) }
101class HttpException :
public std::exception
107 HttpException(
const QString &message )
108 : mMessage( message )
125class TcpServerWorker :
public QObject
130 TcpServerWorker(
const QString &ipAddress,
int port )
132 QHostAddress address { QHostAddress::AnyIPv4 };
133 address.setAddress( ipAddress );
135 if ( !mTcpServer.listen( address, port ) )
137 std::cerr << tr(
"Unable to start the server: %1." )
138 .arg( mTcpServer.errorString() )
144 const int port { mTcpServer.serverPort() };
146 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
148 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
154 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection,
this, [=] {
155 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
157 mConnectionCounter++;
161 QString *incomingData =
new QString();
164 QObject *context {
new QObject };
167 auto connectionDeleter = [=]() {
168 clientConnection->deleteLater();
169 mConnectionCounter--;
174 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
177 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
179 qDebug() <<
"Socket error #" << socketError;
180 }, Qt::QueuedConnection );
184 QObject::connect( clientConnection, &QIODevice::readyRead, context, [=] {
186 while ( clientConnection->bytesAvailable() > 0 )
188 incomingData->append( clientConnection->readAll() );
194 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
195 if ( firstLinePos == -1 )
197 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
200 const QString firstLine { incomingData->left( firstLinePos ) };
201 const QStringList firstLinePieces { firstLine.split(
' ' ) };
202 if ( firstLinePieces.size() != 3 )
204 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
207 const QString methodString { firstLinePieces.at( 0 ) };
210 if ( methodString ==
"GET" )
214 else if ( methodString ==
"POST" )
218 else if ( methodString ==
"HEAD" )
222 else if ( methodString ==
"PUT" )
226 else if ( methodString ==
"PATCH" )
230 else if ( methodString ==
"DELETE" )
236 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
240 const QString protocol { firstLinePieces.at( 2 ) };
241 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
243 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
248 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
250 if ( endHeadersPos == -1 )
252 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
255 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
257 for (
const auto &headerLine : httpHeaders )
259 const auto headerColonPos { headerLine.indexOf(
':' ) };
260 if ( headerColonPos > 0 )
262 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
266 const auto headersSize { endHeadersPos + 4 };
269 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
272 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
273 if ( ok && contentLength > incomingData->length() - headersSize )
284 QString url { qgetenv(
"REQUEST_URI" ) };
289 const QString path { firstLinePieces.at( 1 ) };
291 if ( headers.contains( QStringLiteral(
"Host" ) ) )
293 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
297 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
302 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
304 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
306 auto requestContext =
new RequestContext {
308 firstLinePieces.join(
' ' ),
309 std::chrono::steady_clock::now(),
310 { url, method, headers, &data },
313 REQUEST_QUEUE_MUTEX.lock();
314 REQUEST_QUEUE.enqueue( requestContext );
315 REQUEST_QUEUE_MUTEX.unlock();
316 REQUEST_WAIT_CONDITION.notify_one();
319 catch ( HttpException &ex )
321 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
324 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
325 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
326 clientConnection->write(
"\r\n" );
327 clientConnection->write( ex.message().toUtf8() );
329 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
330 .arg( clientConnection->peerAddress().toString() )
331 .arg( QDateTime::currentDateTime().toString() )
336 clientConnection->disconnectFromHost();
349 bool isListening()
const
357 void responseReady( RequestContext *requestContext )
359 std::unique_ptr<RequestContext> request { requestContext };
360 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
362 const auto &response { request->response };
363 const auto &clientConnection { request->clientConnection };
365 if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
367 std::cout <<
"Connection reset by peer" << std::endl;
372 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() ) )
374 std::cout <<
"Cannot write to output socket" << std::endl;
375 clientConnection->disconnectFromHost();
379 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
380 const auto responseHeaders { response.headers() };
381 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
383 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
385 clientConnection->write(
"\r\n" );
386 const QByteArray body { response.body() };
387 clientConnection->write( body );
390 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
391 .arg( clientConnection->peerAddress().toString(), QDateTime::currentDateTime().toString(), QString::number( body.size() ), QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ), request->httpHeader, QString::number( response.statusCode() ) )
396 clientConnection->disconnectFromHost();
400 QTcpServer mTcpServer;
401 qlonglong mConnectionCounter = 0;
402 bool mIsListening =
false;
406class TcpServerThread :
public QThread
411 TcpServerThread(
const QString &ipAddress,
const int port )
412 : 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, [=] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
464 emit requestReady( REQUEST_QUEUE.dequeue() );
471 void requestReady( RequestContext *requestContext );
481 bool mIsRunning =
true;
484int main(
int argc,
char *argv[] )
495 const QString display { qgetenv(
"DISPLAY" ) };
496 bool withDisplay =
true;
497 if ( display.isEmpty() )
500 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
504 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
508 QCoreApplication::setApplicationName(
"QGIS Development Server" );
509 QCoreApplication::setApplicationVersion( VERSION );
513 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
514 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
521 QFontDatabase fontDB;
525 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
527 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
529 if ( serverPort.isEmpty() )
531 serverPort = QStringLiteral(
"8000" );
534 if ( ipAddress.isEmpty() )
536 ipAddress = QStringLiteral(
"localhost" );
539 QCommandLineParser parser;
540 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
541 parser.addHelpOption();
543 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
544 parser.addOption( versionOption );
546 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ), QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
547 "address and port can also be specified with the environment\n"
548 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ),
549 QStringLiteral(
"[address:port]" ) );
550 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
555 parser.addOption( logLevelOption );
557 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
558 "if specified it will override the query string MAP argument\n"
559 "and the QGIS_PROJECT_FILE environment variable." ),
561 parser.addOption( projectOption );
563 parser.process( app );
565 if ( parser.isSet( versionOption ) )
571 const QStringList args = parser.positionalArguments();
573 if ( args.size() == 1 )
575 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
576 if ( addressAndPort.size() == 2 )
578 ipAddress = addressAndPort.at( 0 );
580 serverPort = addressAndPort.at( 1 );
584 const QString logLevel = parser.value( logLevelOption );
585 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
586 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
587 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
591 if ( !parser.value( projectOption ).isEmpty() )
594 const QString projectFilePath { parser.value( projectOption ) };
597 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
601 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
609#ifdef HAVE_SERVER_PYTHON_PLUGINS
614 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
616 bool isTcpError =
false;
617 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [&] {
619 qApp->quit(); }, Qt::QueuedConnection );
622 QueueMonitorThread queueMonitorThread;
623 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
624 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
626 server.handleRequest( requestContext->request, requestContext->response );
627 SERVER_MUTEX.unlock();
631 delete requestContext;
632 SERVER_MUTEX.unlock();
635 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
636 tcpServerThread.emitResponseReady( requestContext );
638 delete requestContext;
644 auto exitHandler = [](
int signal ) {
645 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
650 signal( SIGTERM, exitHandler );
651 signal( SIGABRT, exitHandler );
652 signal( SIGINT, exitHandler );
653 signal( SIGPIPE, [](
int ) {
654 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
659 tcpServerThread.start();
660 queueMonitorThread.start();
662 QgsApplication::exec();
664 tcpServerThread.exit();
665 tcpServerThread.wait();
666 queueMonitorThread.stop();
667 REQUEST_WAIT_CONDITION.notify_all();
668 queueMonitorThread.wait();
671 return isTcpError ? 1 : 0;
674#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
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[])