28#include <condition_variable>
34using namespace Qt::StringLiterals;
45#include <QFontDatabase>
49#include <QNetworkInterface>
50#include <QCommandLineParser>
63QAtomicInt IS_RUNNING = 1;
68std::condition_variable REQUEST_WAIT_CONDITION;
69std::mutex REQUEST_QUEUE_MUTEX;
70std::mutex SERVER_MUTEX;
74 QPointer<QTcpSocket> clientConnection;
76 std::chrono::steady_clock::time_point startTime;
77 QgsBufferServerRequest request;
78 QgsBufferServerResponse response;
82QQueue<RequestContext *> REQUEST_QUEUE;
84const QMap<int, QString> knownStatuses {
86 { 201, u
"Created"_s },
87 { 202, u
"Accepted"_s },
88 { 204, u
"No Content"_s },
89 { 301, u
"Moved Permanently"_s },
90 { 302, u
"Moved Temporarily"_s },
91 { 304, u
"Not Modified"_s },
92 { 400, u
"Bad Request"_s },
93 { 401, u
"Unauthorized"_s },
94 { 403, u
"Forbidden"_s },
95 { 404, u
"Not Found"_s },
96 { 500, u
"Internal Server Error"_s },
97 { 501, u
"Not Implemented"_s },
98 { 502, u
"Bad Gateway"_s },
99 { 503, u
"Service Unavailable"_s }
105class HttpException :
public std::exception
111 HttpException(
const QString &message )
112 : mMessage( message )
118 QString message()
const {
return mMessage; }
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." ).arg( mTcpServer.errorString() ).toStdString() << std::endl;
141 const int port { mTcpServer.serverPort() };
143 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
145 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
151 QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection,
this, [
this, ipAddress, port] {
152 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
154 mConnectionCounter++;
158 QString *incomingData =
new QString();
161 QObject *context {
new QObject };
164 auto connectionDeleter = [
this, clientConnection, incomingData]() {
165 clientConnection->deleteLater();
166 mConnectionCounter--;
171 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
174 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
176 qDebug() <<
"Socket error #" << socketError;
177 }, Qt::QueuedConnection );
181 QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
183 while ( clientConnection->bytesAvailable() > 0 )
185 incomingData->append( clientConnection->readAll() );
191 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
192 if ( firstLinePos == -1 )
194 throw HttpException( u
"HTTP error finding protocol header"_s );
197 const QString firstLine { incomingData->left( firstLinePos ) };
198 const QStringList firstLinePieces { firstLine.split(
' ' ) };
199 if ( firstLinePieces.size() != 3 )
201 throw HttpException( u
"HTTP error splitting protocol header"_s );
204 const QString methodString { firstLinePieces.at( 0 ) };
207 if ( methodString ==
"GET" )
211 else if ( methodString ==
"POST" )
215 else if ( methodString ==
"HEAD" )
219 else if ( methodString ==
"PUT" )
223 else if ( methodString ==
"PATCH" )
227 else if ( methodString ==
"DELETE" )
233 throw HttpException( u
"HTTP error unsupported method: %1"_s.arg( methodString ) );
237 const QString protocol { firstLinePieces.at( 2 ) };
238 if ( protocol !=
"HTTP/1.0"_L1 && protocol !=
"HTTP/1.1"_L1 )
240 throw HttpException( u
"HTTP error unsupported protocol: %1"_s.arg( protocol ) );
245 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
247 if ( endHeadersPos == -1 )
249 throw HttpException( u
"HTTP error finding headers"_s );
252 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
254 for (
const auto &headerLine : httpHeaders )
256 const auto headerColonPos { headerLine.indexOf(
':' ) };
257 if ( headerColonPos > 0 )
259 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
263 const auto headersSize { endHeadersPos + 4 };
266 if ( headers.contains( u
"Content-Length"_s ) )
269 const int contentLength { headers.value( u
"Content-Length"_s ).toInt( &ok ) };
270 if ( ok && contentLength > incomingData->length() - headersSize )
281 QString url { qgetenv(
"REQUEST_URI" ) };
286 const QString path { firstLinePieces.at( 1 ) };
288 if ( headers.contains( u
"Host"_s ) )
290 url = u
"http://%1%2"_s.arg( headers.value( u
"Host"_s ), path );
294 url = u
"http://%1:%2%3"_s.arg( ipAddress ).arg( port ).arg( path );
299 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
301 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
303 auto requestContext =
new RequestContext {
305 firstLinePieces.join(
' ' ),
306 std::chrono::steady_clock::now(),
307 { url, method, headers, &data },
310 REQUEST_QUEUE_MUTEX.lock();
311 REQUEST_QUEUE.enqueue( requestContext );
312 REQUEST_QUEUE_MUTEX.unlock();
313 REQUEST_WAIT_CONDITION.notify_one();
316 catch ( HttpException &ex )
318 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
321 clientConnection->write( u
"HTTP/1.0 %1 %2\r\n"_s.arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
322 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
323 clientConnection->write(
"\r\n" );
324 clientConnection->write( ex.message().toUtf8() );
327 << u
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m"_s.arg( clientConnection->peerAddress().toString() ).arg( QDateTime::currentDateTime().toString() ).arg( ex.message() ).toStdString()
330 clientConnection->disconnectFromHost();
338 ~TcpServerWorker()
override { mTcpServer.close(); }
340 bool isListening()
const {
return mIsListening; }
345 void responseReady( RequestContext *requestContext )
347 std::unique_ptr<RequestContext> request { requestContext };
348 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
350 const auto &response { request->response };
351 const auto &clientConnection { request->clientConnection };
353 if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
355 std::cout <<
"Connection reset by peer" << std::endl;
360 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() ) )
362 std::cout <<
"Cannot write to output socket" << std::endl;
363 clientConnection->disconnectFromHost();
367 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
368 const auto responseHeaders { response.headers() };
369 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
371 clientConnection->write( u
"%1: %2\r\n"_s.arg( it.key(), it.value() ).toUtf8() );
373 clientConnection->write(
"\r\n" );
374 const QByteArray body { response.body() };
375 clientConnection->write( body );
379 << u
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m"_s
381 clientConnection->peerAddress().toString(),
382 QDateTime::currentDateTime().toString(),
383 QString::number( body.size() ),
384 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
386 QString::number( response.statusCode() )
392 clientConnection->disconnectFromHost();
396 QTcpServer mTcpServer;
397 qlonglong mConnectionCounter = 0;
398 bool mIsListening =
false;
402class TcpServerThread :
public QThread
407 TcpServerThread(
const QString &ipAddress,
const int port )
408 : mIpAddress( ipAddress )
412 void emitResponseReady( RequestContext *requestContext )
414 if ( requestContext->clientConnection )
415 emit responseReady( requestContext );
420 const TcpServerWorker worker( mIpAddress, mPort );
421 if ( !worker.isListening() )
428 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
435 void responseReady( RequestContext *requestContext );
444class QueueMonitorThread :
public QThread
453 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
454 REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
459 emit requestReady( REQUEST_QUEUE.dequeue() );
466 void requestReady( RequestContext *requestContext );
470 void stop() { mIsRunning =
false; }
473 bool mIsRunning =
true;
476int main(
int argc,
char *argv[] )
487 const QString display { qgetenv(
"DISPLAY" ) };
488 bool withDisplay =
true;
489 if ( display.isEmpty() )
492 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
496 const QgsApplication app( argc, argv, withDisplay, QString(), u
"QGIS Development Server"_s );
500 QCoreApplication::setApplicationName(
"QGIS Development Server" );
501 QCoreApplication::setApplicationVersion( VERSION );
506 "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
507 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
516 QFontDatabase fontDB;
520 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
522 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
524 if ( serverPort.isEmpty() )
526 serverPort = u
"8000"_s;
529 if ( ipAddress.isEmpty() )
531 ipAddress = u
"localhost"_s;
534 QCommandLineParser parser;
535 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
536 parser.addHelpOption();
538 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
539 parser.addOption( versionOption );
541 parser.addPositionalArgument(
544 "Address and port (default: \"localhost:8000\")\n"
545 "address and port can also be specified with the environment\n"
546 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT."
550 const QCommandLineOption logLevelOption(
553 "Log level (default: 0)\n"
561 parser.addOption( logLevelOption );
563 const QCommandLineOption projectOption(
566 "Path to a QGIS project file (*.qgs or *.qgz),\n"
567 "if specified it will override the query string MAP argument\n"
568 "and the QGIS_PROJECT_FILE environment variable."
573 parser.addOption( projectOption );
575 parser.process( app );
577 if ( parser.isSet( versionOption ) )
583 const QStringList args = parser.positionalArguments();
585 if ( args.size() == 1 )
587 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
588 if ( addressAndPort.size() == 2 )
590 ipAddress = addressAndPort.at( 0 );
592 serverPort = addressAndPort.at( 1 );
596 const QString logLevel = parser.value( logLevelOption );
597 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
598 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
599 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
603 if ( !parser.value( projectOption ).isEmpty() )
606 const QString projectFilePath { parser.value( projectOption ) };
610 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
614 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
622#ifdef HAVE_SERVER_PYTHON_PLUGINS
627 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
629 bool isTcpError =
false;
630 TcpServerThread::connect(
632 &TcpServerThread::serverError,
642 QueueMonitorThread queueMonitorThread;
643 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
644 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
646 server.
handleRequest( requestContext->request, requestContext->response );
647 SERVER_MUTEX.unlock();
651 delete requestContext;
652 SERVER_MUTEX.unlock();
655 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
656 tcpServerThread.emitResponseReady( requestContext );
658 delete requestContext;
664 auto exitHandler = [](
int signal ) {
665 std::cout << u
"Signal %1 received: quitting"_s.arg( signal ).toStdString() << std::endl;
670 signal( SIGTERM, exitHandler );
671 signal( SIGABRT, exitHandler );
672 signal( SIGINT, exitHandler );
673 signal( SIGPIPE, [](
int ) { std::cerr << u
"Signal SIGPIPE received: ignoring"_s.toStdString() << std::endl; } );
677 tcpServerThread.start();
678 queueMonitorThread.start();
680 QgsApplication::exec();
682 tcpServerThread.exit();
683 tcpServerThread.wait();
684 queueMonitorThread.stop();
685 REQUEST_WAIT_CONDITION.notify_all();
686 queueMonitorThread.wait();
689 return isTcpError ? 1 : 0;
692#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.
int main(int argc, char *argv[])