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 {
 
   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" ) }
 
  101class HttpException : 
public std::exception
 
  107    HttpException( 
const QString &message )
 
  108      : mMessage( message )
 
  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." )
 
  138                       .arg( mTcpServer.errorString() )
 
  144        const int port { mTcpServer.serverPort() };
 
  146        std::cout << tr( 
"QGIS Development Server listening on http://%1:%2" ).arg( ipAddress ).arg( port ).toStdString() << std::endl;
 
  148        std::cout << tr( 
"CTRL+C to exit" ).toStdString() << std::endl;
 
  154        QTcpServer::connect( &mTcpServer, &QTcpServer::newConnection, 
this, [
this, ipAddress, port] {
 
  155          QTcpSocket *clientConnection = mTcpServer.nextPendingConnection();
 
  157          mConnectionCounter++;
 
  161          QString *incomingData = 
new QString();
 
  164          QObject *context { 
new QObject };
 
  167          auto connectionDeleter = [
this, clientConnection, incomingData]() {
 
  168            clientConnection->deleteLater();
 
  169            mConnectionCounter--;
 
  174          QObject::connect( clientConnection, &QAbstractSocket::disconnected, clientConnection, connectionDeleter, Qt::QueuedConnection );
 
  177          clientConnection->connect( clientConnection, &QAbstractSocket::errorOccurred, clientConnection, []( QAbstractSocket::SocketError socketError )
 
  179            qDebug() << 
"Socket error #" << socketError;
 
  180          }, Qt::QueuedConnection );
 
  184          QObject::connect( clientConnection, &QIODevice::readyRead, context, [clientConnection, incomingData, context, ipAddress, port] {
 
  186            while ( clientConnection->bytesAvailable() > 0 )
 
  188              incomingData->append( clientConnection->readAll() );
 
  194              const auto firstLinePos { incomingData->indexOf( 
"\r\n" ) };
 
  195              if ( firstLinePos == -1 )
 
  197                throw HttpException( QStringLiteral( 
"HTTP error finding protocol header" ) );
 
  200              const QString firstLine { incomingData->left( firstLinePos ) };
 
  201              const QStringList firstLinePieces { firstLine.split( 
' ' ) };
 
  202              if ( firstLinePieces.size() != 3 )
 
  204                throw HttpException( QStringLiteral( 
"HTTP error splitting protocol header" ) );
 
  207              const QString methodString { firstLinePieces.at( 0 ) };
 
  210              if ( methodString == 
"GET" )
 
  214              else if ( methodString == 
"POST" )
 
  218              else if ( methodString == 
"HEAD" )
 
  222              else if ( methodString == 
"PUT" )
 
  226              else if ( methodString == 
"PATCH" )
 
  230              else if ( methodString == 
"DELETE" )
 
  236                throw HttpException( QStringLiteral( 
"HTTP error unsupported method: %1" ).arg( methodString ) );
 
  240              const QString protocol { firstLinePieces.at( 2 ) };
 
  241              if ( protocol != QLatin1String( 
"HTTP/1.0" ) && protocol != QLatin1String( 
"HTTP/1.1" ) )
 
  243                throw HttpException( QStringLiteral( 
"HTTP error unsupported protocol: %1" ).arg( protocol ) );
 
  248              const auto endHeadersPos { incomingData->indexOf( 
"\r\n\r\n" ) };
 
  250              if ( endHeadersPos == -1 )
 
  252                throw HttpException( QStringLiteral( 
"HTTP error finding headers" ) );
 
  255              const QStringList httpHeaders { incomingData->mid( firstLinePos + 2, endHeadersPos - firstLinePos ).split( 
"\r\n" ) };
 
  257              for ( 
const auto &headerLine : httpHeaders )
 
  259                const auto headerColonPos { headerLine.indexOf( 
':' ) };
 
  260                if ( headerColonPos > 0 )
 
  262                  headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
 
  266              const auto headersSize { endHeadersPos + 4 };
 
  269              if ( headers.contains( QStringLiteral( 
"Content-Length" ) ) )
 
  272                const int contentLength { headers.value( QStringLiteral( 
"Content-Length" ) ).toInt( &ok ) };
 
  273                if ( ok && contentLength > incomingData->length() - headersSize )
 
  284              QString url { qgetenv( 
"REQUEST_URI" ) };
 
  289                const QString path { firstLinePieces.at( 1 ) };
 
  291                if ( headers.contains( QStringLiteral( 
"Host" ) ) )
 
  293                  url = QStringLiteral( 
"http://%1%2" ).arg( headers.value( QStringLiteral( 
"Host" ) ), path );
 
  297                  url = QStringLiteral( 
"http://%1:%2%3" ).arg( ipAddress ).arg( port ).arg( path );
 
  302              QByteArray data { incomingData->mid( headersSize ).toUtf8() };
 
  304              if ( !incomingData->isEmpty() && clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
 
  306                auto requestContext = 
new RequestContext {
 
  308                  firstLinePieces.join( 
' ' ),
 
  309                  std::chrono::steady_clock::now(),
 
  310                  { url, method, headers, &data },
 
  313                REQUEST_QUEUE_MUTEX.lock();
 
  314                REQUEST_QUEUE.enqueue( requestContext );
 
  315                REQUEST_QUEUE_MUTEX.unlock();
 
  316                REQUEST_WAIT_CONDITION.notify_one();
 
  319            catch ( HttpException &ex )
 
  321              if ( clientConnection->state() == QAbstractSocket::SocketState::ConnectedState )
 
  324                clientConnection->write( QStringLiteral( 
"HTTP/1.0 %1 %2\r\n" ).arg( 500 ).arg( knownStatuses.value( 500 ) ).toUtf8() );
 
  325                clientConnection->write( QStringLiteral( 
"Server: QGIS\r\n" ).toUtf8() );
 
  326                clientConnection->write( 
"\r\n" );
 
  327                clientConnection->write( ex.message().toUtf8() );
 
  329                std::cout << QStringLiteral( 
"\033[1;31m%1 [%2] \"%3\" - - 500\033[0m" )
 
  330                               .arg( clientConnection->peerAddress().toString() )
 
  331                               .arg( QDateTime::currentDateTime().toString() )
 
  336                clientConnection->disconnectFromHost();
 
  349    bool isListening()
 const 
  357    void responseReady( RequestContext *requestContext ) 
 
  359      std::unique_ptr<RequestContext> request { requestContext };
 
  360      const auto elapsedTime { std::chrono::steady_clock::now() - request->startTime };
 
  362      const auto &response { request->response };
 
  363      const auto &clientConnection { request->clientConnection };
 
  365      if ( !clientConnection || clientConnection->state() != QAbstractSocket::SocketState::ConnectedState )
 
  367        std::cout << 
"Connection reset by peer" << std::endl;
 
  372      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() ) )
 
  374        std::cout << 
"Cannot write to output socket" << std::endl;
 
  375        clientConnection->disconnectFromHost();
 
  379      clientConnection->write( QStringLiteral( 
"Server: QGIS\r\n" ).toUtf8() );
 
  380      const auto responseHeaders { response.headers() };
 
  381      for ( 
auto it = responseHeaders.constBegin(); it != responseHeaders.constEnd(); ++it )
 
  383        clientConnection->write( QStringLiteral( 
"%1: %2\r\n" ).arg( it.key(), it.value() ).toUtf8() );
 
  385      clientConnection->write( 
"\r\n" );
 
  386      const QByteArray body { response.body() };
 
  387      clientConnection->write( body );
 
  390      std::cout << QStringLiteral( 
"\033[1;92m%1 [%2] %3 %4ms \"%5\" %6\033[0m" )
 
  391                     .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() ) )
 
  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 )
 
  417    void emitResponseReady( RequestContext *requestContext ) 
 
  419      if ( requestContext->clientConnection )
 
  420        emit responseReady( requestContext ); 
 
  425      const TcpServerWorker worker( mIpAddress, mPort );
 
  426      if ( !worker.isListening() )
 
  433        connect( 
this, &TcpServerThread::responseReady, &worker, &TcpServerWorker::responseReady ); 
 
  440    void responseReady( RequestContext *requestContext ); 
 
  449class QueueMonitorThread : 
public QThread
 
  458        std::unique_lock<std::mutex> requestLocker( REQUEST_QUEUE_MUTEX );
 
  459        REQUEST_WAIT_CONDITION.wait( requestLocker, [
this] { 
return !mIsRunning || !REQUEST_QUEUE.isEmpty(); } );
 
  464          emit requestReady( REQUEST_QUEUE.dequeue() );
 
  471    void requestReady( RequestContext *requestContext );
 
  481    bool mIsRunning = 
true;
 
  484int main( 
int argc, 
char *argv[] )
 
  495  const QString display { qgetenv( 
"DISPLAY" ) };
 
  496  bool withDisplay = 
true;
 
  497  if ( display.isEmpty() )
 
  500    qputenv( 
"QT_QPA_PLATFORM", 
"offscreen" );
 
  504  const QgsApplication app( argc, argv, withDisplay, QString(), QStringLiteral( 
"QGIS Development Server" ) );
 
  508  QCoreApplication::setApplicationName( 
"QGIS Development Server" );
 
  509  QCoreApplication::setApplicationVersion( VERSION );
 
  513    QgsMessageLog::logMessage( 
"DISPLAY environment variable is not set, running in offscreen mode, all printing capabilities will not be available.\n" 
  514                               "Consider installing an X server like 'xvfb' and export DISPLAY to the actual display value.",
 
  521  QFontDatabase fontDB;
 
  525  serverPort = qgetenv( 
"QGIS_SERVER_PORT" );
 
  527  ipAddress = qgetenv( 
"QGIS_SERVER_ADDRESS" );
 
  529  if ( serverPort.isEmpty() )
 
  531    serverPort = QStringLiteral( 
"8000" );
 
  534  if ( ipAddress.isEmpty() )
 
  536    ipAddress = QStringLiteral( 
"localhost" );
 
  539  QCommandLineParser parser;
 
  540  parser.setApplicationDescription( QObject::tr( 
"QGIS Development Server %1" ).arg( VERSION ) );
 
  541  parser.addHelpOption();
 
  543  const QCommandLineOption versionOption( QStringList() << 
"v" << 
"version", QObject::tr( 
"Version of QGIS and libraries" ) );
 
  544  parser.addOption( versionOption );
 
  546  parser.addPositionalArgument( QStringLiteral( 
"addressAndPort" ), QObject::tr( 
"Address and port (default: \"localhost:8000\")\n" 
  547                                                                                 "address and port can also be specified with the environment\n" 
  548                                                                                 "variables QGIS_SERVER_ADDRESS and QGIS_SERVER_PORT." ),
 
  549                                QStringLiteral( 
"[address:port]" ) );
 
  550  const QCommandLineOption logLevelOption( 
"l", QObject::tr( 
"Log level (default: 0)\n" 
  555  parser.addOption( logLevelOption );
 
  557  const QCommandLineOption projectOption( 
"p", QObject::tr( 
"Path to a QGIS project file (*.qgs or *.qgz),\n" 
  558                                                            "if specified it will override the query string MAP argument\n" 
  559                                                            "and the QGIS_PROJECT_FILE environment variable." ),
 
  561  parser.addOption( projectOption );
 
  563  parser.process( app );
 
  565  if ( parser.isSet( versionOption ) )
 
  571  const QStringList args = parser.positionalArguments();
 
  573  if ( args.size() == 1 )
 
  575    const QStringList addressAndPort { args.at( 0 ).split( 
':' ) };
 
  576    if ( addressAndPort.size() == 2 )
 
  578      ipAddress = addressAndPort.at( 0 );
 
  580      serverPort = addressAndPort.at( 1 );
 
  584  const QString logLevel = parser.value( logLevelOption );
 
  585  qunsetenv( 
"QGIS_SERVER_LOG_FILE" );
 
  586  qputenv( 
"QGIS_SERVER_LOG_LEVEL", logLevel.toUtf8() );
 
  587  qputenv( 
"QGIS_SERVER_LOG_STDERR", 
"1" );
 
  591  if ( !parser.value( projectOption ).isEmpty() )
 
  594    const QString projectFilePath { parser.value( projectOption ) };
 
  597      std::cout << QObject::tr( 
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
 
  601      qputenv( 
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
 
  609#ifdef HAVE_SERVER_PYTHON_PLUGINS 
  614  TcpServerThread tcpServerThread { ipAddress, serverPort.toInt() };
 
  616  bool isTcpError = 
false;
 
  617  TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [&] {
 
  619    qApp->quit(); }, Qt::QueuedConnection );
 
  622  QueueMonitorThread queueMonitorThread;
 
  623  QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [&]( RequestContext *requestContext ) {
 
  624    if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  626      server.handleRequest( requestContext->request, requestContext->response );
 
  627      SERVER_MUTEX.unlock();
 
  631      delete requestContext;
 
  632      SERVER_MUTEX.unlock();
 
  635    if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  636      tcpServerThread.emitResponseReady( requestContext ); 
 
  638      delete requestContext;
 
  644  auto exitHandler = []( 
int signal ) {
 
  645    std::cout << QStringLiteral( 
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
 
  650  signal( SIGTERM, exitHandler );
 
  651  signal( SIGABRT, exitHandler );
 
  652  signal( SIGINT, exitHandler );
 
  653  signal( SIGPIPE, []( 
int ) {
 
  654    std::cerr << QStringLiteral( 
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
 
  659  tcpServerThread.start();
 
  660  queueMonitorThread.start();
 
  662  QgsApplication::exec();
 
  664  tcpServerThread.exit();
 
  665  tcpServerThread.wait();
 
  666  queueMonitorThread.stop();
 
  667  REQUEST_WAIT_CONDITION.notify_all();
 
  668  queueMonitorThread.wait();
 
  671  return isTcpError ? 1 : 0;
 
  674#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
 
Defines a request with data.
 
Defines a buffered server 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, 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.
 
int main(int argc, char *argv[])