28#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;
73 QgsBufferServerRequest request;
74 QgsBufferServerResponse response;
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
108 HttpException(
const QString &message )
109 : mMessage( message )
116 QString message()
const
126class TcpServerWorker :
public QObject
131 TcpServerWorker(
const QString &ipAddress,
int port )
133 QHostAddress address { QHostAddress::AnyIPv4 };
134 address.setAddress( ipAddress );
136 if ( !mTcpServer.listen( address, port ) )
138 std::cerr << tr(
"Unable to start the server: %1." )
139 .arg( mTcpServer.errorString() )
145 const int port { mTcpServer.serverPort() };
147 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
149 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
155 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection,
this, [
this, ipAddress, port] {
156 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
158 mConnectionCounter++;
162 QString *incomingData =
new QString();
165 QObject *context {
new QObject };
168 auto connectionDeleter = [
this, clientConnection, incomingData]() {
169 clientConnection->deleteLater();
170 mConnectionCounter--;
175 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
178 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
180 qDebug() <<
"Socket error #" << socketError;
181 }, Qt::QueuedConnection );
185 QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
187 while ( clientConnection->bytesAvailable() > 0 )
189 incomingData->append( clientConnection->readAll() );
195 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
196 if ( firstLinePos == -1 )
198 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
201 const QString firstLine { incomingData->left( firstLinePos ) };
202 const QStringList firstLinePieces { firstLine.split(
' ' ) };
203 if ( firstLinePieces.size() != 3 )
205 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
208 const QString methodString { firstLinePieces.at( 0 ) };
211 if ( methodString ==
"GET" )
215 else if ( methodString ==
"POST" )
219 else if ( methodString ==
"HEAD" )
223 else if ( methodString ==
"PUT" )
227 else if ( methodString ==
"PATCH" )
231 else if ( methodString ==
"DELETE" )
237 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
241 const QString protocol { firstLinePieces.at( 2 ) };
242 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
244 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
249 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
251 if ( endHeadersPos == -1 )
253 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
256 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
258 for (
const auto &headerLine : httpHeaders )
260 const auto headerColonPos { headerLine.indexOf(
':' ) };
261 if ( headerColonPos > 0 )
263 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
267 const auto headersSize { endHeadersPos + 4 };
270 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
273 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
274 if ( ok && contentLength > incomingData->length() - headersSize )
285 QString url { qgetenv(
"REQUEST_URI" ) };
290 const QString path { firstLinePieces.at( 1 ) };
292 if ( headers.contains( QStringLiteral(
"Host" ) ) )
294 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
298 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
303 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
305 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
307 auto requestContext =
new RequestContext {
309 firstLinePieces.join(
' ' ),
310 std::chrono::steady_clock::now(),
311 { url, method, headers, &data },
314 REQUEST_QUEUE_MUTEX.lock();
315 REQUEST_QUEUE.enqueue( requestContext );
316 REQUEST_QUEUE_MUTEX.unlock();
317 REQUEST_WAIT_CONDITION.notify_one();
320 catch ( HttpException &ex )
322 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
325 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
326 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
327 clientConnection->write(
"\r\n" );
328 clientConnection->write( ex.message().toUtf8() );
330 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
331 .arg( clientConnection->peerAddress().toString() )
332 .arg( QDateTime::currentDateTime().toString() )
337 clientConnection->disconnectFromHost();
345 ~TcpServerWorker()
override
350 bool isListening()
const
358 void responseReady( RequestContext *requestContext )
360 std::unique_ptr<RequestContext> request { requestContext };
361 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
363 const auto &response { request->response };
364 const auto &clientConnection { request->clientConnection };
366 if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
368 std::cout <<
"Connection reset by peer" << std::endl;
373 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() ) )
375 std::cout <<
"Cannot write to output socket" << std::endl;
376 clientConnection->disconnectFromHost();
380 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
381 const auto responseHeaders { response.headers() };
382 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
384 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
386 clientConnection->write(
"\r\n" );
387 const QByteArray body { response.body() };
388 clientConnection->write( body );
391 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
392 .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() ) )
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 )
418 void emitResponseReady( RequestContext *requestContext )
420 if ( requestContext->clientConnection )
421 emit responseReady( requestContext );
426 const TcpServerWorker worker( mIpAddress, mPort );
427 if ( !worker.isListening() )
434 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
441 void responseReady( RequestContext *requestContext );
450class QueueMonitorThread :
public QThread
459 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
460 REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
465 emit requestReady( REQUEST_QUEUE.dequeue() );
472 void requestReady( RequestContext *requestContext );
482 bool mIsRunning =
true;
485int main(
int argc,
char *argv[] )
496 const QString display { qgetenv(
"DISPLAY" ) };
497 bool withDisplay =
true;
498 if ( display.isEmpty() )
501 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
505 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
509 QCoreApplication::setApplicationName(
"QGIS Development Server" );
510 QCoreApplication::setApplicationVersion( VERSION );
514 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
515 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
522 QFontDatabase fontDB;
526 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
528 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
530 if ( serverPort.isEmpty() )
532 serverPort = QStringLiteral(
"8000" );
535 if ( ipAddress.isEmpty() )
537 ipAddress = QStringLiteral(
"localhost" );
540 QCommandLineParser parser;
541 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
542 parser.addHelpOption();
544 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
545 parser.addOption( versionOption );
547 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ), QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
548 "address and port can also be specified with the environment\n"
549 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ),
550 QStringLiteral(
"[address:port]" ) );
551 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
556 parser.addOption( logLevelOption );
558 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
559 "if specified it will override the query string MAP argument\n"
560 "and the QGIS_PROJECT_FILE environment variable." ),
562 parser.addOption( projectOption );
564 parser.process( app );
566 if ( parser.isSet( versionOption ) )
572 const QStringList args = parser.positionalArguments();
574 if ( args.size() == 1 )
576 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
577 if ( addressAndPort.size() == 2 )
579 ipAddress = addressAndPort.at( 0 );
581 serverPort = addressAndPort.at( 1 );
585 const QString logLevel = parser.value( logLevelOption );
586 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
587 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
588 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
592 if ( !parser.value( projectOption ).isEmpty() )
595 const QString projectFilePath { parser.value( projectOption ) };
598 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
602 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
610#ifdef HAVE_SERVER_PYTHON_PLUGINS
615 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
617 bool isTcpError =
false;
618 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [&] {
620 qApp->quit(); }, Qt::QueuedConnection );
623 QueueMonitorThread queueMonitorThread;
624 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
625 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
627 server.
handleRequest( requestContext->request, requestContext->response );
628 SERVER_MUTEX.unlock();
632 delete requestContext;
633 SERVER_MUTEX.unlock();
636 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
637 tcpServerThread.emitResponseReady( requestContext );
639 delete requestContext;
645 auto exitHandler = [](
int signal ) {
646 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
651 signal( SIGTERM, exitHandler );
652 signal( SIGABRT, exitHandler );
653 signal( SIGINT, exitHandler );
654 signal( SIGPIPE, [](
int ) {
655 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
660 tcpServerThread.start();
661 queueMonitorThread.start();
663 QgsApplication::exec();
665 tcpServerThread.exit();
666 tcpServerThread.wait();
667 queueMonitorThread.stop();
668 REQUEST_WAIT_CONDITION.notify_all();
669 queueMonitorThread.wait();
672 return isTcpError ? 1 : 0;
675#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())
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.
int main(int argc, char *argv[])