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 )
119 QString message()
const
129class TcpServerWorker :
public QObject
134 TcpServerWorker(
const QString &ipAddress,
int port )
136 QHostAddress address { QHostAddress::AnyIPv4 };
137 address.setAddress( ipAddress );
139 if ( !mTcpServer.listen( address, port ) )
141 std::cerr << tr(
"Unable to start the server: %1." )
142 .arg( mTcpServer.errorString() )
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, [
this, ipAddress, port] {
159 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
161 mConnectionCounter++;
165 QString *incomingData =
new QString();
168 QObject *context {
new QObject };
171 auto connectionDeleter = [
this, clientConnection, incomingData]() {
172 clientConnection->deleteLater();
173 mConnectionCounter--;
178 QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
181 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
183 qDebug() <<
"Socket error #" << socketError;
184 }, Qt::QueuedConnection );
188 QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
190 while ( clientConnection->bytesAvailable() > 0 )
192 incomingData->append( clientConnection->readAll() );
198 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
199 if ( firstLinePos == -1 )
201 throw HttpException( u
"HTTP error finding protocol header"_s );
204 const QString firstLine { incomingData->left( firstLinePos ) };
205 const QStringList firstLinePieces { firstLine.split(
' ' ) };
206 if ( firstLinePieces.size() != 3 )
208 throw HttpException( u
"HTTP error splitting protocol header"_s );
211 const QString methodString { firstLinePieces.at( 0 ) };
214 if ( methodString ==
"GET" )
218 else if ( methodString ==
"POST" )
222 else if ( methodString ==
"HEAD" )
226 else if ( methodString ==
"PUT" )
230 else if ( methodString ==
"PATCH" )
234 else if ( methodString ==
"DELETE" )
240 throw HttpException( u
"HTTP error unsupported method: %1"_s.arg( methodString ) );
244 const QString protocol { firstLinePieces.at( 2 ) };
245 if ( protocol !=
"HTTP/1.0"_L1 && protocol !=
"HTTP/1.1"_L1 )
247 throw HttpException( u
"HTTP error unsupported protocol: %1"_s.arg( protocol ) );
252 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
254 if ( endHeadersPos == -1 )
256 throw HttpException( u
"HTTP error finding headers"_s );
259 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
261 for (
const auto &headerLine : httpHeaders )
263 const auto headerColonPos { headerLine.indexOf(
':' ) };
264 if ( headerColonPos > 0 )
266 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
270 const auto headersSize { endHeadersPos + 4 };
273 if ( headers.contains( u
"Content-Length"_s ) )
276 const int contentLength { headers.value( u
"Content-Length"_s ).toInt( &ok ) };
277 if ( ok && contentLength > incomingData->length() - headersSize )
288 QString url { qgetenv(
"REQUEST_URI" ) };
293 const QString path { firstLinePieces.at( 1 ) };
295 if ( headers.contains( u
"Host"_s ) )
297 url = u
"http://%1%2"_s.arg( headers.value( u
"Host"_s ), path );
301 url = u
"http://%1:%2%3"_s.arg( ipAddress ).arg( port ).arg( path );
306 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
308 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
310 auto requestContext =
new RequestContext {
312 firstLinePieces.join(
' ' ),
313 std::chrono::steady_clock::now(),
314 { url, method, headers, &data },
317 REQUEST_QUEUE_MUTEX.lock();
318 REQUEST_QUEUE.enqueue( requestContext );
319 REQUEST_QUEUE_MUTEX.unlock();
320 REQUEST_WAIT_CONDITION.notify_one();
323 catch ( HttpException &ex )
325 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
328 clientConnection->write( u
"HTTP/1.0 %1 %2\r\n"_s.arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
329 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
330 clientConnection->write(
"\r\n" );
331 clientConnection->write( ex.message().toUtf8() );
333 std::cout << u
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m"_s
334 .arg( clientConnection->peerAddress().toString() )
335 .arg( QDateTime::currentDateTime().toString() )
340 clientConnection->disconnectFromHost();
348 ~TcpServerWorker()
override
353 bool isListening()
const
361 void responseReady( RequestContext *requestContext )
363 std::unique_ptr<RequestContext> request { requestContext };
364 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
366 const auto &response { request->response };
367 const auto &clientConnection { request->clientConnection };
369 if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
371 std::cout <<
"Connection reset by peer" << std::endl;
376 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() ) )
378 std::cout <<
"Cannot write to output socket" << std::endl;
379 clientConnection->disconnectFromHost();
383 clientConnection->write( u
"Server: QGIS\r\n"_s.toUtf8() );
384 const auto responseHeaders { response.headers() };
385 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
387 clientConnection->write( u
"%1: %2\r\n"_s.arg( it.key(), it.value() ).toUtf8() );
389 clientConnection->write(
"\r\n" );
390 const QByteArray body { response.body() };
391 clientConnection->write( body );
394 std::cout << u
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m"_s
395 .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() ) )
400 clientConnection->disconnectFromHost();
404 QTcpServer mTcpServer;
405 qlonglong mConnectionCounter = 0;
406 bool mIsListening =
false;
410class TcpServerThread :
public QThread
415 TcpServerThread(
const QString &ipAddress,
const int port )
416 : mIpAddress( ipAddress )
421 void emitResponseReady( RequestContext *requestContext )
423 if ( requestContext->clientConnection )
424 emit responseReady( requestContext );
429 const TcpServerWorker worker( mIpAddress, mPort );
430 if ( !worker.isListening() )
437 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
444 void responseReady( RequestContext *requestContext );
453class QueueMonitorThread :
public QThread
462 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
463 REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] {
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
468 emit requestReady( REQUEST_QUEUE.dequeue() );
475 void requestReady( RequestContext *requestContext );
485 bool mIsRunning =
true;
488int main(
int argc,
char *argv[] )
499 const QString display { qgetenv(
"DISPLAY" ) };
500 bool withDisplay =
true;
501 if ( display.isEmpty() )
504 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
508 const QgsApplication app( argc, argv, withDisplay, QString(), u
"QGIS Development Server"_s );
512 QCoreApplication::setApplicationName(
"QGIS Development Server" );
513 QCoreApplication::setApplicationVersion( VERSION );
517 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
518 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
525 QFontDatabase fontDB;
529 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
531 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
533 if ( serverPort.isEmpty() )
535 serverPort = u
"8000"_s;
538 if ( ipAddress.isEmpty() )
540 ipAddress = u
"localhost"_s;
543 QCommandLineParser parser;
544 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
545 parser.addHelpOption();
547 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
548 parser.addOption( versionOption );
550 parser.addPositionalArgument( u
"addressAndPort"_s, QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
551 "address and port can also be specified with the environment\n"
552 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ),
553 u
"[address:port]"_s );
554 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
559 parser.addOption( logLevelOption );
561 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
562 "if specified it will override the query string MAP argument\n"
563 "and the QGIS_PROJECT_FILE environment variable." ),
565 parser.addOption( projectOption );
567 parser.process( app );
569 if ( parser.isSet( versionOption ) )
575 const QStringList args = parser.positionalArguments();
577 if ( args.size() == 1 )
579 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
580 if ( addressAndPort.size() == 2 )
582 ipAddress = addressAndPort.at( 0 );
584 serverPort = addressAndPort.at( 1 );
588 const QString logLevel = parser.value( logLevelOption );
589 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
590 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
591 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
595 if ( !parser.value( projectOption ).isEmpty() )
598 const QString projectFilePath { parser.value( projectOption ) };
601 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
605 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
613#ifdef HAVE_SERVER_PYTHON_PLUGINS
618 TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
620 bool isTcpError =
false;
621 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [&] {
623 qApp->quit(); }, Qt::QueuedConnection );
626 QueueMonitorThread queueMonitorThread;
627 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
628 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
630 server.
handleRequest( requestContext->request, requestContext->response );
631 SERVER_MUTEX.unlock();
635 delete requestContext;
636 SERVER_MUTEX.unlock();
639 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
640 tcpServerThread.emitResponseReady( requestContext );
642 delete requestContext;
648 auto exitHandler = [](
int signal ) {
649 std::cout << u
"Signal %1 received: quitting"_s.arg( signal ).toStdString() << std::endl;
654 signal( SIGTERM, exitHandler );
655 signal( SIGABRT, exitHandler );
656 signal( SIGINT, exitHandler );
657 signal( SIGPIPE, [](
int ) {
658 std::cerr << u
"Signal SIGPIPE received: ignoring"_s.toStdString() << std::endl;
663 tcpServerThread.start();
664 queueMonitorThread.start();
666 QgsApplication::exec();
668 tcpServerThread.exit();
669 tcpServerThread.wait();
670 queueMonitorThread.stop();
671 REQUEST_WAIT_CONDITION.notify_all();
672 queueMonitorThread.wait();
675 return isTcpError ? 1 : 0;
678#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[])