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 ) );
 
  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 auto 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 auto headerColonPos { headerLine.indexOf( 
':' ) };
 
  266                if ( headerColonPos > 0 )
 
  268                  headers.insert( headerLine.left( headerColonPos ), headerLine.mid( headerColonPos + 2 ) );
 
  272              const auto 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;
 
  418class 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 );  
 
  463class 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;
 
  501int 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 ) };
 
  609                                         Qgis::ProjectReadFlag::DontResolveLayers
 
  610                                         | Qgis::ProjectReadFlag::DontLoadLayouts
 
  611                                         | Qgis::ProjectReadFlag::DontStoreOriginalStyles
 
  612                                         | Qgis::ProjectReadFlag::DontLoad3DViews ) )
 
  614      std::cout << QObject::tr( 
"Project file not found, the option will be ignored." ).toStdString() << std::endl;
 
  618      qputenv( 
"QGIS_PROJECT_FILE", projectFilePath.toUtf8() );
 
  626#ifdef HAVE_SERVER_PYTHON_PLUGINS 
  631  TcpServerThread tcpServerThread{ ipAddress, serverPort.toInt() };
 
  633  bool isTcpError = 
false;
 
  634  TcpServerThread::connect( &tcpServerThread, &TcpServerThread::serverError, qApp, [ & ]
 
  638  }, Qt::QueuedConnection );
 
  641  QueueMonitorThread queueMonitorThread;
 
  642  QueueMonitorThread::connect( &queueMonitorThread, &QueueMonitorThread::requestReady, qApp, [ & ]( RequestContext * requestContext )
 
  644    if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  646      server.handleRequest( requestContext->request, requestContext->response );
 
  647      SERVER_MUTEX.unlock();
 
  651      delete requestContext;
 
  652      SERVER_MUTEX.unlock();
 
  655    if ( requestContext->clientConnection && requestContext->clientConnection->isValid() )
 
  656      tcpServerThread.emitResponseReady( requestContext );  
 
  658      delete requestContext;
 
  664  auto exitHandler = [ ]( 
int signal )
 
  666    std::cout << QStringLiteral( 
"Signal %1 received: quitting" ).arg( signal ).toStdString() << std::endl;
 
  671  signal( SIGTERM, exitHandler );
 
  672  signal( SIGABRT, exitHandler );
 
  673  signal( SIGINT, exitHandler );
 
  674  signal( SIGPIPE, [ ]( 
int )
 
  676    std::cerr << QStringLiteral( 
"Signal SIGPIPE received: ignoring" ).toStdString() << std::endl;
 
  681  tcpServerThread.start();
 
  682  queueMonitorThread.start();
 
  684  QgsApplication::exec();
 
  686  tcpServerThread.exit();
 
  687  tcpServerThread.wait();
 
  688  queueMonitorThread.stop();
 
  689  REQUEST_WAIT_CONDITION.notify_all();
 
  690  queueMonitorThread.wait();
 
  693  return isTcpError ? 1 : 0;
 
  696#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[])