30 #include <condition_variable>
34 #include "qgsconfig.h"
41 #include <QFontDatabase>
45 #include <QNetworkInterface>
46 #include <QCommandLineParser>
59 QAtomicInt IS_RUNNING = 1;
64 std::condition_variable REQUEST_WAIT_CONDITION;
65 std::mutex REQUEST_QUEUE_MUTEX;
66 std::mutex SERVER_MUTEX;
70 QPointer<QTcpSocket> clientConnection;
72 std::chrono::steady_clock::time_point startTime;
78 QQueue<RequestContext *> REQUEST_QUEUE;
80 const 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" ) }
102 class HttpException:
public std::exception
110 HttpException(
const QString &message )
111 : mMessage( message )
130 class 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 mTcpServer.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 clientConnection->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 clientConnection->connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
193 while ( clientConnection->bytesAvailable() > 0 )
195 incomingData->append( clientConnection->readAll() );
201 const int 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 ) );
246 const QString protocol { firstLinePieces.at( 2 )};
247 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
249 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
254 const int endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
256 if ( endHeadersPos == -1 )
258 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
261 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
263 for (
const auto &headerLine : httpHeaders )
265 const int headerColonPos { headerLine.indexOf(
':' ) };
266 if ( headerColonPos > 0 )
268 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
272 const int headersSize { endHeadersPos + 4 };
275 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
278 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
279 if ( ok && contentLength > incomingData->length() - headersSize )
290 QString url { qgetenv(
"REQUEST_URI" ) };
294 const QString path { firstLinePieces.at( 1 )};
296 if ( headers.contains( QStringLiteral(
"Host" ) ) )
298 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
302 url = QStringLiteral(
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
307 QByteArray data { incomingData->mid( headersSize ).toUtf8() };
309 if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
311 auto requestContext =
new RequestContext
314 firstLinePieces.join(
' ' ),
315 std::chrono::steady_clock::now(),
316 { url, method, headers, &data },
319 REQUEST_QUEUE_MUTEX.lock();
320 REQUEST_QUEUE.enqueue( requestContext );
321 REQUEST_QUEUE_MUTEX.unlock();
322 REQUEST_WAIT_CONDITION.notify_one();
325 catch ( HttpException &ex )
327 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
330 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
331 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
332 clientConnection->write(
"\r\n" );
333 clientConnection->write( ex.message().toUtf8() );
335 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
336 .arg( clientConnection->peerAddress().toString() )
337 .arg( QDateTime::currentDateTime().toString() )
338 .arg( ex.message() ).toStdString() << std::endl;
340 clientConnection->disconnectFromHost();
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 ||
370 clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
372 std::cout <<
"Connection reset by peer" << std::endl;
377 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() ) )
379 std::cout <<
"Cannot write to output socket" << std::endl;
380 clientConnection->disconnectFromHost();
384 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
385 const auto responseHeaders { response.headers() };
386 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
388 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
390 clientConnection->write(
"\r\n" );
391 const QByteArray body { response.body() };
392 clientConnection->write( body );
395 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
396 .arg( clientConnection->peerAddress().toString(),
397 QDateTime::currentDateTime().toString(),
398 QString::number( body.size() ),
399 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
401 QString::number( response.statusCode() ) )
406 clientConnection->disconnectFromHost();
411 QTcpServer mTcpServer;
412 qlonglong mConnectionCounter = 0;
413 bool mIsListening =
false;
418 class TcpServerThread:
public QThread
424 TcpServerThread(
const QString &ipAddress,
const int port )
425 : mIpAddress( ipAddress )
430 void emitResponseReady( RequestContext *requestContext )
432 if ( requestContext->clientConnection )
433 emit responseReady( requestContext );
438 const TcpServerWorker worker( mIpAddress, mPort );
439 if ( ! worker.isListening() )
446 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
453 void responseReady( RequestContext *requestContext );
463 class QueueMonitorThread:
public QThread
473 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
474 REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] {
return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
479 emit requestReady( REQUEST_QUEUE.dequeue() );
486 void requestReady( RequestContext *requestContext );
497 bool mIsRunning =
true;
501 int main(
int argc,
char *argv[] )
512 const QString display { qgetenv(
"DISPLAY" ) };
513 bool withDisplay =
true;
514 if ( display.isEmpty() )
517 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
521 const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
525 QCoreApplication::setApplicationName(
"QGIS Development Server" );
526 QCoreApplication::setApplicationVersion( VERSION );
530 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
531 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
"Server", Qgis::MessageLevel::Warning );
537 QFontDatabase fontDB;
541 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
543 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
545 if ( serverPort.isEmpty() )
547 serverPort = QStringLiteral(
"8000" );
550 if ( ipAddress.isEmpty() )
552 ipAddress = QStringLiteral(
"localhost" );
555 QCommandLineParser parser;
556 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
557 parser.addHelpOption();
559 const QCommandLineOption versionOption( QStringList() <<
"v" <<
"version", QObject::tr(
"Version of QGIS and libraries" ) );
560 parser.addOption( versionOption );
562 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ),
563 QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
564 "address and port can also be specified with the environment\n"
565 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral(
"[address:port]" ) );
566 const QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
569 "2: CRITICAL" ),
"logLevel",
"0" );
570 parser.addOption( logLevelOption );
572 const QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
573 "if specified it will override the query string MAP argument\n"
574 "and the QGIS_PROJECT_FILE environment variable." ),
"projectPath",
"" );
575 parser.addOption( projectOption );
577 parser.process( app );
579 if ( parser.isSet( versionOption ) )
585 const QStringList args = parser.positionalArguments();
587 if ( args.size() == 1 )
589 const QStringList addressAndPort { args.at( 0 ).split(
':' ) };
590 if ( addressAndPort.size() == 2 )
592 ipAddress = addressAndPort.at( 0 );
593 serverPort = addressAndPort.at( 1 );
597 const QString logLevel = parser.value( logLevelOption );
598 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
599 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
600 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
604 if ( ! parser.value( projectOption ).isEmpty( ) )
607 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( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
634 }, Qt::QueuedConnection );
637 QueueMonitorThread queueMonitorThread;
638 queueMonitorThread.connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
640 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
642 server.handleRequest( requestContext->request, requestContext->response );
643 SERVER_MUTEX.unlock();
647 delete requestContext;
648 SERVER_MUTEX.unlock();
651 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
652 tcpServerThread.emitResponseReady( requestContext );
654 delete requestContext;
660 auto exitHandler = [ ](
int signal )
662 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
667 signal( SIGTERM, exitHandler );
668 signal( SIGABRT, exitHandler );
669 signal( SIGINT, exitHandler );
670 signal( SIGPIPE, [ ](
int )
672 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
677 tcpServerThread.start();
678 queueMonitorThread.start();
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"
Extends QApplication to provide access to QGIS specific resources such as theme paths,...
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.
@ FlagDontStoreOriginalStyles
Skip the initial XML style storage for layers. Useful for minimising project load times in non-intera...
@ FlagDontLoadLayouts
Don't load print layouts. Improves project read time if layouts are not required, and allows projects...
@ FlagDontResolveLayers
Don't resolve layer paths (i.e. don't load any layer content). Dramatically improves project read tim...
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[])