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" )
231 else if ( methodString ==
"OPTIONS" )
237 throw HttpException( u
"HTTP error unsupported method: %1"_s.arg( methodString ) );
241 const QString protocol { firstLinePieces.at( 2 ) };
242 if ( protocol !=
"HTTP/1.0"_L1 && protocol !=
"HTTP/1.1"_L1 )
244 throw HttpException( u
"HTTP error unsupported protocol: %1"_s.arg( protocol ) );
249 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
251 if ( endHeadersPos == -1 )
253 throw HttpException( u
"HTTP error finding headers"_s );
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( u
"Content-Length"_s ) )
273 const int contentLength { headers.value( u
"Content-Length"_s ).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( u
"Host"_s ) )
294 url = u
"http://%1%2"_s.arg( headers.value( u
"Host"_s ), path );
298 url = u
"http://%1:%2%3"_s.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( u
"HTTP/1.0 %1 %2\r\n"_s.arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
326 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
327 clientConnection->write(
"\r\n" );
328 clientConnection->write( ex.message().toUtf8() );
331 << u
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m"_s.arg( clientConnection->peerAddress().toString() ).arg( QDateTime::currentDateTime().toString() ).arg( ex.message() ).toStdString()
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.headers() };
373 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
375 clientConnection->write( u
"%1: %2\r\n"_s.arg( it.key(), it.value() ).toUtf8() );
377 clientConnection->write(
"\r\n" );
378 const QByteArray body { response.body() };
379 clientConnection->write( body );
383 << u
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m"_s
385 clientConnection->peerAddress().toString(),
386 QDateTime::currentDateTime().toString(),
387 QString::number( body.size() ),
388 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
390 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 )
416 void emitResponseReady( RequestContext *requestContext )
418 if ( requestContext->clientConnection )
419 emit responseReady( requestContext );
424 const TcpServerWorker worker( mIpAddress, mPort );
425 if ( !worker.isListening() )
432 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
439 void responseReady( RequestContext *requestContext );
448class QueueMonitorThread :
public QThread
457 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
458 REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
463 emit requestReady( REQUEST_QUEUE.dequeue() );
470 void requestReady( RequestContext *requestContext );
474 void stop() { mIsRunning =
false; }
477 bool mIsRunning =
true;
480int main(
int argc,
char *argv[] )
491 const QString display { qgetenv(
"DISPLAY" ) };
492 bool withDisplay =
true;
493 if ( display.isEmpty() )
496 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
500 const QgsApplication app( argc, argv, withDisplay, QString(), u
"QGIS Development Server"_s );
504 QCoreApplication::setApplicationName(
"QGIS Development Server" );
505 QCoreApplication::setApplicationVersion( VERSION );
510 "DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
511 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
520 QFontDatabase fontDB;
524 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
526 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
528 if ( serverPort.isEmpty() )
530 serverPort = u
"8000"_s;
533 if ( ipAddress.isEmpty() )
535 ipAddress = u
"localhost"_s;
538 QCommandLineParser parser;
539 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
540 parser.addHelpOption();
542 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
543 parser.addOption( versionOption );
545 parser.addPositionalArgument(
548 "Address and port (default: \"localhost:8000\")\n"
549 "address and port can also be specified with the environment\n"
550 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT."
554 const QCommandLineOption logLevelOption(
557 "Log level (default: 0)\n"
565 parser.addOption( logLevelOption );
567 const QCommandLineOption projectOption(
570 "Path to a QGIS project file (*.qgs or *.qgz),\n"
571 "if specified it will override the query string MAP argument\n"
572 "and the QGIS_PROJECT_FILE environment variable."
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 ) };
614 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
618 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
626#ifdef HAVE_SERVER_PYTHON_PLUGINS
631 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
633 bool isTcpError =
false;
634 TcpServerThread::connect(
636 &TcpServerThread::serverError,
646 QueueMonitorThread queueMonitorThread;
647 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
648 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
650 server.
handleRequest( requestContext->request, requestContext->response );
651 SERVER_MUTEX.unlock();
655 delete requestContext;
656 SERVER_MUTEX.unlock();
659 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
660 tcpServerThread.emitResponseReady( requestContext );
662 delete requestContext;
668 auto exitHandler = [](
int signal ) {
669 std::cout << u
"Signal %1 received: quitting"_s.arg( signal ).toStdString() << std::endl;
674 signal( SIGTERM, exitHandler );
675 signal( SIGABRT, exitHandler );
676 signal( SIGINT, exitHandler );
677 signal( SIGPIPE, [](
int ) { std::cerr << u
"Signal SIGPIPE received: ignoring"_s.toStdString() << std::endl; } );
681 tcpServerThread.start();
682 queueMonitorThread.start();
684 QgsApplication::exec();
686 tcpServerThread.exit();
687 tcpServerThread.wait();
688 queueMonitorThread.stop();
689 REQUEST_WAIT_CONDITION.notify_all();
690 queueMonitorThread.wait();
693 return isTcpError ? 1 : 0;
696#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[])