30 #include <condition_variable>
33 #include "qgsconfig.h"
40 #include <QFontDatabase>
44 #include <QNetworkInterface>
45 #include <QCommandLineParser>
58 QAtomicInt IS_RUNNING = 1;
63 std::condition_variable REQUEST_WAIT_CONDITION;
64 std::mutex REQUEST_QUEUE_MUTEX;
65 std::mutex SERVER_MUTEX;
69 QPointer<QTcpSocket> clientConnection;
71 std::chrono::steady_clock::time_point startTime;
77 QQueue<RequestContext *> REQUEST_QUEUE;
79 const QMap<int, QString> knownStatuses
81 { 200, QStringLiteral(
"OK" ) },
82 { 201, QStringLiteral(
"Created" ) },
83 { 202, QStringLiteral(
"Accepted" ) },
84 { 204, QStringLiteral(
"No Content" ) },
85 { 301, QStringLiteral(
"Moved Permanently" ) },
86 { 302, QStringLiteral(
"Moved Temporarily" ) },
87 { 304, QStringLiteral(
"Not Modified" ) },
88 { 400, QStringLiteral(
"Bad Request" ) },
89 { 401, QStringLiteral(
"Unauthorized" ) },
90 { 403, QStringLiteral(
"Forbidden" ) },
91 { 404, QStringLiteral(
"Not Found" ) },
92 { 500, QStringLiteral(
"Internal Server Error" ) },
93 { 501, QStringLiteral(
"Not Implemented" ) },
94 { 502, QStringLiteral(
"Bad Gateway" ) },
95 { 503, QStringLiteral(
"Service Unavailable" ) }
101 class HttpException:
public std::exception
109 HttpException(
const QString &message )
110 : mMessage( message )
129 class TcpServerWorker:
public QObject
135 TcpServerWorker(
const QString &ipAddress,
int port )
137 QHostAddress address { QHostAddress::AnyIPv4 };
138 address.setAddress( ipAddress );
140 if ( ! mTcpServer.listen( address, port ) )
142 std::cerr << tr(
"Unable to start the server: %1." )
143 .arg( mTcpServer.errorString() ).toStdString() << std::endl;
147 const int port { mTcpServer.serverPort() };
149 std::cout << tr(
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
151 std::cout << tr(
"CTRL+C to exit" ).toStdString() << std::endl;
157 mTcpServer.connect( &mTcpServer, &QTcpServer::newConnection,
this, [ = ]
159 QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
161 mConnectionCounter++;
165 QString *incomingData =
new QString();
168 QObject *context {
new QObject };
171 auto connectionDeleter = [ = ]()
173 clientConnection->deleteLater();
174 mConnectionCounter--;
179 clientConnection->connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
182 clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, [ = ]( QAbstractSocket::SocketError socketError )
184 qDebug() <<
"Socket error #" << socketError;
185 }, Qt::QueuedConnection );
189 clientConnection->connect( clientConnection, &QIODevice::readyRead, context, [ = ] {
192 while ( clientConnection->bytesAvailable() > 0 )
194 incomingData->append( clientConnection->readAll() );
200 int firstLinePos { incomingData->indexOf(
"\r\n" ) };
201 if ( firstLinePos == -1 )
203 throw HttpException( QStringLiteral(
"HTTP error finding protocol header" ) );
206 const QString firstLine { incomingData->left( firstLinePos ) };
207 const QStringList firstLinePieces { firstLine.split(
' ' ) };
208 if ( firstLinePieces.size() != 3 )
210 throw HttpException( QStringLiteral(
"HTTP error splitting protocol header" ) );
213 const QString methodString { firstLinePieces.at( 0 ) };
216 if ( methodString ==
"GET" )
218 method = QgsServerRequest::Method::GetMethod;
220 else if ( methodString ==
"POST" )
222 method = QgsServerRequest::Method::PostMethod;
224 else if ( methodString ==
"HEAD" )
226 method = QgsServerRequest::Method::HeadMethod;
228 else if ( methodString ==
"PUT" )
230 method = QgsServerRequest::Method::PutMethod;
232 else if ( methodString ==
"PATCH" )
234 method = QgsServerRequest::Method::PatchMethod;
236 else if ( methodString ==
"DELETE" )
238 method = QgsServerRequest::Method::DeleteMethod;
242 throw HttpException( QStringLiteral(
"HTTP error unsupported method: %1" ).arg( methodString ) );
245 const QString protocol { firstLinePieces.at( 2 )};
246 if ( protocol != QLatin1String(
"HTTP/1.0" ) && protocol != QLatin1String(
"HTTP/1.1" ) )
248 throw HttpException( QStringLiteral(
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
253 int endHeadersPos { incomingData->indexOf(
"\r\n\r\n" ) };
255 if ( endHeadersPos == -1 )
257 throw HttpException( QStringLiteral(
"HTTP error finding headers" ) );
260 const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split(
"\r\n" ) };
262 for (
const auto &headerLine : httpHeaders )
264 const int headerColonPos { headerLine.indexOf(
':' ) };
265 if ( headerColonPos > 0 )
267 headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
271 const int headersSize { endHeadersPos + 4 };
274 if ( headers.contains( QStringLiteral(
"Content-Length" ) ) )
277 const int contentLength { headers.value( QStringLiteral(
"Content-Length" ) ).toInt( &ok ) };
278 if ( ok && contentLength > incomingData->length() - headersSize )
289 QString url { qgetenv(
"REQUEST_URI" ) };
293 const QString path { firstLinePieces.at( 1 )};
295 if ( headers.contains( QStringLiteral(
"Host" ) ) )
297 url = QStringLiteral(
"http://%1%2" ).arg( headers.value( QStringLiteral(
"Host" ) ), path );
301 url = QStringLiteral(
"http://%1:%2%3" ).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
313 firstLinePieces.join(
' ' ),
314 std::chrono::steady_clock::now(),
315 { url, method, headers, &data },
318 REQUEST_QUEUE_MUTEX.lock();
319 REQUEST_QUEUE.enqueue( requestContext );
320 REQUEST_QUEUE_MUTEX.unlock();
321 REQUEST_WAIT_CONDITION.notify_one();
324 catch ( HttpException &ex )
326 if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
329 clientConnection->write( QStringLiteral(
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
330 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
331 clientConnection->write(
"\r\n" );
332 clientConnection->write( ex.message().toUtf8() );
334 std::cout << QStringLiteral(
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
335 .arg( clientConnection->peerAddress().toString() )
336 .arg( QDateTime::currentDateTime().toString() )
337 .arg( ex.message() ).toStdString() << std::endl;
339 clientConnection->disconnectFromHost();
352 bool isListening()
const
360 void responseReady( RequestContext *requestContext )
362 std::unique_ptr<RequestContext> request { requestContext };
363 auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
365 const auto &response { request->response };
366 const auto &clientConnection { request->clientConnection };
368 if ( ! clientConnection ||
369 clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
371 std::cout <<
"Connection reset by peer" << std::endl;
376 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() ) )
378 std::cout <<
"Cannot write to output socket" << std::endl;
379 clientConnection->disconnectFromHost();
383 clientConnection->write( QStringLiteral(
"Server: QGIS\r\n" ).toUtf8() );
384 const auto responseHeaders { response.headers() };
385 for (
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
387 clientConnection->write( QStringLiteral(
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
389 clientConnection->write(
"\r\n" );
390 const QByteArray body { response.body() };
391 clientConnection->write( body );
394 std::cout << QStringLiteral(
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
395 .arg( clientConnection->peerAddress().toString(),
396 QDateTime::currentDateTime().toString(),
397 QString::number( body.size() ),
398 QString::number( std::chrono::duration_cast<std::chrono::milliseconds>( elapsedTime ).count() ),
400 QString::number( response.statusCode() ) )
405 clientConnection->disconnectFromHost();
410 QTcpServer mTcpServer;
411 qlonglong mConnectionCounter = 0;
412 bool mIsListening =
false;
417 class TcpServerThread:
public QThread
423 TcpServerThread(
const QString &ipAddress,
const int port )
424 : mIpAddress( ipAddress )
429 void emitResponseReady( RequestContext *requestContext )
431 if ( requestContext->clientConnection )
432 emit responseReady( requestContext );
437 TcpServerWorker worker( mIpAddress, mPort );
438 if ( ! worker.isListening() )
445 connect(
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady );
452 void responseReady( RequestContext *requestContext );
462 class QueueMonitorThread:
public QThread
472 std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
473 REQUEST_WAIT_CONDITION.wait( requestLocker, [ = ] {
return ! mIsRunning || ! REQUEST_QUEUE.isEmpty(); } );
478 emit requestReady( REQUEST_QUEUE.dequeue() );
485 void requestReady( RequestContext *requestContext );
496 bool mIsRunning =
true;
500 int main(
int argc,
char *argv[] )
511 const QString display { qgetenv(
"DISPLAY" ) };
512 bool withDisplay =
true;
513 if ( display.isEmpty() )
516 qputenv(
"QT_QPA_PLATFORM",
"offscreen" );
520 QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral(
"QGIS Development Server" ) );
524 QCoreApplication::setApplicationName(
"QGIS Development Server" );
525 QCoreApplication::setApplicationVersion( VERSION );
529 QgsMessageLog::logMessage(
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n"
530 "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
"Server", Qgis::MessageLevel::Warning );
536 QFontDatabase fontDB;
540 serverPort = qgetenv(
"QGIS_SERVER_PORT" );
542 ipAddress = qgetenv(
"QGIS_SERVER_ADDRESS" );
544 if ( serverPort.isEmpty() )
546 serverPort = QStringLiteral(
"8000" );
549 if ( ipAddress.isEmpty() )
551 ipAddress = QStringLiteral(
"localhost" );
554 QCommandLineParser parser;
555 parser.setApplicationDescription( QObject::tr(
"QGIS Development Server %1" ).arg( VERSION ) );
556 parser.addHelpOption();
557 parser.addVersionOption();
558 parser.addPositionalArgument( QStringLiteral(
"addressAndPort" ),
559 QObject::tr(
"Address and port (default: \"localhost:8000\")\n"
560 "address and port can also be specified with the environment\n"
561 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ), QStringLiteral(
"[address:port]" ) );
562 QCommandLineOption logLevelOption(
"l", QObject::tr(
"Log level (default: 0)\n"
565 "2: CRITICAL" ),
"logLevel",
"0" );
566 parser.addOption( logLevelOption );
568 QCommandLineOption projectOption(
"p", QObject::tr(
"Path to a QGIS project file (*.qgs or *.qgz),\n"
569 "if specified it will override the query string MAP argument\n"
570 "and the QGIS_PROJECT_FILE environment variable." ),
"projectPath",
"" );
571 parser.addOption( projectOption );
573 parser.process( app );
574 const QStringList args = parser.positionalArguments();
576 if ( args.size() == 1 )
578 QStringList addressAndPort { args.at( 0 ).split(
':' ) };
579 if ( addressAndPort.size() == 2 )
581 ipAddress = addressAndPort.at( 0 );
582 serverPort = addressAndPort.at( 1 );
586 QString logLevel = parser.value( logLevelOption );
587 qunsetenv(
"QGIS_SERVER_LOG_FILE" );
588 qputenv(
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
589 qputenv(
"QGIS_SERVER_LOG_STDERR",
"1" );
593 if ( ! parser.value( projectOption ).isEmpty( ) )
596 const QString projectFilePath { parser.value( projectOption ) };
599 std::cout << QObject::tr(
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
603 qputenv(
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
611 #ifdef HAVE_SERVER_PYTHON_PLUGINS
616 TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
618 bool isTcpError =
false;
619 tcpServerThread.connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
623 }, Qt::QueuedConnection );
626 QueueMonitorThread queueMonitorThread;
627 queueMonitorThread.connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
629 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
631 server.handleRequest( requestContext->request, requestContext->response );
632 SERVER_MUTEX.unlock();
636 delete requestContext;
637 SERVER_MUTEX.unlock();
640 if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
641 tcpServerThread.emitResponseReady( requestContext );
643 delete requestContext;
649 auto exitHandler = [ ](
int signal )
651 std::cout << QStringLiteral(
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
656 signal( SIGTERM, exitHandler );
657 signal( SIGABRT, exitHandler );
658 signal( SIGINT, exitHandler );
659 signal( SIGPIPE, [ ](
int )
661 std::cerr << QStringLiteral(
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
666 tcpServerThread.start();
667 queueMonitorThread.start();
671 tcpServerThread.exit();
672 tcpServerThread.wait();
673 queueMonitorThread.stop();
674 REQUEST_WAIT_CONDITION.notify_all();
675 queueMonitorThread.wait();
678 return isTcpError ? 1 : 0;
681 #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 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[])