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
82 { 200, QStringLiteral(
"OK" ) },
83 { 201, QStringLiteral(
"Created" ) },
84 { 202, QStringLiteral(
"Accepted" ) },
85 { 204, QStringLiteral(
"No Content" ) },
86 { 301, QStringLiteral(
"Moved Permanently" ) },
87 { 302, QStringLiteral(
"Moved Temporarily" ) },
88 { 304, QStringLiteral(
"Not Modified" ) },
89 { 400, QStringLiteral(
"Bad Request" ) },
90 { 401, QStringLiteral(
"Unauthorized" ) },
91 { 403, QStringLiteral(
"Forbidden" ) },
92 { 404, QStringLiteral(
"Not Found" ) },
93 { 500, QStringLiteral(
"Internal Server Error" ) },
94 { 501, QStringLiteral(
"Not Implemented" ) },
95 { 502, QStringLiteral(
"Bad Gateway" ) },
96 { 503, QStringLiteral(
"Service Unavailable" ) }
102class HttpException:
public std::exception
110 HttpException(
const QString &message )
111 : mMessage( message )
130class TcpServerWorker:
public QObject
136 TcpServerWorker(
const QString &ipAddress,
int port )
138 QHostAddress address { QHostAddress::AnyIPv4 };
139 address.setAddress( ipAddress );
141 if ( ! mTcpServer.listen( address, port ) )
143 std::cerr << tr(
"Unable to start the server: %1." )
144 .arg( mTcpServer.errorString() ).toStdString() << std::endl;
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, [ = ]
160 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
162 mConnectionCounter++;
166 QString *incomingData =
new QString();
169 QObject *context {
new QObject };
172 auto connectionDeleter = [ = ]()
174 clientConnection->deleteLater();
175 mConnectionCounter--;
180 QTcpSocket::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
183 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
185 qDebug() <<
"Socket error #" << socketError;
186 }, Qt::QueuedConnection );
190 QTcpSocket::connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
193 while ( clientConnection->bytesAvailable() > 0 )
195 incomingData->append( clientConnection->readAll() );
201 const auto firstLinePos { incomingData->indexOf(
"\r\n" ) };
202 if ( firstLinePos == -1 )
204 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
207 const QString firstLine { incomingData->left( firstLinePos ) };
208 const QStringList firstLinePieces { firstLine.split(
' ' ) };
209 if ( firstLinePieces.size() != 3 )
211 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
214 const QString methodString { firstLinePieces.at( 0 ) };
217 if ( methodString ==
"GET" )
219 method = QgsServerRequest::Method::GetMethod;
221 else if ( methodString ==
"POST" )
223 method = QgsServerRequest::Method::PostMethod;
225 else if ( methodString ==
"HEAD" )
227 method = QgsServerRequest::Method::HeadMethod;
229 else if ( methodString ==
"PUT" )
231 method = QgsServerRequest::Method::PutMethod;
233 else if ( methodString ==
"PATCH" )
235 method = QgsServerRequest::Method::PatchMethod;
237 else if ( methodString ==
"DELETE" )
239 method = QgsServerRequest::Method::DeleteMethod;
243 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
247 const QString protocol { firstLinePieces.at( 2 )};
248 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
250 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
255 const auto endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
257 if ( endHeadersPos == -1 )
259 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
262 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
264 for (
const auto &headerLine : httpHeaders )
266 const auto headerColonPos { headerLine.indexOf(
':' ) };
267 if ( headerColonPos > 0 )
269 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
273 const auto headersSize { endHeadersPos + 4 };
276 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
279 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
280 if ( ok && contentLength > incomingData->length() - headersSize )
291 QString url { qgetenv(
"REQUEST_URI" ) };
296 const QString path { firstLinePieces.at( 1 )};
298 if ( headers.contains( QStringLiteral(
"Host" ) ) )
300 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
304 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
309 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
311 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
313 auto requestContext =
new RequestContext
316 firstLinePieces.join(
' ' ),
317 std::chrono::steady_clock::now(),
318 { url, method, headers, &data },
321 REQUEST_QUEUE_MUTEX.lock();
322 REQUEST_QUEUE.enqueue( requestContext );
323 REQUEST_QUEUE_MUTEX.unlock();
324 REQUEST_WAIT_CONDITION.notify_one();
327 catch ( HttpException &ex )
329 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
332 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
333 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
334 clientConnection->write(
"\r\n" );
335 clientConnection->write( ex.message().toUtf8() );
337 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
338 .arg( clientConnection->peerAddress().toString() )
339 .arg( QDateTime::currentDateTime().toString() )
340 .arg( ex.message() ).toStdString() << std::endl;
342 clientConnection->disconnectFromHost();
355 bool isListening()
const
363 void responseReady( RequestContext *requestContext )
365 std::unique_ptr<RequestContext> request { requestContext };
366 const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
368 const auto &response { request->response };
369 const auto &clientConnection { request->clientConnection };
371 if ( ! clientConnection ||
372 clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
374 std::cout <<
"Connection reset by peer" << std::endl;
379 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() ) )
381 std::cout <<
"Cannot write to output socket" << std::endl;
382 clientConnection->disconnectFromHost();
386 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
387 const auto responseHeaders { response.headers() };
388 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
390 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
392 clientConnection->write(
"\r\n" );
393 const QByteArray body { response.body() };
394 clientConnection->write( body );
397 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
398 .arg( clientConnection->peerAddress().toString(),
399 QDateTime::currentDateTime().toString(),
400 QString::number( body.size() ),
401 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
403 QString::number( response.statusCode() ) )
408 clientConnection->disconnectFromHost();
413 QTcpServer mTcpServer;
414 qlonglong mConnectionCounter = 0;
415 bool mIsListening =
false;
420class TcpServerThread:
public QThread
426 TcpServerThread(
const QString &ipAddress,
const int port )
427 : mIpAddress( ipAddress )
432 void emitResponseReady( RequestContext *requestContext )
434 if ( requestContext->clientConnection )
435 emit responseReady( requestContext );
440 const TcpServerWorker worker( mIpAddress, mPort );
441 if ( ! worker.isListening() )
448 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
455 void responseReady( RequestContext *requestContext );
465class QueueMonitorThread:
public QThread
475 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
476 REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] {
return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
481 emit requestReady( REQUEST_QUEUE.dequeue() );
488 void requestReady( RequestContext *requestContext );
499 bool mIsRunning =
true;
503int main(
int argc,
char *argv[] )
514 const QString display { qgetenv(
"DISPLAY" ) };
515 bool withDisplay =
true;
516 if ( display.isEmpty() )
519 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
523 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
527 QCoreApplication::setApplicationName(
"QGIS Development Server" );
528 QCoreApplication::setApplicationVersion( VERSION );
532 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
533 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
"Server", Qgis::MessageLevel::Warning );
539 QFontDatabase fontDB;
543 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
545 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
547 if ( serverPort.isEmpty() )
549 serverPort = QStringLiteral(
"8000" );
552 if ( ipAddress.isEmpty() )
554 ipAddress = QStringLiteral(
"localhost" );
557 QCommandLineParser parser;
558 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
559 parser.addHelpOption();
561 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
562 parser.addOption( versionOption );
564 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ),
565 QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
566 "address and port can also be specified with the environment\n"
567 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral(
"[address:port]" ) );
568 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
571 "2: CRITICAL" ),
"logLevel",
"0" );
572 parser.addOption( logLevelOption );
574 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
575 "if specified it will override the query string MAP argument\n"
576 "and the QGIS_PROJECT_FILE environment variable." ),
"projectPath",
"" );
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 ) };
612 Qgis::ProjectReadFlag::DontResolveLayers
613 | Qgis::ProjectReadFlag::DontLoadLayouts
614 | Qgis::ProjectReadFlag::DontStoreOriginalStyles
615 | Qgis::ProjectReadFlag::DontLoad3DViews ) )
617 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
621 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
629#ifdef HAVE_SERVER_PYTHON_PLUGINS
634 TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
636 bool isTcpError =
false;
637 TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
641 }, Qt::QueuedConnection );
644 QueueMonitorThread queueMonitorThread;
645 QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
647 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
649 server.handleRequest( requestContext->request, requestContext->response );
650 SERVER_MUTEX.unlock();
654 delete requestContext;
655 SERVER_MUTEX.unlock();
658 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
659 tcpServerThread.emitResponseReady( requestContext );
661 delete requestContext;
667 auto exitHandler = [ ](
int signal )
669 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
674 signal( SIGTERM, exitHandler );
675 signal( SIGABRT, exitHandler );
676 signal( SIGINT, exitHandler );
677 signal( SIGPIPE, [ ](
int )
679 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
684 tcpServerThread.start();
685 queueMonitorThread.start();
687 QgsApplication::exec();
689 tcpServerThread.exit();
690 tcpServerThread.wait();
691 queueMonitorThread.stop();
692 REQUEST_WAIT_CONDITION.notify_all();
693 queueMonitorThread.wait();
696 return isTcpError ? 1 : 0;
699#include "qgis_mapserver.moc"
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[])