diff --git a/engine/script/scriptapi.cpp b/engine/script/scriptapi.cpp index 32b4aa6..dee991f 100644 --- a/engine/script/scriptapi.cpp +++ b/engine/script/scriptapi.cpp @@ -1,2380 +1,2381 @@ /* * Copyright 2012 Friedrich Pülz * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "scriptapi.h" // Own includes #include "config.h" #include "global.h" #include "serviceproviderglobal.h" // KDE includes #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Other includes #include #include namespace ScriptApi { NetworkRequest::NetworkRequest( QObject *parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_network(0), m_isFinished(false), m_request(0), m_reply(0) { } NetworkRequest::NetworkRequest( const QString &url, const QString &userUrl, Network *network, QObject* parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_url(url), m_userUrl(userUrl.isEmpty() ? url : userUrl), m_network(network), m_isFinished(false), m_request(new QNetworkRequest(url)), m_reply(0) { } NetworkRequest::~NetworkRequest() { abort(); delete m_request; delete m_mutex; } void NetworkRequest::abort() { const bool timedOut = qobject_cast< QTimer* >( sender() ); if ( !isRunning() ) { if ( timedOut ) { kDebug() << "Timeout, but request already finished"; } return; } // isRunning() returned true => m_reply != 0 - m_mutex->lockInline(); + m_mutex->lock(); disconnect( m_reply, 0, this, 0 ); m_reply->abort(); m_reply->deleteLater(); m_reply = 0; const QString url = m_url; const QVariant userData = m_userData; - m_mutex->unlockInline(); + m_mutex->unlock(); emit aborted( timedOut ); emit finished( QByteArray(), true, i18nc("@info/plain", "The request was aborted"), -1, 0, url, userData ); } void NetworkRequest::slotReadyRead() { - m_mutex->lockInline(); + m_mutex->lock(); if ( !m_reply ) { // Prevent crashes on exit - m_mutex->unlockInline(); + m_mutex->unlock(); qWarning() << "Reply object already deleted, aborted?"; return; } // Read all data, decode it and give it to the script QByteArray data = m_reply->readAll(); m_data.append( data ); if ( data.isEmpty() ) { qWarning() << "Error downloading" << m_url << m_reply->errorString(); } - m_mutex->unlockInline(); + m_mutex->unlock(); emit readyRead( data ); } QByteArray gzipDecompress( QByteArray compressData ) { // Strip header and trailer compressData.remove( 0, 10 ); compressData.chop( 12 ); // FIXME Decompress in one chunk, because otherwise it fails with error -3 "distance too far back", // estimate size of uncompressed data, expect that the data was compressed at best to 20% size, // limit to 512KB const int chunkSize = qMin( int(compressData.size() / 0.2), 512 * 1024 ); kDebug() << "Chunk size:" << chunkSize; if ( chunkSize == 512 * 1024 ) { qWarning() << "Maximum chunk size for decompression reached, may fail"; } unsigned char buffer[ chunkSize ]; z_stream stream; stream.next_in = (Bytef*)(compressData.data()); stream.avail_in = compressData.size(); stream.total_in = 0; stream.total_out = 0; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; if ( inflateInit2(&stream, -8) != Z_OK ) { kDebug() << "cmpr_stream error!"; return QByteArray(); } QByteArray uncompressed; do { stream.next_out = buffer; stream.avail_out = chunkSize; int status = inflate( &stream, Z_SYNC_FLUSH ); if ( status == Z_OK || status == Z_STREAM_END ) { uncompressed.append( QByteArray::fromRawData( (char*)buffer, chunkSize - stream.avail_out) ); if ( status == Z_STREAM_END ) { break; } } else { qWarning() << "Error while decompressing" << status << stream.msg; if ( status == Z_NEED_DICT || status == Z_DATA_ERROR || status == Z_MEM_ERROR ) { inflateEnd( &stream ); } return QByteArray(); } } while( stream.avail_out == 0 ); inflateEnd( &stream ); return uncompressed; } void NetworkRequest::slotFinished() { - m_mutex->lockInline(); + m_mutex->lock(); if ( !m_reply ) { // Prevent crashes on exit - m_mutex->unlockInline(); + m_mutex->unlock(); qWarning() << "Reply object already deleted, aborted?"; return; } if ( m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid() ) { if ( m_redirectUrl.isValid() ) { qWarning() << "Only one redirection allowed, from" << m_url << "to" << m_redirectUrl; qWarning() << "New redirection to" << m_reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); } else { m_redirectUrl = m_reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl(); DEBUG_NETWORK("Redirection to" << m_redirectUrl); // Delete the reply m_reply->deleteLater(); m_reply = 0; delete m_request; m_request = new QNetworkRequest( m_redirectUrl ); const QUrl redirectUrl = m_redirectUrl; - m_mutex->unlockInline(); + m_mutex->unlock(); emit redirected( redirectUrl ); return; } } const int size = m_reply->size(); const int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); // Read all data, decode it and give it to the script m_data.append( m_reply->readAll() ); if ( m_data.isEmpty() ) { qWarning() << "Error downloading" << m_url << (m_reply ? m_reply->errorString() : "Reply already deleted"); } // Check if the data is compressed and was not decompressed by QNetworkAccessManager if ( m_data.length() >= 2 && quint8(m_data[0]) == 0x1f && quint8(m_data[1]) == 0x8b ) { // Data is compressed, uncompress it // NOTE qUncompress() did not work here, "invalid zip" m_data = gzipDecompress( m_data ); DEBUG_NETWORK("Uncompressed data from" << size << "Bytes to" << m_data.size() << "Bytes, ratio:" << (100 - (m_data.isEmpty() ? 100 : qRound(100 * size / m_data.size()))) << '%'); } DEBUG_NETWORK("Request finished" << m_reply->url()); if ( m_reply->url().isEmpty() ) { qWarning() << "Empty URL in QNetworkReply!"; } const bool hasError = m_reply->error() != QNetworkReply::NoError; const QString errorString = m_reply->errorString(); m_reply->deleteLater(); m_reply = 0; m_isFinished = true; const QByteArray data = m_data; const QString url = m_url; const QVariant userData = m_userData; - m_mutex->unlockInline(); + m_mutex->unlock(); emit finished( data, hasError, errorString, statusCode, size, url, userData ); } void NetworkRequest::started( QNetworkReply* reply, int timeout ) { - m_mutex->lockInline(); + m_mutex->lock(); if ( !m_network ) { qWarning() << "Can't decode, no m_network given..."; - m_mutex->unlockInline(); + m_mutex->unlock(); return; } m_data.clear(); m_reply = reply; if ( timeout > 0 ) { QTimer::singleShot( timeout, this, SLOT(abort()) ); } // Connect to signals of reply only when the associated signals of this class are connected if ( receivers(SIGNAL(readyRead(QByteArray))) > 0 ) { connect( m_reply, SIGNAL(readyRead()), this, SLOT(slotReadyRead()) ); } connect( m_reply, SIGNAL(finished()), this, SLOT(slotFinished()) ); - m_mutex->unlockInline(); + m_mutex->unlock(); emit started(); } bool NetworkRequest::isValid() const { QMutexLocker locker( m_mutex ); if ( m_request ) { return true; } else { // Default constructor used, not available to scripts kDebug() << "Request is invalid"; return false; } } QByteArray NetworkRequest::getCharset( const QString& charset ) const { QMutexLocker locker( m_mutex ); QByteArray baCharset; if ( charset.isEmpty() ) { // No charset given, use the one specified in the ContentType header baCharset = m_request->header( QNetworkRequest::ContentTypeHeader ).toByteArray(); // No ContentType header, use utf8 if ( baCharset.isEmpty() ) { baCharset = "utf8"; } } else { baCharset = charset.toUtf8(); } return baCharset; } QByteArray NetworkRequest::postDataByteArray() const { QMutexLocker locker( m_mutex ); return m_postData; } void NetworkRequest::setPostData( const QString& postData, const QString& charset ) { if ( !isValid() ) { return; } if ( isRunning() ) { kDebug() << "Cannot set POST data for an already running request!"; return; } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); if ( codec ) { m_request->setHeader( QNetworkRequest::ContentTypeHeader, baCharset ); m_postData = codec->fromUnicode( postData ); } else { kDebug() << "Codec" << baCharset << "couldn't be found to encode the data " "to post, now using UTF-8"; m_request->setHeader( QNetworkRequest::ContentTypeHeader, "utf8" ); m_postData = postData.toUtf8(); } } QString NetworkRequest::header( const QString &header, const QString& charset ) const { if ( !isValid() ) { return QString(); } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); return m_request->rawHeader( codec ? codec->fromUnicode(header) : header.toUtf8() ); } void NetworkRequest::setHeader( const QString& header, const QString& value, const QString& charset ) { if ( !isValid() ) { return; } if ( isRunning() ) { kDebug() << "Cannot set headers for an already running request!"; return; } QByteArray baCharset = getCharset( charset ); QTextCodec *codec = QTextCodec::codecForName( baCharset ); QMutexLocker locker( m_mutex ); if ( codec ) { m_request->setRawHeader( codec->fromUnicode(header), codec->fromUnicode(value) ); } else { kDebug() << "Codec" << baCharset << "couldn't be found to encode the data " "to post, now using UTF-8"; m_request->setRawHeader( header.toUtf8(), value.toUtf8() ); } } QNetworkRequest* NetworkRequest::request() const { QMutexLocker locker( m_mutex ); return m_request; } Network::Network( const QByteArray &fallbackCharset, QObject* parent ) : QObject(parent), m_mutex(new QMutex(QMutex::Recursive)), m_fallbackCharset(fallbackCharset), m_manager(new QNetworkAccessManager()), m_quit(false), m_synchronousRequestCount(0), m_lastDownloadAborted(false) { qRegisterMetaType< NetworkRequest* >( "NetworkRequest*" ); qRegisterMetaType< NetworkRequest::Ptr >( "NetworkRequest::Ptr" ); } Network::~Network() { // Quit event loop of possibly running synchronous requests m_mutex->lock(); m_quit = true; m_mutex->unlock(); // Notify about the abort request to abort running synchronous requests abortSynchronousRequests(); qApp->processEvents(); // Handle the signal const QList< NetworkRequest::Ptr > _runningRequests = runningRequests(); if ( !_runningRequests.isEmpty() ) { qWarning() << "Deleting Network object with" << _runningRequests.count() << "running requests"; foreach ( const NetworkRequest::Ptr &request, _runningRequests ) { request->abort(); } } delete m_mutex; m_manager->deleteLater(); } NetworkRequest* Network::createRequest( const QString& url, const QString &userUrl ) { NetworkRequest *request = new NetworkRequest( url, userUrl, this, parent() ); NetworkRequest::Ptr requestPtr( request ); m_mutex->lock(); m_requests << requestPtr; m_mutex->unlock(); connect( request, SIGNAL(started()), this, SLOT(slotRequestStarted()) ); connect( request, SIGNAL(finished(QByteArray,bool,QString,int,int)), this, SLOT(slotRequestFinished(QByteArray,bool,QString,int,int)) ); connect( request, SIGNAL(aborted()), this, SLOT(slotRequestAborted()) ); connect( request, SIGNAL(redirected(QUrl)), this, SLOT(slotRequestRedirected(QUrl)) ); return request; } NetworkRequest::Ptr Network::getSharedRequest( NetworkRequest *request ) const { QMutexLocker locker( m_mutex ); foreach ( const NetworkRequest::Ptr &sharedRequest, m_requests ) { if ( sharedRequest.data() == request ) { return sharedRequest; } } return NetworkRequest::Ptr(); } void Network::slotRequestStarted() { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest - m_mutex->lockInline(); + m_mutex->lock(); m_lastDownloadAborted = false; - m_mutex->unlockInline(); + m_mutex->unlock(); emit requestStarted( sharedRequest ); } void Network::slotRequestFinished( const QByteArray &data, bool error, const QString &errorString, int statusCode, int size ) { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest // Remove finished request from request list and add it to the list of finished requests // to not have it deleted here. The script might try to use the request object later, // eg. because of connected slots, and will crash if the request object was already deleted. // This way NetworkRequest objects stay alive at least as long as the Network object does. - m_mutex->lockInline(); + m_mutex->lock(); const QDateTime timestamp = QDateTime::currentDateTime(); m_requests.removeOne( sharedRequest ); m_finishedRequests << sharedRequest; - m_mutex->unlockInline(); + m_mutex->unlock(); emit requestFinished( sharedRequest, data, error, errorString, timestamp, statusCode, size ); if ( !hasRunningRequests() ) { emit allRequestsFinished(); } } void Network::slotRequestRedirected( const QUrl &newUrl ) { NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest - m_mutex->lockInline(); + m_mutex->lock(); QNetworkReply *reply = m_manager->get( *request->request() ); m_lastUrl = newUrl.toString(); - m_mutex->unlockInline(); + m_mutex->unlock(); request->started( reply ); DEBUG_NETWORK("Redirected to" << newUrl); emit requestRedirected( sharedRequest, newUrl ); } void Network::slotRequestAborted() { - m_mutex->lockInline(); + m_mutex->lock(); if ( m_quit ) { - m_mutex->unlockInline(); + m_mutex->unlock(); return; } NetworkRequest *request = qobject_cast< NetworkRequest* >( sender() ); NetworkRequest::Ptr sharedRequest = getSharedRequest( request ); if ( !sharedRequest ) { qWarning() << "Network object already deleted?"; } // Q_ASSERT( sharedRequest ); // This slot should only be connected to signals of NetworkRequest DEBUG_NETWORK("Aborted" << request->url()); m_lastDownloadAborted = true; - m_mutex->unlockInline(); + m_mutex->unlock(); emit requestAborted( sharedRequest ); } bool Network::checkRequest( NetworkRequest* request ) { // Wrong argument type from script or no argument if ( !request ) { kDebug() << "Need a NetworkRequest object as argument, create it with " "'network.createRequest(url)'"; return false; } // The same request can not be executed more than once at a time if ( request->isRunning() ) { kDebug() << "Request is currently running" << request->url(); return false; } else if ( request->isFinished() ) { kDebug() << "Request is already finished" << request->url(); return false; } return request->isValid(); } void Network::get( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a get request - m_mutex->lockInline(); + m_mutex->lock(); QNetworkReply *reply = m_manager->get( *request->request() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); - m_mutex->unlockInline(); + m_mutex->unlock(); request->started( reply, timeout ); } void Network::head( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a head request - m_mutex->lockInline(); + m_mutex->lock(); QNetworkReply *reply = m_manager->head( *request->request() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); - m_mutex->unlockInline(); + m_mutex->unlock(); request->started( reply, timeout ); } void Network::post( NetworkRequest* request, int timeout ) { if ( !checkRequest(request) ) { return; } // Create a head request - m_mutex->lockInline(); + m_mutex->lock(); QNetworkReply *reply = m_manager->post( *request->request(), request->postDataByteArray() ); m_lastUrl = request->url(); m_lastUserUrl = request->userUrl(); - m_mutex->unlockInline(); + m_mutex->unlock(); request->started( reply, timeout ); } void Network::abortAllRequests() { const QList< NetworkRequest::Ptr > requests = runningRequests(); DEBUG_NETWORK("Abort" << requests.count() << "request(s)"); for ( int i = requests.count() - 1; i >= 0; --i ) { // Calling abort automatically removes the aborted request from m_createdRequests requests[i]->abort(); } // Notify about the abort request to abort running synchronous requests abortSynchronousRequests(); } void Network::abortSynchronousRequests() { emit doAbortSynchronousRequests(); } QByteArray Network::getSynchronous( const QString &url, const QString &userUrl, int timeout ) { // Create a get request QNetworkRequest request( url ); DEBUG_NETWORK("Start synchronous request" << url); - m_mutex->lockInline(); + m_mutex->lock(); QNetworkReply *reply = m_manager->get( request ); ++m_synchronousRequestCount; m_lastUrl = url; m_lastUserUrl = userUrl.isEmpty() ? url : userUrl; m_lastDownloadAborted = false; - m_mutex->unlockInline(); + m_mutex->unlock(); emit synchronousRequestStarted( url ); const QTime start = QTime::currentTime(); int redirectCount = 0; const int maxRedirections = 3; forever { // Use an event loop to wait for execution of the request, // ie. make netAccess.download() synchronous for scripts QEventLoop eventLoop; connect( reply, SIGNAL(finished()), &eventLoop, SLOT(quit()) ); connect( this, SIGNAL(doAbortSynchronousRequests()), &eventLoop, SLOT(quit()) ); if ( timeout > 0 ) { QTimer::singleShot( timeout, &eventLoop, SLOT(quit()) ); } if ( !reply->isFinished() ) { eventLoop.exec( QEventLoop::ExcludeUserInputEvents ); } m_mutex->lock(); const bool quit = m_quit || m_lastDownloadAborted || reply->isRunning(); m_mutex->unlock(); // Check if the timeout occured before the request finished if ( quit ) { DEBUG_NETWORK("Cancelled, destroyed or timeout while downloading" << url); emitSynchronousRequestFinished( url, QByteArray(), true ); return QByteArray(); } if ( reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid() ) { ++redirectCount; if ( redirectCount > maxRedirections ) { reply->deleteLater(); emitSynchronousRequestFinished( url, QByteArray("Too many redirections"), true, 200, start.msecsTo(QTime::currentTime()) ); return QByteArray(); } // Request the redirection target const QUrl redirectUrl = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl(); request.setUrl( redirectUrl ); DEBUG_NETWORK("Redirected to" << redirectUrl); m_mutex->lock(); delete reply; reply = m_manager->get( request ); m_lastUrl = redirectUrl.toString(); m_mutex->unlock(); emit synchronousRequestRedirected( redirectUrl.toEncoded() ); } else { // No (more) redirection break; } } const int time = start.msecsTo( QTime::currentTime() ); const int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); DEBUG_NETWORK("Waited" << (time / 1000.0) << "seconds for download of" << url << "Status:" << statusCode); // Read all data, decode it and give it to the script QByteArray data = reply->readAll(); reply->deleteLater(); if ( data.isEmpty() ) { qWarning() << "Error downloading" << url << reply->errorString(); emitSynchronousRequestFinished( url, QByteArray(), true, statusCode, time ); return QByteArray(); } else { emitSynchronousRequestFinished( url, data, false, statusCode, time, data.size() ); return data; } } void Network::emitSynchronousRequestFinished( const QString &url, const QByteArray &data, bool cancelled, int statusCode, int waitTime, int size ) { m_mutex->lock(); --m_synchronousRequestCount; m_mutex->unlock(); emit synchronousRequestFinished( url, data, cancelled, statusCode, waitTime, size ); if ( !hasRunningRequests() ) { emit allRequestsFinished(); } } ResultObject::ResultObject( QObject* parent ) : QObject(parent), m_mutex(new QMutex()), m_features(DefaultFeatures), m_hints(NoHint) { } ResultObject::~ResultObject() { delete m_mutex; } bool ResultObject::isHintGiven( ResultObject::Hint hint ) const { QMutexLocker locker( m_mutex ); return m_hints.testFlag( hint ); } void ResultObject::giveHint( ResultObject::Hint hint, bool enable ) { QMutexLocker locker( m_mutex ); if ( enable ) { // Remove incompatible flags of hint if any if ( hint == CityNamesAreLeft && m_hints.testFlag(CityNamesAreRight) ) { m_hints &= ~CityNamesAreRight; } else if ( hint == CityNamesAreRight && m_hints.testFlag(CityNamesAreLeft) ) { m_hints &= ~CityNamesAreLeft; } m_hints |= hint; } else { m_hints &= ~hint; } } bool ResultObject::isFeatureEnabled( ResultObject::Feature feature ) const { QMutexLocker locker( m_mutex ); return m_features.testFlag( feature ); } void ResultObject::enableFeature( ResultObject::Feature feature, bool enable ) { QMutexLocker locker( m_mutex ); if ( enable ) { m_features |= feature; } else { m_features &= ~feature; } } void ResultObject::dataList( const QList< TimetableData > &dataList, PublicTransportInfoList *infoList, ParseDocumentMode parseMode, Enums::VehicleType defaultVehicleType, const GlobalTimetableInfo *globalInfo, ResultObject::Features features, ResultObject::Hints hints ) { // TODO Use features and hints QDate curDate; QTime lastTime; int dayAdjustment = hints.testFlag(DatesNeedAdjustment) ? QDate::currentDate().daysTo(globalInfo->requestDate) : 0; if ( dayAdjustment != 0 ) { kDebug() << "Dates get adjusted by" << dayAdjustment << "days"; } // Find words at the beginning/end of target and route stop names that have many // occurrences. These words are most likely the city names where the stops are in. // But the timetable becomes easier to read and looks nicer, if not each stop name // includes the same city name. QHash< QString, int > firstWordCounts; // Counts occurrences of words at the beginning QHash< QString, int > lastWordCounts; // Counts occurrences of words at the end // This is the range of occurrences of one word in stop names, // that causes that word to be removed const int minWordOccurrence = qMax( 2, dataList.count() ); const int maxWordOccurrence = qMax( 3, dataList.count() / 2 ); // This regular expression gets used to search for word at the end, possibly including // a colon before the last word QRegExp rxLastWord( ",?\\s+\\S+$" ); // These strings store the words with the most occurrences in stop names at the beginning/end QString removeFirstWord; QString removeLastWord; // Read timetable data from the script for ( int i = 0; i < dataList.count(); ++i ) { TimetableData timetableData = dataList[ i ]; // Set default vehicle type if none is set if ( !timetableData.contains(Enums::TypeOfVehicle) || timetableData[Enums::TypeOfVehicle].toString().isEmpty() ) { timetableData[ Enums::TypeOfVehicle ] = static_cast( defaultVehicleType ); } if ( parseMode != ParseForStopSuggestions ) { QDateTime dateTime = timetableData[ Enums::DepartureDateTime ].toDateTime(); QDate departureDate = timetableData[ Enums::DepartureDate ].toDate(); QTime departureTime = timetableData[ Enums::DepartureTime ].toTime(); if ( !dateTime.isValid() && !departureTime.isValid() ) { kDebug() << "No departure time given!" << timetableData[Enums::DepartureTime]; kDebug() << "Use eg. helper.matchTime() to convert a string to a time object"; } if ( !dateTime.isValid() ) { if ( departureDate.isValid() ) { dateTime = QDateTime( departureDate, departureTime ); } else if ( curDate.isNull() ) { // First departure if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 ) { dateTime.setDate( QDate::currentDate().addDays(-1) ); } else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < 3 ) { dateTime.setDate( QDate::currentDate().addDays(1) ); } else { dateTime.setDate( QDate::currentDate() ); } } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) { // Time too much ealier than last time, estimate it's tomorrow dateTime.setDate( curDate.addDays(1) ); } else { dateTime.setDate( curDate ); } timetableData[ Enums::DepartureDateTime ] = dateTime; } if ( dayAdjustment != 0 ) { dateTime.setDate( dateTime.date().addDays(dayAdjustment) ); timetableData[ Enums::DepartureDateTime ] = dateTime; } curDate = dateTime.date(); lastTime = departureTime; } // Create info object for the timetable data PublicTransportInfoPtr info; if ( parseMode == ParseForJourneysByDepartureTime || parseMode == ParseForJourneysByArrivalTime ) { info = QSharedPointer( new JourneyInfo(timetableData) ); } else if ( parseMode == ParseForDepartures || parseMode == ParseForArrivals ) { info = QSharedPointer( new DepartureInfo(timetableData) ); } else if ( parseMode == ParseForStopSuggestions ) { info = QSharedPointer( new StopInfo(timetableData) ); } if ( !info->isValid() ) { info.clear(); continue; } // Find word to remove from beginning/end of stop names, if not already found // TODO Use hint from the data engine.. if ( features.testFlag(AutoRemoveCityFromStopNames) && removeFirstWord.isEmpty() && removeLastWord.isEmpty() ) { // First count the first/last word of the target stop name const QString target = info->value( Enums::Target ).toString(); int pos = target.indexOf( ' ' ); if ( pos > 0 && ++firstWordCounts[target.left(pos)] >= maxWordOccurrence ) { removeFirstWord = target.left(pos); } if ( rxLastWord.indexIn(target) != -1 && ++lastWordCounts[rxLastWord.cap()] >= maxWordOccurrence ) { removeLastWord = rxLastWord.cap(); } // Check if route stop names are available if ( info->contains(Enums::RouteStops) ) { QStringList stops = info->value( Enums::RouteStops ).toStringList(); QString target = info->value( Enums::Target ).toString(); // TODO Break if 70% or at least three of the route stop names // begin/end with the same word // int minCount = qMax( 3, int(stops.count() * 0.7) ); foreach ( const QString &stop, stops ) { // Test first word pos = stop.indexOf( ' ' ); const QString newFirstWord = stop.left( pos ); if ( pos > 0 && ++firstWordCounts[newFirstWord] >= maxWordOccurrence ) { removeFirstWord = newFirstWord; break; } // Test last word if ( rxLastWord.indexIn(stop) != -1 && ++lastWordCounts[rxLastWord.cap()] >= maxWordOccurrence ) { removeLastWord = rxLastWord.cap(); break; } } } } infoList->append( info ); } // Remove word with most occurrences from beginning/end of stop names if ( features.testFlag(AutoRemoveCityFromStopNames) ) { if ( removeFirstWord.isEmpty() && removeLastWord.isEmpty() ) { // If no first/last word with at least maxWordOccurrence occurrences was found, // find the word with the most occurrences int max = 0; // Find word at the beginning with most occurrences for ( QHash< QString, int >::ConstIterator it = firstWordCounts.constBegin(); it != firstWordCounts.constEnd(); ++it ) { if ( it.value() > max ) { max = it.value(); removeFirstWord = it.key(); } } // Find word at the end with more occurrences for ( QHash< QString, int >::ConstIterator it = lastWordCounts.constBegin(); it != lastWordCounts.constEnd(); ++it ) { if ( it.value() > max ) { max = it.value(); removeLastWord = it.key(); } } if ( max < minWordOccurrence ) { // The first/last word with the most occurrences has too few occurrences // Do not remove any word removeFirstWord.clear(); removeLastWord.clear(); } else if ( !removeLastWord.isEmpty() ) { // removeLastWord has more occurrences than removeFirstWord removeFirstWord.clear(); } } if ( !removeFirstWord.isEmpty() ) { // Remove removeFirstWord from all stop names for ( int i = 0; i < infoList->count(); ++i ) { QSharedPointer info = infoList->at( i ); QString target = info->value( Enums::Target ).toString(); if ( target.startsWith(removeFirstWord) ) { target = target.mid( removeFirstWord.length() + 1 ); info->insert( Enums::TargetShortened, target ); } QStringList stops = info->value( Enums::RouteStops ).toStringList(); for ( int i = 0; i < stops.count(); ++i ) { if ( stops[i].startsWith(removeFirstWord) ) { stops[i] = stops[i].mid( removeFirstWord.length() + 1 ); } } info->insert( Enums::RouteStopsShortened, stops ); } } else if ( !removeLastWord.isEmpty() ) { // Remove removeLastWord from all stop names for ( int i = 0; i < infoList->count(); ++i ) { QSharedPointer info = infoList->at( i ); QString target = info->value( Enums::Target ).toString(); if ( target.endsWith(removeLastWord) ) { target = target.left( target.length() - removeLastWord.length() ); info->insert( Enums::TargetShortened, target ); } QStringList stops = info->value( Enums::RouteStops ).toStringList(); for ( int i = 0; i < stops.count(); ++i ) { if ( stops[i].endsWith(removeLastWord) ) { stops[i] = stops[i].left( stops[i].length() - removeLastWord.length() ); } } info->insert( Enums::RouteStopsShortened, stops ); } } } } QString Helper::decodeHtmlEntities( const QString& html ) { return Global::decodeHtmlEntities( html ); } QString Helper::encodeHtmlEntities( const QString &html ) { return Global::encodeHtmlEntities( html ); } QString Helper::decodeHtml( const QByteArray &document, const QByteArray &fallbackCharset ) { return Global::decodeHtml( document, fallbackCharset ); } QString Helper::decode( const QByteArray &document, const QByteArray &charset ) { return Global::decode( document, charset ); } Helper::Helper( const QString &serviceProviderId, QObject *parent ) : QObject(parent), m_mutex(new QMutex()), m_serviceProviderId(serviceProviderId), m_errorMessageRepetition(0) { } Helper::~Helper() { emitRepeatedMessageWarning(); delete m_mutex; } void Helper::emitRepeatedMessageWarning() { QMutexLocker locker( m_mutex ); if ( m_errorMessageRepetition > 0 ) { kDebug() << "(Last error message repeated" << m_errorMessageRepetition << "times)"; m_errorMessageRepetition = 0; emit messageReceived( i18nc("@info/plain", "Last error message repeated %1 times", m_errorMessageRepetition), QScriptContextInfo() ); } } void Helper::messageReceived( const QString &message, const QString &failedParseText, ErrorSeverity severity ) { m_mutex->lock(); if ( message == m_lastErrorMessage ) { ++m_errorMessageRepetition; m_mutex->unlock(); return; } m_lastErrorMessage = message; QScriptContextInfo info( context()->parentContext() ); const QString serviceProviderId = m_serviceProviderId; m_mutex->unlock(); emitRepeatedMessageWarning(); emit messageReceived( message, info, failedParseText, severity ); // Output debug message and a maximum count of 200 characters of the text where the parsing failed QString shortParseText = failedParseText.trimmed().left(350); int diff = failedParseText.length() - shortParseText.length(); if ( diff > 0 ) { shortParseText.append(QString("... <%1 more chars>").arg(diff)); } shortParseText = shortParseText.replace('\n', "\n "); // Indent #ifdef ENABLE_DEBUG_SCRIPT_ERROR m_mutex->lock(); const QString name = severity == Information ? "Information" : (severity == Warning ? "Warning" : "Fatal error"); DEBUG_SCRIPT_ERROR( QString("%1 in %2-script, function %3(), file %4, line %5") .arg(name) .arg(m_serviceProviderId) .arg(info.functionName().isEmpty() ? "[anonymous]" : info.functionName()) .arg(QFileInfo(info.fileName()).fileName()) .arg(info.lineNumber()) ); m_mutex->unlock(); DEBUG_SCRIPT_ERROR( message ); if ( !shortParseText.isEmpty() ) { DEBUG_SCRIPT_ERROR( QString("The text of the document where parsing failed is: \"%1\"") .arg(shortParseText) ); } #endif // Log the complete message to the log file. - QString logFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport" ); + QString logFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + "plasma_engine_publictransport" +); logFileName.append( "serviceproviders.log" ); if ( !logFileName.isEmpty() ) { QFile logFile(logFileName); if ( logFile.size() > 1024 * 512 ) { // == 0.5 MB if ( !logFile.remove() ) { kDebug() << "Error: Couldn't delete old log file."; } else { kDebug() << "Deleted old log file, because it was getting too big."; } } if ( !logFile.open(QIODevice::Append | QIODevice::Text) ) { kDebug() << "Couldn't open the log file in append mode" << logFileName << logFile.errorString(); return; } logFile.write( QString("%1 (%2, in function %3(), file %4, line %5):\n \"%6\"\n Failed while reading this text: \"%7\"\n-------------------------------------\n\n") .arg(serviceProviderId) .arg(QDateTime::currentDateTime().toString()) .arg(info.functionName().isEmpty() ? "[anonymous]" : info.functionName()) .arg(QFileInfo(info.fileName()).fileName()) .arg(info.lineNumber()) .arg(message) .arg(failedParseText.trimmed()).toUtf8() ); logFile.close(); } } void ResultObject::addData( const QVariantMap &item ) { - m_mutex->lockInline(); + m_mutex->lock(); TimetableData data; for ( QVariantMap::ConstIterator it = item.constBegin(); it != item.constEnd(); ++it ) { bool ok; Enums::TimetableInformation info = static_cast< Enums::TimetableInformation >( it.key().toInt(&ok) ); if ( !ok || info == Enums::Nothing ) { info = Global::timetableInformationFromString( it.key() ); } const QVariant value = it.value(); if ( info == Enums::Nothing ) { qWarning() << "Invalid property name" << it.key() << "with value" << value; QString message; if ( it.key() == QLatin1String("length") && value.canConvert(QVariant::Int) && item.count() == value.toInt() + 1 ) // +1 for the "length" property { // Seems like a list was given and autoconverted to QVariantMap in this way message = i18nc("@info/plain", "Invalid property name \"%1\" " "with value \"%2\", seems like a list was passed to " "result.addData() instead of an object with properties.", it.key(), value.toString()); context()->throwError( message ); } else { message = i18nc("@info/plain", "Invalid property name \"%1\" with value \"%2\"", it.key(), value.toString()); } const int count = m_timetableData.count(); - m_mutex->unlockInline(); + m_mutex->unlock(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); - m_mutex->lockInline(); + m_mutex->lock(); continue; } else if ( value.isNull() ) { // Null value received, simply leave the data empty continue; } else if ( !value.isValid() ) { qWarning() << "Value for" << info << "is invalid or null" << value; const QString message = i18nc("@info/plain", "Invalid value received for \"%1\"", it.key()); const int count = m_timetableData.count(); - m_mutex->unlockInline(); + m_mutex->unlock(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); - m_mutex->lockInline(); + m_mutex->lock(); continue; } else if ( info == Enums::TypeOfVehicle && static_cast(value.toInt()) == Enums::InvalidVehicleType && Global::vehicleTypeFromString(value.toString()) == Enums::InvalidVehicleType ) { qWarning() << "Invalid type of vehicle value" << value; const QString message = i18nc("@info/plain", "Invalid type of vehicle received: \"%1\"", value.toString()); const int count = m_timetableData.count(); - m_mutex->unlockInline(); + m_mutex->unlock(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); - m_mutex->lockInline(); + m_mutex->lock(); } else if ( info == Enums::TypesOfVehicleInJourney || info == Enums::RouteTypesOfVehicles ) { const QVariantList types = value.toList(); foreach ( const QVariant &type, types ) { if ( static_cast(type.toInt()) == Enums::InvalidVehicleType && Global::vehicleTypeFromString(type.toString()) == Enums::InvalidVehicleType ) { kDebug() << "Invalid type of vehicle value in" << Global::timetableInformationToString(info) << value; const QString message = i18nc("@info/plain", "Invalid type of vehicle received in \"%1\": \"%2\"", Global::timetableInformationToString(info), type.toString()); const int count = m_timetableData.count(); - m_mutex->unlockInline(); + m_mutex->unlock(); emit invalidDataReceived( info, message, context()->parentContext(), count, item ); - m_mutex->lockInline(); + m_mutex->lock(); } } } else if ( info == Enums::JourneyNewsUrl ) { QString url = value.toString(); if ( url.startsWith('/') ) { // Prepend provider URL to relative URLs qWarning() << "Prepending provider URL to relative JourneyNewsUrls is not implemented"; // url.prepend( m_ ); TODO } } else if ( info == Enums::JourneyNewsOther ) { // DEPRECATED qWarning() << "JourneyNewsOther is deprecated, use JourneyNews instead"; info = Enums::JourneyNews; } if ( m_features.testFlag(AutoDecodeHtmlEntities) ) { if ( value.canConvert(QVariant::String) && (info == Enums::StopName || info == Enums::Target || info == Enums::StartStopName || info == Enums::TargetStopName || info == Enums::Operator || info == Enums::TransportLine || info == Enums::Platform || info == Enums::DelayReason || info == Enums::Status || info == Enums::Pricing) ) { // Decode HTML entities in string values data[ info ] = Global::decodeHtmlEntities( value.toString() ).trimmed(); } else if ( value.canConvert(QVariant::StringList) && (info == Enums::RouteStops || info == Enums::RoutePlatformsDeparture || info == Enums::RoutePlatformsArrival) ) { // Decode HTML entities in string list values QStringList stops = value.toStringList(); for ( QStringList::Iterator it = stops.begin(); it != stops.end(); ++it ) { *it = Helper::trim( Global::decodeHtmlEntities(*it) ); } data[ info ] = stops; } else { // Other values don't need decoding data[ info ] = value; } } else { data[ info ] = value; } } m_timetableData << data; if ( m_features.testFlag(AutoPublish) && m_timetableData.count() == 10 ) { // Publish the first 10 data items automatically - m_mutex->unlockInline(); + m_mutex->unlock(); emit publish(); } else { - m_mutex->unlockInline(); + m_mutex->unlock(); } } QString Helper::trim( const QString& str ) { return QString(str).trimmed() .replace( QRegExp("^( )+|( )+$", Qt::CaseInsensitive), QString() ) .trimmed(); } QString Helper::simplify( const QString &str ) { return QString(str).replace( QRegExp("( )+", Qt::CaseInsensitive), QString() ) .simplified(); } QString Helper::stripTags( const QString& str ) { const QString attributePattern = "\\w+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\"'>\\s]+))?"; QRegExp rx( QString("<\\/?\\w+(?:\\s+%1)*(?:\\s*/)?>").arg(attributePattern) ); rx.setMinimal( true ); return QString( str ).remove( rx ); } QString Helper::camelCase( const QString& str ) { QString ret = str.toLower(); QRegExp rx( "(^\\w)|\\W(\\w)" ); int pos = 0; while ( (pos = rx.indexIn(ret, pos)) != -1 ) { if ( rx.pos(2) == -1 || rx.pos(2) >= ret.length() ) { ret[ rx.pos(1) ] = ret[ rx.pos(1) ].toUpper(); } else { ret[ rx.pos(2) ] = ret[ rx.pos(2) ].toUpper(); } pos += rx.matchedLength(); } return ret; } QString Helper::extractBlock( const QString& str, const QString& beginString, const QString& endString ) { int pos = str.indexOf( beginString ); if ( pos == -1 ) { return ""; } int end = str.indexOf( endString, pos + 1 ); return str.mid( pos, end - pos ); } QVariantMap Helper::matchTime( const QString& str, const QString& format ) { QString pattern = QRegExp::escape( format ); pattern = pattern.replace( "hh", "\\d{2}" ) .replace( "h", "\\d{1,2}" ) .replace( "mm", "\\d{2}" ) .replace( "m", "\\d{1,2}" ) .replace( "AP", "(AM|PM)" ) .replace( "ap", "(am|pm)" ); QVariantMap ret; QRegExp rx( pattern ); if ( rx.indexIn(str) != -1 ) { QTime time = QTime::fromString( rx.cap(), format ); ret.insert( "hour", time.hour() ); ret.insert( "minute", time.minute() ); } else if ( format != "hh:mm" ) { // Try default format if the one specified doesn't work QRegExp rx2( "\\d{1,2}:\\d{2}" ); if ( rx2.indexIn(str) != -1 ) { QTime time = QTime::fromString( rx2.cap(), "hh:mm" ); ret.insert( "hour", time.hour() ); ret.insert( "minute", time.minute() ); } else { ret.insert( "error", true ); DEBUG_SCRIPT_HELPER("Couldn't match time in" << str << pattern); } } else { ret.insert( "error", true ); DEBUG_SCRIPT_HELPER("Couldn't match time in" << str << pattern); } return ret; } QDate Helper::matchDate( const QString& str, const QString& format ) { QString pattern = QRegExp::escape( format ).replace( "d", "D" ); pattern = pattern.replace( "DD", "\\d{2}" ) .replace( "D", "\\d{1,2}" ) .replace( "MM", "\\d{2}" ) .replace( "M", "\\d{1,2}" ) .replace( "yyyy", "\\d{4}" ) .replace( "yy", "\\d{2}" ); QRegExp rx( pattern ); QDate date; if ( rx.indexIn(str) != -1 ) { date = QDate::fromString( rx.cap(), format ); } else if ( format != "yyyy-MM-dd" ) { // Try default format if the one specified doesn't work QRegExp rx2( "\\d{2,4}-\\d{2}-\\d{2}" ); if ( rx2.indexIn(str) != -1 ) { date = QDate::fromString( rx2.cap(), "yyyy-MM-dd" ); } } if ( !date.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't match date in" << str << pattern); } // Adjust date, needed for formats with only two "yy" for year matching // A year 12 means 2012, not 1912 if ( date.year() < 1970 ) { date.setDate( date.year() + 100, date.month(), date.day() ); } return date; } QString Helper::formatTime( int hour, int minute, const QString& format ) { return QTime( hour, minute ).toString( format ); } QString Helper::formatDate( int year, int month, int day, const QString& format ) { return QDate( year, month, day ).toString( format ); } QString Helper::formatDateTime( const QDateTime& dateTime, const QString& format ) { return dateTime.toString( format ); } int Helper::duration( const QString& sTime1, const QString& sTime2, const QString& format ) { QTime time1 = QTime::fromString( sTime1, format ); QTime time2 = QTime::fromString( sTime2, format ); if ( !time1.isValid() || !time2.isValid() ) { return -1; } return time1.secsTo( time2 ) / 60; } QString Helper::addMinsToTime( const QString& sTime, int minsToAdd, const QString& format ) { QTime time = QTime::fromString( sTime, format ); if ( !time.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't parse the given time" << sTime << format); return ""; } return time.addSecs( minsToAdd * 60 ).toString( format ); } QString Helper::addDaysToDate( const QString& sDate, int daysToAdd, const QString& format ) { QDate date = QDate::fromString( sDate, format ).addDays( daysToAdd ); if ( !date.isValid() ) { DEBUG_SCRIPT_HELPER("Couldn't parse the given date" << sDate << format); return sDate; } return date.toString( format ); } QDateTime Helper::addDaysToDate( const QDateTime& dateTime, int daysToAdd ) { return dateTime.addDays( daysToAdd ); } QStringList Helper::splitSkipEmptyParts( const QString& str, const QString& sep ) { return str.split( sep, QString::SkipEmptyParts ); } QVariantMap Helper::findFirstHtmlTag( const QString &str, const QString &tagName, const QVariantMap &options ) { // Set/overwrite maxCount option to match only the first tag using findHtmlTags() QVariantMap _options = options; _options[ "maxCount" ] = 1; QVariantList tags = findHtmlTags( str, tagName, _options ); // Copy values of first matched tag (if any) to the result object QVariantMap result; result.insert( "found", !tags.isEmpty() ); if ( !tags.isEmpty() ) { const QVariantMap firstTag = tags.first().toMap(); result.insert( "contents", firstTag["contents"] ); result.insert( "position", firstTag["position"] ); result.insert( "endPosition", firstTag["endPosition"] ); result.insert( "attributes", firstTag["attributes"] ); result.insert( "name", firstTag["name"] ); } return result; } QVariantList Helper::findHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options ) { const QVariantMap &attributes = options[ "attributes" ].toMap(); const int maxCount = options.value( "maxCount", 0 ).toInt(); const bool noContent = options.value( "noContent", false ).toBool(); const bool noNesting = options.value( "noNesting", false ).toBool(); const bool debug = options.value( "debug", false ).toBool(); const QString contentsRegExpPattern = options.value( "contentsRegExp", QString() ).toString(); const QVariantMap namePosition = options[ "namePosition" ].toMap(); int position = options.value( "position", 0 ).toInt(); const bool namePositionIsAttribute = namePosition["type"].toString().toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; const QString namePositionRegExpPattern = namePosition.contains("regexp") ? namePosition["regexp"].toString() : QString(); // Create regular expression that matches HTML elements with or without attributes. // Since QRegExp offers no way to retreive multiple matches of the same capture group // the whole attribute string gets matched here and then analyzed in another loop // using attributeRegExp. // Matching the attributes with all details here is required to prevent eg. having a match // end after a ">" character in a string in an attribute. const QString attributePattern = "\\w+(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\"'>\\s]+))?"; QRegExp htmlTagRegExp( noContent ? QString("<%1((?:\\s+%2)*)(?:\\s*/)?>").arg(tagName).arg(attributePattern) : QString("<%1((?:\\s+%2)*)>").arg(tagName).arg(attributePattern), // TODO TEST does this need a "\\s*" before the ">"? // : QString("<%1((?:\\s+%2)*)>%3").arg(tagName).arg(attributePattern) // .arg(contentsRegExpPattern), Qt::CaseInsensitive ); QRegExp htmlCloseTagRegExp( QString("").arg(tagName), Qt::CaseInsensitive ); QRegExp contentsRegExp( contentsRegExpPattern, Qt::CaseInsensitive ); htmlTagRegExp.setMinimal( true ); // Match attributes with or without value, with single/double/not quoted value QRegExp attributeRegExp( "(\\w+)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\"'>\\s]+)))?", Qt::CaseInsensitive ); QVariantList foundTags; while ( (foundTags.count() < maxCount || maxCount <= 0) && (position = htmlTagRegExp.indexIn(str, position)) != -1 ) { if ( debug ) { kDebug() << "Test match at" << position << htmlTagRegExp.cap().left(500); } const QString attributeString = htmlTagRegExp.cap( 1 ); QString tagContents; // = noContent ? QString() : htmlTagRegExp.cap( 2 ); QVariantMap foundAttributes; int attributePos = 0; while ( (attributePos = attributeRegExp.indexIn(attributeString, attributePos)) != -1 ) { const int valueCap = attributeRegExp.cap(2).isEmpty() ? (attributeRegExp.cap(3).isEmpty() ? 4 : 3) : 2; foundAttributes.insert( attributeRegExp.cap(1), attributeRegExp.cap(valueCap) ); attributePos += attributeRegExp.matchedLength(); } if ( debug ) { kDebug() << "Found attributes" << foundAttributes << "in" << attributeString; } // Test if the attributes match bool attributesMatch = true; for ( QVariantMap::ConstIterator it = attributes.constBegin(); it != attributes.constEnd(); ++it ) { if ( !foundAttributes.contains(it.key()) ) { // Did not find exact attribute name, try to use it as regular expression pattern attributesMatch = false; QRegExp attributeNameRegExp( it.key(), Qt::CaseInsensitive ); foreach ( const QString &attributeName, foundAttributes.keys() ) { if ( attributeNameRegExp.indexIn(attributeName) != -1 ) { // Matched the attribute name attributesMatch = true; break; } } if ( !attributesMatch ) { if ( debug ) { kDebug() << "Did not find attribute" << it.key(); } break; } } // Attribute exists, test it's value const QString value = foundAttributes[ it.key() ].toString(); const QString valueRegExpPattern = it.value().toString(); if ( !(value.isEmpty() && valueRegExpPattern.isEmpty()) ) { QRegExp valueRegExp( valueRegExpPattern, Qt::CaseInsensitive ); if ( valueRegExp.indexIn(value) == -1 ) { // Attribute value regexp did not matched attributesMatch = false; if ( debug ) { kDebug() << "Value" << value << "did not match pattern" << valueRegExpPattern; } break; } else if ( valueRegExp.captureCount() > 0 ) { // Attribute value regexp matched, store captures foundAttributes[ it.key() ] = valueRegExp.capturedTexts(); } } } // Search for new opening HTML tags (with same tag name) before the closing HTML tag int endPosition = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( !attributesMatch ) { position = endPosition; continue; } if ( !noContent ) { if ( noNesting ) { // "noNesting" option set, simply search for next closing tag, no matter if it is // a nested tag or not const int posClosing = htmlCloseTagRegExp.indexIn( str, endPosition ); const int contentsBegin = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); tagContents = str.mid( contentsBegin, posClosing - contentsBegin ); endPosition = htmlCloseTagRegExp.pos() + htmlCloseTagRegExp.matchedLength(); } else { // Find next closing tag, skipping nested tags. // Get string after the opening HTML tag const QString rest = str.mid( htmlTagRegExp.pos() + htmlTagRegExp.matchedLength() ); int posClosing = htmlCloseTagRegExp.indexIn( rest ); if ( posClosing == -1 ) { position = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( debug ) { kDebug() << "Closing tag" << tagName << "could not be found"; } position = endPosition; continue; } // Search for nested opening tags in between the main opening tag and the // next closing tag int posOpening = htmlTagRegExp.indexIn( rest.left(posClosing) ); while ( posOpening != -1 ) { // Found a nested tag, find the next closing tag posClosing = htmlCloseTagRegExp.indexIn( rest, posClosing + htmlCloseTagRegExp.matchedLength() ); if ( posClosing == -1 ) { position = htmlTagRegExp.pos() + htmlTagRegExp.matchedLength(); if ( debug ) { kDebug() << "Closing tag" << tagName << "could not be found"; } break; } // Search for more nested opening tags posOpening = htmlTagRegExp.indexIn( rest.left(posClosing), posOpening + htmlTagRegExp.matchedLength() ); } tagContents = rest.left( posClosing ); endPosition += htmlCloseTagRegExp.pos() + htmlCloseTagRegExp.matchedLength(); } } // Match contents, only use regular expression if one was given in the options argument if ( !contentsRegExpPattern.isEmpty() ) { if ( contentsRegExp.indexIn(tagContents) == -1 ) { if ( debug ) { kDebug() << "Did not match tag contents" << tagContents.left(500); } position = endPosition; continue; } else { // Use first matched group as contents string, if any // Otherwise use the whole match as contents string tagContents = contentsRegExp.cap( contentsRegExp.captureCount() <= 1 ? 0 : 1 ); } } else { // No regexp pattern for contents, use complete contents, but trimmed tagContents = tagContents.trimmed(); } // Construct a result object QVariantMap result; result.insert( "contents", tagContents ); result.insert( "position", position ); result.insert( "endPosition", endPosition ); result.insert( "attributes", foundAttributes ); // Find name if a "namePosition" option is given if ( !namePosition.isEmpty() ) { const QString name = getTagName( result, namePosition["type"].toString(), namePositionRegExpPattern, namePositionIsAttribute ? namePosition["name"].toString() : QString() ); result.insert( "name", name ); } if ( debug ) { kDebug() << "Found HTML tag" << tagName << "at" << position << foundAttributes; } foundTags << result; position = endPosition; } if ( debug ) { if ( foundTags.isEmpty() ) { kDebug() << "Found no" << tagName << "HTML tags in HTML" << str; } else { kDebug() << "Found" << foundTags.count() << tagName << "HTML tags"; } } return foundTags; } QString Helper::getTagName( const QVariantMap &searchResult, const QString &type, const QString ®Exp, const QString attributeName ) { const bool namePositionIsAttribute = type.toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; QString name = trim( namePositionIsAttribute ? searchResult["attributes"].toMap()[ attributeName ].toString() : searchResult["contents"].toString() ); if ( !regExp.isEmpty() ) { // Use "regexp" property of namePosition to match the header name QRegExp namePositionRegExp( regExp, Qt::CaseInsensitive ); if ( namePositionRegExp.indexIn(name) != -1 ) { name = namePositionRegExp.cap( qMin(1, namePositionRegExp.captureCount()) ); } } return name; } QVariantMap Helper::findNamedHtmlTags( const QString &str, const QString &tagName, const QVariantMap &options ) { QVariantMap namePosition; if ( !options.contains("namePosition") ) { namePosition[ "type" ] = "contents"; // Can be "attribute", "contents" } else { namePosition = options[ "namePosition" ].toMap(); } const bool namePositionIsAttribute = namePosition["type"].toString().toLower().compare( QLatin1String("attribute"), Qt::CaseInsensitive ) == 0; const QString namePositionRegExpPattern = namePosition.contains("regexp") ? namePosition["regexp"].toString() : QString(); const QString ambiguousNameResolution = options.contains("ambiguousNameResolution") ? options["ambiguousNameResolution"].toString().toLower() : "replace"; const bool debug = options.value( "debug", false ).toBool(); const QVariantList foundTags = findHtmlTags( str, tagName, options ); QVariantMap foundTagsMap; foreach ( const QVariant &foundTag, foundTags ) { QString name = getTagName( foundTag.toMap(), namePosition["type"].toString(), namePositionRegExpPattern, namePositionIsAttribute ? namePosition["name"].toString() : QString() ); if ( name.isEmpty() ) { if ( debug ) { kDebug() << "Empty name in" << str; } continue; } // Check if the newly found name was already found // and decide what to do based on the "ambiguousNameResolution" option if ( ambiguousNameResolution == QLatin1String("addnumber") && foundTagsMap.contains(name) ) { QRegExp rx( "(\\d+)$" ); if ( rx.indexIn(name) != -1 ) { name += QString::number( rx.cap(1).toInt() + 1 ); } else { name += "2"; } } foundTagsMap[ name ] = foundTag; // TODO Use lists here? The same name could be found multiply times } // Store list of names in the "names" property, therefore "names" should not be a found tag name if ( !foundTagsMap.contains("names") ) { foundTagsMap[ "names" ] = QVariant::fromValue( foundTagsMap.keys() ); } else if ( debug ) { kDebug() << "A tag with the name 'names' was found. Normally a property 'names' gets " "added to the object returned by this functionm, which lists all found " "names in a list."; } return foundTagsMap; } const char* Storage::LIFETIME_ENTRYNAME_SUFFIX = "__expires__"; class StoragePrivate { public: StoragePrivate( const QString &serviceProvider ) : readWriteLock(new QReadWriteLock(QReadWriteLock::Recursive)), readWriteLockPersistent(new QReadWriteLock(QReadWriteLock::Recursive)), serviceProvider(serviceProvider), lastLifetimeCheck(0), config(0) { }; ~StoragePrivate() { delete readWriteLock; delete readWriteLockPersistent; delete config; }; void readPersistentData() { // Delete already read config object if ( config ) { delete config; } config = new KConfig( ServiceProviderGlobal::cacheFileName(), KConfig::SimpleConfig ); }; KConfigGroup persistentGroup() { if ( !config ) { readPersistentData(); } return config->group( serviceProvider ).group( QLatin1String("storage") ); }; quint16 checkLength( const QByteArray &data ) { if ( data.length() > 65535 ) { kDebug() << "Data is too long, only 65535 bytes are supported" << data.length(); } return static_cast( data.length() ); }; QReadWriteLock *readWriteLock; QReadWriteLock *readWriteLockPersistent; QVariantMap data; const QString serviceProvider; uint lastLifetimeCheck; // as time_t KConfig *config; KConfigGroup lastPersistentGroup; }; Storage::Storage( const QString &serviceProviderId, QObject *parent ) : QObject(parent), d(new StoragePrivate(serviceProviderId)) { // Delete persistently stored data which lifetime has expired checkLifetime(); } Storage::~Storage() { delete d; } void Storage::write( const QVariantMap& data ) { for ( QVariantMap::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { write( it.key(), it.value() ); } } void Storage::write( const QString& name, const QVariant& data ) { QWriteLocker locker( d->readWriteLock ); d->data.insert( name, data ); } QVariantMap Storage::read() { QReadLocker locker( d->readWriteLock ); return d->data; } QVariant Storage::read( const QString& name, const QVariant& defaultData ) { QReadLocker locker( d->readWriteLock ); return d->data.contains(name) ? d->data[name] : defaultData; } void Storage::remove( const QString& name ) { QWriteLocker locker( d->readWriteLock ); d->data.remove( name ); } void Storage::clear() { QWriteLocker locker( d->readWriteLock ); d->data.clear(); } int Storage::lifetime( const QString& name ) { QReadLocker locker( d->readWriteLockPersistent ); return lifetime( name, d->persistentGroup() ); } int Storage::lifetime( const QString& name, const KConfigGroup& group ) { QReadLocker locker( d->readWriteLockPersistent ); return lifetimeNoLock( name, group ); } int Storage::lifetimeNoLock( const QString &name, const KConfigGroup &group ) { const uint lifetimeTime_t = group.readEntry( name + LIFETIME_ENTRYNAME_SUFFIX, 0 ); return QDateTime::currentDateTime().daysTo( QDateTime::fromTime_t(lifetimeTime_t) ); } void Storage::checkLifetime() { QWriteLocker locker( d->readWriteLockPersistent ); if ( QDateTime::currentDateTime().toTime_t() - d->lastLifetimeCheck < MIN_LIFETIME_CHECK_INTERVAL * 60 ) { // Last lifetime check was less than 15 minutes ago return; } // Try to load script features from a cache file KConfigGroup group = d->persistentGroup(); QMap< QString, QString > data = group.entryMap(); for ( QMap< QString, QString >::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { if ( it.key().endsWith(LIFETIME_ENTRYNAME_SUFFIX) ) { // Do not check lifetime of entries which store the lifetime of the real data entries continue; } const int remainingLifetime = lifetimeNoLock( it.key(), group ); if ( remainingLifetime <= 0 ) { // Lifetime has expired kDebug() << "Lifetime of storage data" << it.key() << "for" << d->serviceProvider << "has expired" << remainingLifetime; removePersistent( it.key(), group ); } } d->lastLifetimeCheck = QDateTime::currentDateTime().toTime_t(); } bool Storage::hasData( const QString &name ) const { QReadLocker locker( d->readWriteLock ); return d->data.contains( name ); } bool Storage::hasPersistentData( const QString &name ) const { QReadLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); return group.hasKey( name ); } QByteArray Storage::encodeData( const QVariant &data ) const { // Store the type of the variant, because it is needed to read the QVariant with the correct // type using KConfigGroup::readEntry() const uchar type = static_cast( data.type() ); if ( type >= QVariant::LastCoreType ) { kDebug() << "Invalid data type, only QVariant core types are supported" << data.type(); return QByteArray(); } // Write type into the first byte QByteArray encodedData; encodedData[0] = type; // Write data if ( data.canConvert(QVariant::ByteArray) ) { encodedData += data.toByteArray(); } else if ( data.canConvert(QVariant::String) ) { encodedData += data.toString().toUtf8(); } else { switch ( data.type() ) { case QVariant::StringList: case QVariant::List: { // Lists are stored like this (one entry after the other): // "<2 Bytes: Value length>" const QVariantList list = data.toList(); foreach ( const QVariant &item, list ) { // Encode current list item QByteArray encodedItem = encodeData( item ); // Construct a QByteArray which contains the length in two bytes (0 .. 65535) const quint16 length = d->checkLength( encodedItem ); const QByteArray baLength( (const char*)&length, sizeof(length) ); // Use 2 bytes for the length of the data, append data encodedData += baLength; encodedData += encodedItem; } break; } case QVariant::Map: { // Maps are stored like this (one entry after the other): // "<2 Bytes: Key length><2 Bytes: Value length>" const QVariantMap map = data.toMap(); for ( QVariantMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it ) { // Encode current key and value QByteArray encodedKey = it.key().toUtf8(); QByteArray encodedValue = encodeData( it.value() ); // Construct QByteArrays which contain the lengths in two bytes each (0 .. 65535) const quint16 lengthKey = d->checkLength( encodedKey ); const quint16 lengthValue = d->checkLength( encodedValue ); const QByteArray baLengthKey( (const char*)&lengthKey, sizeof(lengthKey) ); const QByteArray baLengthValue( (const char*)&lengthValue, sizeof(lengthValue) ); // Use 2 bytes for the length of the key, append key encodedData += baLengthKey; encodedData += encodedKey; // Use 2 bytes for the length of the value, append value encodedData += baLengthValue; encodedData += encodedValue; } break; } default: kDebug() << "Cannot convert from type" << data.type(); return QByteArray(); } } return encodedData; } QVariant Storage::decodeData( const QByteArray &data ) const { const QVariant::Type type = static_cast( data[0] ); if ( type >= QVariant::LastCoreType ) { kDebug() << "Invalid encoding for data" << data; return QVariant(); } const QByteArray encodedValue = data.mid( 1 ); QVariant value( type ); value.setValue( encodedValue ); if ( value.canConvert( type ) ) { value.convert( type ); return value; } else { switch ( type ) { case QVariant::Date: // QVariant::toString() uses Qt::ISODate to convert QDate to string return QDate::fromString( value.toString(), Qt::ISODate ); case QVariant::Time: return QTime::fromString( value.toString() ); case QVariant::DateTime: // QVariant::toString() uses Qt::ISODate to convert QDateTime to string return QDateTime::fromString( value.toString(), Qt::ISODate ); case QVariant::StringList: case QVariant::List: { // Lists are stored like this (one entry after the other): // "<2 Bytes: Value length>" QVariantList decoded; QByteArray encoded = value.toByteArray(); int pos = 0; while ( pos + 2 < encoded.length() ) { const quint16 length = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 2 + length > encoded.length() ) { kDebug() << "Invalid list data" << encoded; return QVariant(); } const QByteArray encodedValue = encoded.mid( pos + 2, length ); const QVariant decodedValue = decodeData( encodedValue ); decoded << decodedValue; pos += 2 + length; } return decoded; } case QVariant::Map: { // Maps are stored like this (one entry after the other): // "<2 Bytes: Key length><2 Bytes: Value length>" QVariantMap decoded; const QByteArray encoded = value.toByteArray(); int pos = 0; while ( pos + 4 < encoded.length() ) { // Decode two bytes to quint16, this is the key length const quint16 keyLength = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 4 + keyLength > encoded.length() ) { // + 4 => 2 Bytes for keyLength + 2 Bytes for valueLength kDebug() << "Invalid map data" << encoded; return QVariant(); } // Extract key, starting after the two bytes for the key length const QString key = encoded.mid( pos + 2, keyLength ); pos += 2 + keyLength; // Decode two bytes to quint16, this is the value length const quint16 valueLength = *reinterpret_cast( encoded.mid(pos, 2).data() ); if ( pos + 2 + valueLength > encoded.length() ) { kDebug() << "Invalid map data" << encoded; return QVariant(); } // Extract and decode value, starting after the two bytes for the value length const QByteArray encodedValue = encoded.mid( pos + 2, valueLength ); const QVariant decodedValue = decodeData( encodedValue ); pos += 2 + valueLength; // Insert decoded value into the result map decoded.insert( key, decodedValue ); } return decoded; } default: kDebug() << "Cannot convert to type" << type; return QVariant(); } } } void Storage::writePersistent( const QVariantMap& data, uint lifetime ) { QWriteLocker locker( d->readWriteLockPersistent ); for ( QVariantMap::ConstIterator it = data.constBegin(); it != data.constEnd(); ++it ) { writePersistent( it.key(), it.value(), lifetime ); } } void Storage::writePersistent( const QString& name, const QVariant& data, uint lifetime ) { if ( lifetime > MAX_LIFETIME ) { lifetime = MAX_LIFETIME; } // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); group.writeEntry( name + LIFETIME_ENTRYNAME_SUFFIX, QDateTime::currentDateTime().addDays(lifetime).toTime_t() ); group.writeEntry( name, encodeData(data) ); } QVariant Storage::readPersistent( const QString& name, const QVariant& defaultData ) { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); const QByteArray data = d->persistentGroup().readEntry( name, QByteArray() ); return data.isEmpty() ? defaultData : decodeData(data); } void Storage::removePersistent( const QString& name, KConfigGroup& group ) { QWriteLocker locker( d->readWriteLockPersistent ); group.deleteEntry( name + LIFETIME_ENTRYNAME_SUFFIX ); group.deleteEntry( name ); } void Storage::removePersistent( const QString& name ) { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); KConfigGroup group = d->persistentGroup(); removePersistent( name, group ); } void Storage::clearPersistent() { // Try to load script features from a cache file QWriteLocker locker( d->readWriteLockPersistent ); d->persistentGroup().deleteGroup(); } QString Network::lastUrl() const { QMutexLocker locker( m_mutex ); return m_lastUrl; } QString Network::lastUserUrl() const { QMutexLocker locker( m_mutex ); return m_lastUserUrl; } void Network::clear() { QMutexLocker locker( m_mutex ); m_lastUrl.clear(); m_lastUserUrl.clear(); } bool Network::lastDownloadAborted() const { QMutexLocker locker( m_mutex ); return m_lastDownloadAborted; } bool Network::hasRunningRequests() const { QMutexLocker locker( m_mutex ); if ( m_synchronousRequestCount > 0 ) { // There is a synchronous request running return true; } foreach ( const NetworkRequest::Ptr &request, m_requests ) { if ( request->isRunning() ) { // Found a running asynchronous request return true; } } // No running request found return false; } QList< NetworkRequest::Ptr > Network::runningRequests() const { QMutexLocker locker( m_mutex ); QList< NetworkRequest::Ptr > requests; foreach ( const NetworkRequest::Ptr &request, m_requests ) { if ( request->isRunning() ) { requests << request; } } return requests; } int Network::runningRequestCount() const { QMutexLocker locker( m_mutex ); return runningRequests().count() + m_synchronousRequestCount; } QByteArray Network::fallbackCharset() const { QMutexLocker locker( m_mutex ); return m_fallbackCharset; } QList< TimetableData > ResultObject::data() const { QMutexLocker locker( m_mutex ); return m_timetableData; } QVariant ResultObject::data( int index, Enums::TimetableInformation information ) const { QMutexLocker locker( m_mutex ); if ( index < 0 || index >= m_timetableData.count() ) { context()->throwError( QScriptContext::RangeError, "Index out of range" ); return QVariant(); } return m_timetableData[ index ][ information ]; } bool ResultObject::hasData() const { QMutexLocker locker( m_mutex ); return !m_timetableData.isEmpty(); } int ResultObject::count() const { QMutexLocker locker( m_mutex ); return m_timetableData.count(); } ResultObject::Features ResultObject::features() const { QMutexLocker locker( m_mutex ); return m_features; } ResultObject::Hints ResultObject::hints() const { QMutexLocker locker( m_mutex ); return m_hints; } void ResultObject::clear() { QMutexLocker locker( m_mutex ); m_timetableData.clear(); } QScriptValue constructStream( QScriptContext *context, QScriptEngine *engine ) { QScriptValue argument = context->argument(0); DataStreamPrototype *object; if ( argument.instanceOf(context->callee()) ) { object = new DataStreamPrototype( qscriptvalue_cast(argument) ); } else if ( argument.isQObject() && qscriptvalue_cast(argument) ) { object = new DataStreamPrototype( qscriptvalue_cast(argument) ); } else if ( argument.isVariant() ) { const QVariant variant = argument.toVariant(); object = new DataStreamPrototype( variant.toByteArray() ); } else { return engine->undefinedValue(); } return engine->newQObject( object, QScriptEngine::ScriptOwnership ); } QScriptValue dataStreamToScript( QScriptEngine *engine, const DataStreamPrototypePtr &stream ) { return engine->newQObject( const_cast(stream), QScriptEngine::QtOwnership, QScriptEngine::PreferExistingWrapperObject ); } void dataStreamFromScript( const QScriptValue &object, DataStreamPrototypePtr &stream ) { stream = qobject_cast< DataStreamPrototype* >( object.toQObject() ); } DataStreamPrototype::DataStreamPrototype( QObject *parent ) : QObject( parent ) { } DataStreamPrototype::DataStreamPrototype( const QByteArray &byteArray, QObject *parent ) : QObject( parent ) { QBuffer *buffer = new QBuffer( const_cast(&byteArray), this ); buffer->open( QIODevice::ReadOnly ); m_dataStream = QSharedPointer< QDataStream >( new QDataStream(buffer) ); } DataStreamPrototype::DataStreamPrototype( QIODevice *device, QObject *parent ) : QObject( parent ) { if ( !device->isOpen() ) { qWarning() << "Device not opened"; } m_dataStream = QSharedPointer< QDataStream >( new QDataStream(device) ); } DataStreamPrototype::DataStreamPrototype( DataStreamPrototype *other ) : QObject(), m_dataStream(other->stream()) { } QDataStream *DataStreamPrototype::thisDataStream() const { return m_dataStream.data(); } qint8 DataStreamPrototype::readInt8() { qint8 i; thisDataStream()->operator >>( i ); return i; } quint8 DataStreamPrototype::readUInt8() { quint8 i; thisDataStream()->operator >>( i ); return i; } quint16 DataStreamPrototype::readUInt16() { quint16 i; thisDataStream()->operator >>( i ); return i; } qint32 DataStreamPrototype::readInt32() { qint32 i; thisDataStream()->operator >>( i ); return i; } qint16 DataStreamPrototype::readInt16() { qint16 i; thisDataStream()->operator >>( i ); return i; } quint32 DataStreamPrototype::readUInt32() { quint32 i; thisDataStream()->operator >>( i ); return i; } QString DataStreamPrototype::readString() { QString string; char character; while ( thisDataStream()->device()->getChar(&character) && character != 0 ) { string.append( character ); } return string; } QByteArray DataStreamPrototype::readBytes( uint byteCount ) { char *chars = new char[ byteCount ]; const uint bytesRead = thisDataStream()->readRawData( chars, byteCount ); if ( bytesRead != byteCount ) { qWarning() << "Did not read all requested bytes, read" << bytesRead << "of" << byteCount; } QByteArray bytes( chars ); delete[] chars; return bytes; } QByteArray DataStreamPrototype::readBytesUntilZero() { QByteArray bytes; char character; while ( thisDataStream()->device()->getChar(&character) && character != 0 ) { bytes.append( character ); } return bytes; } QString NetworkRequest::url() const { QMutexLocker locker( m_mutex ); return m_url; } QString NetworkRequest::userUrl() const { QMutexLocker locker( m_mutex ); return m_userUrl; } bool NetworkRequest::isRunning() const { QMutexLocker locker( m_mutex ); return m_reply; } bool NetworkRequest::isFinished() const { QMutexLocker locker( m_mutex ); return m_isFinished; } bool NetworkRequest::isRedirected() const { QMutexLocker locker( m_mutex ); return m_redirectUrl.isValid(); } QUrl NetworkRequest::redirectedUrl() const { QMutexLocker locker( m_mutex ); return m_redirectUrl; } QString NetworkRequest::postData() const { QMutexLocker locker( m_mutex ); return m_postData; } quint64 NetworkRequest::uncompressedSize() const { QMutexLocker locker( m_mutex ); return m_uncompressedSize; } void NetworkRequest::setUserData( const QVariant &userData ) { QMutexLocker locker( m_mutex ); m_userData = userData; } QVariant NetworkRequest::userData() const { QMutexLocker locker( m_mutex ); return m_userData; } #include "scriptapi.moc" } // namespace ScriptApi diff --git a/engine/timetablemate/src/debugger/debuggeragent.cpp b/engine/timetablemate/src/debugger/debuggeragent.cpp index 1d24d0a..bc8fed1 100644 --- a/engine/timetablemate/src/debugger/debuggeragent.cpp +++ b/engine/timetablemate/src/debugger/debuggeragent.cpp @@ -1,1685 +1,1685 @@ /* * Copyright 2012 Friedrich Pülz * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "debuggeragent.h" // Own includes #include "backtracemodel.h" #include "breakpointmodel.h" #include "variablemodel.h" // PublicTransport engine includes #include // KDE includes #include #include // Qt includes #include #include #include #include #include #include #include #include #include // Only used for debugging namespace Debugger { // In milliseconds const int DebuggerAgent::CHECK_RUNNING_INTERVAL = 1000; const int DebuggerAgent::CHECK_RUNNING_WHILE_INTERRUPTED_INTERVAL = 5000; QScriptValue debugPrintFunction( QScriptContext *context, QScriptEngine *engine ) { QString result; for ( int i = 0; i < context->argumentCount(); ++i ) { if ( i > 0 ) { result.append(" "); } result.append( context->argument(i).toString() ); } QScriptValue calleeData = context->callee().data(); DebuggerAgent *debugger = qobject_cast( calleeData.toQObject() ); debugger->slotOutput( result, QScriptContextInfo(context->parentContext()) ); return engine->undefinedValue(); } DebuggerAgent::DebuggerAgent( QScriptEngine *engine, QSemaphore *engineSemaphore, bool mutexIsLocked ) : QObject(engine), QScriptEngineAgent(engine), m_mutex(new QMutex(QMutex::Recursive)), m_interruptWaiter(new QWaitCondition()), m_interruptMutex(new QMutex()), m_engineSemaphore(engineSemaphore), m_checkRunningTimer(new QTimer(this)), m_lastRunAborted(false), m_runUntilLineNumber(-1), m_debugFlags(DefaultDebugFlags), m_currentContext(0), m_interruptContext(0) { qRegisterMetaType< VariableChange >( "VariableChange" ); qRegisterMetaType< BacktraceChange >( "BacktraceChange" ); qRegisterMetaType< BreakpointChange >( "BreakpointChange" ); qRegisterMetaType< Breakpoint >( "Breakpoint" ); - m_mutex->lockInline(); + m_mutex->lock(); m_lineNumber = -1; m_columnNumber = -1; m_currentScriptId = -1; m_state = NotRunning; m_injectedScriptState = InjectedScriptNotRunning; m_injectedScriptId = -1; m_executionControl = ExecuteRun; m_previousExecutionControl = ExecuteRun; m_hasUncaughtException = false; m_uncaughtExceptionLineNumber = -1; m_interruptFunctionLevel = -2; m_functionDepth = 0; - m_mutex->unlockInline(); + m_mutex->unlock(); // Install custom print function (overwriting the builtin print function) if ( !mutexIsLocked ) { m_engineSemaphore->acquire(); } QScriptValue printFunction = engine->newFunction( debugPrintFunction ); printFunction.setData( engine->newQObject(this) ); QScriptValue::PropertyFlags flags = QScriptValue::ReadOnly | QScriptValue::Undeletable; engine->globalObject().setProperty( "print", printFunction, flags ); if ( !mutexIsLocked ) { m_engineSemaphore->release(); } } DebuggerAgent::~DebuggerAgent() { abortDebugger(); delete m_mutex; delete m_interruptMutex; delete m_interruptWaiter; } bool DebuggerAgent::isInterrupted() const { QMutexLocker locker( m_mutex ); if ( m_state == Interrupted ) { return true; } const bool canLock = m_interruptMutex->tryLock(); if ( canLock ) { - m_interruptMutex->unlockInline(); + m_interruptMutex->unlock(); } return !canLock; } bool DebuggerAgent::isRunning() const { QMutexLocker locker( m_mutex ); return m_state == Running; } bool DebuggerAgent::isAborting() const { QMutexLocker locker( m_mutex ); return m_state == Aborting; } bool DebuggerAgent::wasLastRunAborted() const { QMutexLocker locker( m_mutex ); return m_lastRunAborted; } bool DebuggerAgent::isInjectedScriptEvaluating() const { QMutexLocker locker( m_mutex ); return m_injectedScriptState != InjectedScriptNotRunning && m_injectedScriptState != InjectedScriptAborting; } void DebuggerAgent::finish() const { - m_mutex->lockInline(); + m_mutex->lock(); if ( m_state != NotRunning ) { DEBUGGER_EVENT("Wait until script execution finishes..."); QEventLoop loop; connect( this, SIGNAL(stopped(QDateTime,bool,bool,int,QString,QStringList)), &loop, SLOT(quit()) ); - m_mutex->unlockInline(); + m_mutex->unlock(); loop.exec(); DEBUGGER_EVENT("...Script execution has finished"); } else { - m_mutex->unlockInline(); + m_mutex->unlock(); } } NextEvaluatableLineHint DebuggerAgent::canBreakAt( int lineNumber, const QStringList &programLines ) { kDebug() << "canBreakAt(" << lineNumber << "), code lines:" << programLines.count(); const int scriptLineCount = programLines.count(); if ( lineNumber < 1 || lineNumber > scriptLineCount ) { return CannotFindNextEvaluatableLine; } QString line = programLines[ lineNumber - 1 ].trimmed(); if ( line.isEmpty() || line.startsWith("//") ) { return NextEvaluatableLineBelow; } else if ( line.startsWith("/**") ) { return NextEvaluatableLineAbove; } // Test if the line can be evaluated // If not, try if appending more lines makes the text evaluatable (multiline statement) for ( int lines = 1; lines < 20 && lineNumber + lines <= scriptLineCount; ++lines ) { QScriptSyntaxCheckResult result = QScriptEngine::checkSyntax( line ); if ( result.state() == QScriptSyntaxCheckResult::Valid ) { return FoundEvaluatableLine; } line.append( '\n' + programLines[lineNumber - 1 + lines] ); } return NextEvaluatableLineAbove; } int DebuggerAgent::getNextBreakableLineNumber( int lineNumber, const QStringList &programLines ) { kDebug() << "getNextBreakableLineNumber(" << lineNumber << "), code lines:" << programLines.count(); for ( int distance = 0; distance < 15; ++distance ) { const int lineNumber1 = lineNumber + distance; if ( canBreakAt(lineNumber1, programLines) == FoundEvaluatableLine ) { return lineNumber1; } const int lineNumber2 = lineNumber - distance; if ( lineNumber1 != lineNumber2 && canBreakAt(lineNumber2, programLines) == FoundEvaluatableLine ) { return lineNumber2; } } return -1; } NextEvaluatableLineHint DebuggerAgent::canBreakAt( const QString &fileName, int lineNumber ) const { kDebug() << "canBreakAt(" << QFileInfo(fileName).fileName() << "," << lineNumber << ")"; QMutexLocker locker( m_mutex ); return canBreakAt( lineNumber, m_scriptLines[fileName] ); } int DebuggerAgent::getNextBreakableLineNumber( const QString &fileName, int lineNumber ) const { kDebug() << "getNextBreakableLineNumber(" << QFileInfo(fileName).fileName() << "," << lineNumber << ")"; QMutexLocker locker( m_mutex ); return getNextBreakableLineNumber( lineNumber, m_scriptLines[fileName] ); } QString DebuggerAgent::stateToString( DebuggerState state ) { switch ( state ) { case NotRunning: return i18nc("@info/plain Debugger state", "Not running"); case Running: return i18nc("@info/plain Debugger state", "Running"); case Interrupted: return i18nc("@info/plain Debugger state", "Interrupted"); case Aborting: return i18nc("@info/plain Debugger state", "Aborting"); default: return "Unknown"; } } bool DebuggerAgent::executeCommand( const ConsoleCommand &command, QString *returnValue ) { if ( !command.isValid() ) { return false; } switch ( command.command() ) { case ConsoleCommand::HelpCommand: if ( returnValue ) { if ( !command.arguments().isEmpty() ) { // "help" command with at least one argument const ConsoleCommand::Command commandType = ConsoleCommand::commandFromName( command.argument() ); *returnValue = i18nc("@info", "Command %1: %2" "Syntax: %3", command.argument(), ConsoleCommand::commandDescription(commandType), ConsoleCommand::commandSyntax(commandType)); } else { // "help" command without arguments *returnValue = i18nc("@info", "Type a command beginning with a point ('.') or " "JavaScript code. Available commands: %1" "Use .help with an argument to get more information " "about individual commands" "Syntax: %2", ConsoleCommand::availableCommands().join(", "), ConsoleCommand::commandSyntax(command.command()) ); } } return true; case ConsoleCommand::ClearCommand: qWarning() << "ClearCommand needs to be implemented outside of DebuggerAgent"; return true; case ConsoleCommand::LineNumberCommand: if ( returnValue ) { *returnValue = QString::number( lineNumber() ); } return true; case ConsoleCommand::BreakpointCommand: { bool ok; int lineNumber = command.argument().toInt( &ok ); if ( !ok ) { *returnValue = i18nc("@info", "Invalid argument '%1', expected a line number", command.argument()); return false; } // TODO: Add argument to control breakpoints in external scripts m_mutex->lock(); const QString mainScriptFileName = m_mainScriptFileName; m_mutex->unlock(); lineNumber = getNextBreakableLineNumber( mainScriptFileName, lineNumber ); ok = lineNumber >= 0; if ( !ok ) { *returnValue = i18nc("@info", "Cannot interrupt script execution at line %1", lineNumber); return false; } // TODO Use filename from a new argument const QHash< uint, Breakpoint > &breakpoints = breakpointsForFile( mainScriptFileName ); const bool breakpointExists = breakpoints.contains( lineNumber ); kDebug() << "Breakpoint exists" << breakpointExists; Breakpoint breakpoint = breakpointExists ? breakpoints[lineNumber] : Breakpoint( mainScriptFileName, lineNumber ); if ( command.arguments().count() == 1 ) { // Only ".break ", no command to execute // Return information about the breakpoint if ( breakpointExists ) { *returnValue = i18nc("@info", "Breakpoint at line %1: %2 hits, %3, %4", lineNumber, breakpoint.hitCount(), breakpoint.isEnabled() ? i18nc("@info", "enabled") : i18nc("@info", "disabled"), breakpoint.condition().isEmpty() ? i18nc("@info", "No condition") : i18nc("@info", "Condition: %1", breakpoint.condition()) ); } else { *returnValue = i18nc("@info", "No breakpoint found at line %1", lineNumber); } return breakpointExists; } // More than one argument given, ie. more than ".break ..." const QString &argument = command.arguments().count() == 1 ? QString() : command.argument( 1 ); bool errorNotFound = false; QRegExp maxhitRegExp( "^maxhits(?:=|:)(\\d+)$", Qt::CaseInsensitive ); if ( command.arguments().count() == 1 || argument.compare(QLatin1String("add")) == 0 ) { emit breakpointsChanged( BreakpointChange(AddBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint added at line %1", lineNumber) : i18nc("@info", "Cannot add breakpoint at line %1", lineNumber); } else if ( argument.compare(QLatin1String("remove")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else { emit breakpointsChanged( BreakpointChange(RemoveBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint at line %1 removed", lineNumber) : i18nc("@info", "Cannot remove breakpoint at line %1", lineNumber); } } else if ( argument.compare(QLatin1String("toggle")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else { breakpoint.setEnabled( !breakpoint.isEnabled() ); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint toggled at line %1", lineNumber) : i18nc("@info", "Cannot toggle breakpoint at line %1", lineNumber); } } else if ( argument.compare(QLatin1String("enable")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else { breakpoint.setEnabled(); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint enabled at line %1", lineNumber) : i18nc("@info", "Cannot enable breakpoint at line %1", lineNumber); } } else if ( argument.compare(QLatin1String("disable")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else { breakpoint.setEnabled( false ); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint disabled at line %1", lineNumber) : i18nc("@info", "Cannot disable breakpoint at line %1", lineNumber); } } else if ( argument.compare(QLatin1String("reset")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else { breakpoint.reset(); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint reset at line %1", lineNumber) : i18nc("@info", "Cannot reset breakpoint at line %1", lineNumber); } } else if ( argument.compare(QLatin1String("condition")) == 0 ) { if ( !breakpointExists ) { errorNotFound = true; } else if ( command.arguments().count() < 3 ) { // Needs at least 3 arguments: ".break condition " *returnValue = i18nc("@info", "Condition code missing"); } else { breakpoint.setCondition( QStringList(command.arguments().mid(2)).join(" ") ); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint condition set to %1" " at line %2", breakpoint.condition(), lineNumber) : i18nc("@info", "Cannot set breakpoint condition to %1" " at line %1", breakpoint.condition(), lineNumber); } } else if ( maxhitRegExp.indexIn(argument) != -1 ) { if ( !breakpointExists ) { errorNotFound = true; } else { breakpoint.setMaximumHitCount( maxhitRegExp.cap(1).toInt() ); emit breakpointsChanged( BreakpointChange(UpdateBreakpoint, breakpoint) ); *returnValue = ok ? i18nc("@info", "Breakpoint changed at line %1", lineNumber) : i18nc("@info", "Cannot change breakpoint at line %1", lineNumber); } } else { kDebug() << "Unexcepted argument:" << argument; ok = false; if ( returnValue ) { *returnValue = i18nc("@info", "Unexcepted argument: %1Excepted: " "add (default), " "remove, " "toggle, " "enable, " "disable, " "reset, " "condition=<conditionCode>, " "maxhits=<number>", argument); } } if ( errorNotFound ) { ok = false; if ( returnValue ) { *returnValue = i18nc("@info", "No breakpoint found at line %1", lineNumber ); } } return ok; } case ConsoleCommand::DebuggerControlCommand: { const QString argument = command.argument(); if ( argument == QLatin1String("status") && returnValue ) { - m_mutex->lockInline(); + m_mutex->lock(); *returnValue = i18nc("@info", "Debugger status: %1", stateToString(m_state)); if ( m_state != NotRunning ) { *returnValue += ", " + i18nc("@info", "line %1", m_lineNumber); } if ( m_hasUncaughtException ) { *returnValue += ", " + i18nc("@info", "uncaught exception in line %1: %2", m_uncaughtExceptionLineNumber, m_uncaughtException.toString()); } - m_mutex->unlockInline(); + m_mutex->unlock(); return true; } else { ConsoleCommandExecutionControl executionControl = consoleCommandExecutionControlFromString( argument ); if ( executionControl != InvalidControlExecution ) { QString errorMessage; const bool ok = debugControl( executionControl, command.argument(1), &errorMessage ); if ( returnValue ) { if ( ok ) { *returnValue = i18nc("@info", "Command successfully executed"); } else { *returnValue = i18nc("@info", "Cannot execute command: %1", errorMessage); } } return ok; } else if ( returnValue ) { *returnValue = i18nc("@info", "Unexcepted argument %1" "Expected one of these: " "status, " "continue, " "interrupt, " "abort, " "stepinto <count = 1>, " "stepover <count = 1>, " "stepout <count = 1>, " "rununtil <lineNumber>", command.argument()); return false; } } } case ConsoleCommand::DebugCommand: { bool error; int errorLineNumber; QString errorMessage; QStringList backtrace; QScriptValue result = evaluateInContext( command.arguments().join(" "), i18nc("@info/plain", "Console Debug Command"), &error, &errorLineNumber, &errorMessage, &backtrace, InterruptAtStart ); if ( error ) { if ( returnValue ) { *returnValue = i18nc("@info", "Error: %1" "Backtrace: %2", errorMessage, backtrace.join("
")); } } else if ( returnValue ) { *returnValue = result.toString(); } return !error; } default: kDebug() << "Command execution not implemented" << command.command(); return false; } } void DebuggerAgent::continueToDoSomething() { QMutexLocker locker( m_mutex ); if ( m_state != Interrupted ) { kDebug() << "Debugger is not interrupted" << m_state; return; } // Wake from interrupt, then emit doSomething() and directly interrupt again m_executionControl = ExecuteInterrupt; m_interruptWaiter->wakeAll(); } QScriptValue DebuggerAgent::evaluateInContext( const QString &program, const QString &contextName, bool *hadUncaughtException, int *errorLineNumber, QString *errorMessage, QStringList *backtrace, DebugFlags debugFlags ) { // Use new context for program evaluation QScriptContext *context = engine()->pushContext(); // Store current execution type/debug flags, to restore it later - m_mutex->lockInline(); + m_mutex->lock(); const ExecutionControl executionType = m_executionControl; const DebugFlags oldDebugFlags = m_debugFlags; m_debugFlags = debugFlags; - m_mutex->unlockInline(); + m_mutex->unlock(); QTimer timer; if ( debugFlags.testFlag(InterruptAtStart) ) { debugStepIntoInjectedProgram(); } else { debugRunInjectedProgram(); // Start a countdown, if evaluation does not finish within this countdown, it gets aborted timer.setSingleShot( true ); connect( &timer, SIGNAL(timeout()), this, SLOT(cancelInjectedCodeExecution()) ); timer.start( 3000 ); // 3 seconds time for the evaluation } // Evaluate program QScriptValue result = engine()->evaluate( program, contextName.isEmpty() ? "" : contextName ); timer.stop(); // Stop cancel timeout // Restore previous execution type (if not interrupted) if ( debugFlags.testFlag(InterruptAtStart) ) { - m_mutex->lockInline(); + m_mutex->lock(); m_executionControl = executionType; - m_mutex->unlockInline(); + m_mutex->unlock(); } // Restore previous debug flags if ( debugFlags != oldDebugFlags ) { - m_mutex->lockInline(); + m_mutex->lock(); m_debugFlags = oldDebugFlags; - m_mutex->unlockInline(); + m_mutex->unlock(); } DEBUGGER_EVENT("Evaluate-in-context result" << result.toString() << program); if ( hadUncaughtException ) { *hadUncaughtException = engine()->hasUncaughtException(); } if ( errorLineNumber ) { *errorLineNumber = engine()->uncaughtExceptionLineNumber(); } if ( errorMessage ) { *errorMessage = engine()->uncaughtException().toString(); } if ( backtrace ) { *backtrace = engine()->uncaughtExceptionBacktrace(); } if ( engine()->hasUncaughtException() ) { kDebug() << "Uncaught exception in program:" << engine()->uncaughtExceptionBacktrace(); engine()->clearExceptions(); } engine()->popContext(); // Transfer values from evaluation context to script context QScriptValueIterator it( context->activationObject() ); QScriptValue scriptContext = engine()->currentContext()->activationObject(); if ( it.hasNext() ) { it.next(); scriptContext.setProperty( it.name(), it.value() ); } return result; } void DebuggerAgent::cancelInjectedCodeExecution() { QMutexLocker locker( m_mutex ); if ( m_injectedScriptState == InjectedScriptEvaluating ) { DEBUGGER_EVENT("Evaluation did not finish in time or was cancelled"); m_executionControl = ExecuteAbortInjectedProgram; } else { // Is not running injected code checkHasExited(); } wakeFromInterrupt(); } void DebuggerAgent::addBreakpoint( const Breakpoint &breakpoint ) { QHash< uint, Breakpoint > &breakpoints = breakpointsForFile( breakpoint.fileName() ); breakpoints.insert( breakpoint.lineNumber(), breakpoint ); } void DebuggerAgent::removeBreakpoint( const Breakpoint &breakpoint ) { QHash< uint, Breakpoint > &breakpoints = breakpointsForFile( breakpoint.fileName() ); breakpoints.remove( breakpoint.lineNumber() ); } bool DebuggerAgent::debugControl( DebuggerAgent::ConsoleCommandExecutionControl controlType, const QVariant &argument, QString *errorMessage ) { switch ( controlType ) { case ControlExecutionContinue: if ( !isInterrupted() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not interrupted!"); } return false; } debugContinue(); break; case ControlExecutionInterrupt: if ( !isRunning() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not running! Start the debugger first."); } return false; } debugInterrupt(); break; case ControlExecutionAbort: if ( !isRunning() && !isInterrupted() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not running or interrupted! Start the debugger first."); } return false; } abortDebugger(); break; case ControlExecutionStepInto: if ( !isInterrupted() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not interrupted!"); } return false; } debugStepInto( qMax(0, argument.toInt() - 1) ); break; case ControlExecutionStepOver: if ( !isInterrupted() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not interrupted!"); } return false; } debugStepOver( qMax(0, argument.toInt() - 1) ); break; case ControlExecutionStepOut: if ( !isInterrupted() ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger is not interrupted!"); } return false; } debugStepOut( qMax(0, argument.toInt() - 1) ); break; case ControlExecutionRunUntil: { bool ok; int lineNumber = argument.toInt( &ok ); - m_mutex->lockInline(); + m_mutex->lock(); const int scriptLineCount = m_scriptLines[m_mainScriptFileName].count(); const QString mainScriptFileName = m_mainScriptFileName; - m_mutex->unlockInline(); + m_mutex->unlock(); if ( !argument.isValid() || !ok ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Invalid argument '%1', expected line number!", argument.toString() ); } return false; } else if ( lineNumber < 1 || lineNumber > scriptLineCount ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Invalid line number %1! Must be between 1 and %2", lineNumber, scriptLineCount); } return false; } // TODO: Add argument to run until a line in an external script lineNumber = getNextBreakableLineNumber( mainScriptFileName, lineNumber ); if ( lineNumber < 0 ) { if ( errorMessage ) { *errorMessage = i18nc("@info", "Cannot interrupt script execution at line %!", lineNumber); } return false; } debugRunUntilLineNumber( mainScriptFileName, lineNumber ); break; } case InvalidControlExecution: kDebug() << "Invalid control execution type"; break; default: kDebug() << "Unknown Debugger::ExecutionControl" << controlType; if ( errorMessage ) { *errorMessage = i18nc("@info", "Debugger command %1 not implemented!", controlType ); } return false; } return true; } DebuggerAgent::ConsoleCommandExecutionControl DebuggerAgent::consoleCommandExecutionControlFromString( const QString &str ) { const QString _str = str.trimmed().toLower(); if ( _str == QLatin1String("continue") ) { return ControlExecutionContinue; } else if ( _str == QLatin1String("interrupt") ) { return ControlExecutionInterrupt; } else if ( _str == QLatin1String("abort") ) { return ControlExecutionAbort; } else if ( _str == QLatin1String("stepinto") ) { return ControlExecutionStepInto; } else if ( _str == QLatin1String("stepover") ) { return ControlExecutionStepOver; } else if ( _str == QLatin1String("stepout") ) { return ControlExecutionStepOut; } else if ( _str == QLatin1String("rununtil") ) { return ControlExecutionRunUntil; } else { return InvalidControlExecution; } } void DebuggerAgent::setExecutionControlType( ExecutionControl executionType ) { QMutexLocker locker( m_mutex ); m_executionControl = executionType; m_repeatExecutionTypeCount = 0; // If execution type is repeatable, ie. stepInto/stepOver/stepOut } void DebuggerAgent::abortDebugger() { DEBUGGER_CONTROL("abortDebugger"); QMutexLocker locker( m_mutex ); const DebuggerState state = m_state; switch ( state ) { case Aborting: DEBUGGER_EVENT("Is already aborting"); return; case NotRunning: if ( m_injectedScriptState == InjectedScriptEvaluating ) { cancelInjectedCodeExecution(); } return; case Running: case Interrupted: default: DEBUGGER_EVENT("Abort"); m_lastRunAborted = true; m_executionControl = ExecuteAbort; setState( Aborting ); engine()->abortEvaluation(); if ( m_injectedScriptState == InjectedScriptEvaluating ) { cancelInjectedCodeExecution(); } break; } wakeFromInterrupt( state ); } void DebuggerAgent::debugInterrupt() { QMutexLocker locker( m_mutex ); DEBUGGER_CONTROL( "debugInterrupt" ); if ( m_state != Running ) { kDebug() << "Debugger is not running" << m_state; return; } m_executionControl = ExecuteInterrupt; } void DebuggerAgent::debugContinue() { QMutexLocker locker( m_mutex ); DEBUGGER_CONTROL( "debugContinue" ); if ( m_state != Interrupted ) { kDebug() << "Debugger is not interrupted" << m_state; return; } m_executionControl = ExecuteRun; m_interruptWaiter->wakeAll(); } void DebuggerAgent::debugStepInto( int repeat ) { DEBUGGER_CONTROL2( "debugStepInto", repeat ); QMutexLocker locker( m_mutex ); if ( m_state != Interrupted ) { kDebug() << "Debugger is not interrupted" << m_state; return; } // Wake from interrupt and run until the next statement m_repeatExecutionTypeCount = repeat; m_executionControl = ExecuteStepInto; m_interruptWaiter->wakeAll(); } void DebuggerAgent::debugStepOver( int repeat ) { DEBUGGER_CONTROL2( "debugStepOver", repeat ); QMutexLocker locker( m_mutex ); if ( m_state != Interrupted ) { kDebug() << "Debugger is not interrupted" << m_state; return; } // Wake from interrupt and run until the next statement in the function level m_interruptFunctionLevel = 0; m_repeatExecutionTypeCount = repeat; m_executionControl = ExecuteStepOver; m_interruptWaiter->wakeAll(); } void DebuggerAgent::debugStepOut( int repeat ) { DEBUGGER_CONTROL2( "debugStepOut", repeat ); QMutexLocker locker( m_mutex ); if ( m_state != Interrupted) { kDebug() << "Debugger is not interrupted" << m_state; return; } // Wake from interrupt and run until the current function gets exited m_interruptFunctionLevel = 0; m_repeatExecutionTypeCount = repeat; m_executionControl = ExecuteStepOut; m_interruptWaiter->wakeAll(); } void DebuggerAgent::debugRunUntilLineNumber( const QString &fileName, int lineNumber ) { DEBUGGER_CONTROL3( "debugRunUntilLineNumber", QFileInfo(fileName).fileName(), lineNumber ); int runUntilLineNumber = getNextBreakableLineNumber( fileName, lineNumber ); QMutexLocker locker( m_mutex ); m_runUntilLineNumber = runUntilLineNumber; if ( runUntilLineNumber != -1 ) { m_executionControl = ExecuteRun; wakeFromInterrupt(); } } void DebuggerAgent::debugRunInjectedProgram() { QMutexLocker locker( m_mutex ); DEBUGGER_CONTROL("debugRunInjectedProgram"); m_repeatExecutionTypeCount = 0; m_injectedScriptState = InjectedScriptInitializing; // Update in next scriptLoad() m_previousExecutionControl = m_executionControl; m_executionControl = ExecuteRunInjectedProgram; // Do not wake from interrupt here, because the script should stay interrupted, // while the injected program runs in another thread } void DebuggerAgent::debugStepIntoInjectedProgram() { QMutexLocker locker( m_mutex ); DEBUGGER_CONTROL("debugStepIntoInjectedProgram"); m_repeatExecutionTypeCount = 0; m_injectedScriptState = InjectedScriptInitializing; // Update in next scriptLoad() m_previousExecutionControl = m_executionControl; m_executionControl = ExecuteStepIntoInjectedProgram; // Do not wake from interrupt here, because the script should stay interrupted, // while the injected program runs in another thread } DebuggerState DebuggerAgent::state() const { QMutexLocker locker( m_mutex ); return m_state; } QString DebuggerAgent::mainScriptFileName() const { QMutexLocker locker( m_mutex ); return m_mainScriptFileName; } void DebuggerAgent::setMainScriptFileName( const QString &mainScriptFileName ) { QMutexLocker locker( m_mutex ); m_mainScriptFileName = mainScriptFileName; } void DebuggerAgent::setScriptText( const QString &fileName, const QString &program ) { QMutexLocker locker( m_mutex ); m_scriptLines[ fileName ] = program.split( '\n', QString::KeepEmptyParts ); } void DebuggerAgent::scriptLoad( qint64 id, const QString &program, const QString &fileName, int baseLineNumber ) { Q_UNUSED( baseLineNumber ); if ( id != -1 ) { QMutexLocker locker( m_mutex ); if ( m_state != Running && !isInjectedScriptEvaluating() ) { // Call fireup() in case a script was evaluated fireup(); } m_scriptIdToFileName.insert( id, fileName ); m_currentScriptId = id; if ( m_injectedScriptState == InjectedScriptInitializing ) { // The new script is code that should be executed in the current scripts context // while the main script is interrupted m_injectedScriptId = id; m_injectedScriptState = InjectedScriptEvaluating; } else if ( m_executionControl != ExecuteRunInjectedProgram && m_executionControl != ExecuteStepIntoInjectedProgram ) { DEBUGGER_EVENT( "Load script" << QFileInfo(fileName).fileName() << "with id" << id << "for provider" << QFileInfo(m_mainScriptFileName ).baseName()); setScriptText( fileName, program ); } } } void DebuggerAgent::scriptUnload( qint64 id ) { Q_UNUSED( id ); QMutexLocker locker( m_mutex ); if ( m_injectedScriptState == InjectedScriptUpdateVariablesInParentContext ) { m_injectedScriptState = InjectedScriptNotRunning; QTimer::singleShot( 0, this, SLOT(wakeFromInterrupt()) ); } DEBUGGER_EVENT( "Unload script" << m_scriptIdToFileName[id] << "with id" << id ); m_scriptIdToFileName.remove( id ); if ( m_currentScriptId == id ) { m_currentScriptId = -1; if ( m_lineNumber == -1 && m_state == Running ) { shutdown(); } } } void DebuggerAgent::wakeFromInterrupt( DebuggerState unmodifiedState ) { QMutexLocker locker( m_mutex ); if ( unmodifiedState == Interrupted ) { m_interruptWaiter->wakeAll(); } } void DebuggerAgent::contextPush() { } void DebuggerAgent::contextPop() { } void DebuggerAgent::functionEntry( qint64 scriptId ) { if ( scriptId != -1 ) { QMutexLocker locker( m_mutex ); ++m_functionDepth; if ( m_interruptFunctionLevel >= -1 && (m_executionControl == ExecuteStepOver || m_executionControl == ExecuteStepOut) ) { ++m_interruptFunctionLevel; } emit variablesChanged( VariableChange(PushVariableStack) ); emit backtraceChanged( BacktraceChange(PushBacktraceFrame) ); } } void DebuggerAgent::functionExit( qint64 scriptId, const QScriptValue& returnValue ) { if ( scriptId != -1 ) { emit variablesChanged( VariableChange(PopVariableStack) ); emit backtraceChanged( BacktraceChange(PopBacktraceFrame) ); } QMutexLocker locker( m_mutex ); if ( scriptId != -1 && m_injectedScriptId == scriptId ) { DEBUGGER_EVENT( "Evaluation in context finished with" << returnValue.toString() ); m_executionControl = m_previousExecutionControl; emit evaluationInContextFinished( returnValue ); // Interrupts again if it was interrupted before, // but variables can be updated in the script context m_injectedScriptState = InjectedScriptUpdateVariablesInParentContext; } else { if ( scriptId != -1 && m_interruptFunctionLevel >= 0 ) { if ( m_executionControl == ExecuteStepOver ) { // m_interruptFunctionLevel may be 0 here, if the script exits in one function, // waiting for a signal to continue script execution at a connected slot --m_interruptFunctionLevel; } else if ( m_executionControl == ExecuteStepOut ) { --m_interruptFunctionLevel; } } } if ( scriptId != -1 ) { if ( m_executionControl == ExecuteAbort || m_state == Aborting ) { // Do nothing when aborting, changing m_functionDepth when aborting // leads to problems when starting the script again return; } --m_functionDepth; const int functionDepth = m_functionDepth; if ( functionDepth == 0 ) { // Engine mutex is still locked here to protect the engine while it is executing, // unlock after execution has ended here shutdown(); } } } ExecutionControl DebuggerAgent::applyExecutionControl( ExecutionControl executionControl, QScriptContext *currentContext ) { QMutexLocker locker( m_mutex ); Q_UNUSED( currentContext ); switch ( executionControl ) { case ExecuteStepInto: case ExecuteStepIntoInjectedProgram: // Decrease repeation counter, if it is at 0 interrupt if ( m_repeatExecutionTypeCount > 0 ) { --m_repeatExecutionTypeCount; } else if ( m_repeatExecutionTypeCount == 0 ) { if ( m_executionControl != ExecuteAbort && m_executionControl != ExecuteAbortInjectedProgram ) { m_executionControl = executionControl = ExecuteInterrupt; } } break; case ExecuteStepOver: if ( m_interruptFunctionLevel == 0 ) { if ( m_repeatExecutionTypeCount > 0 ) { --m_repeatExecutionTypeCount; } else if ( m_repeatExecutionTypeCount == 0 ) { m_interruptFunctionLevel = -2; if ( m_executionControl != ExecuteAbort && m_executionControl != ExecuteAbortInjectedProgram ) { m_executionControl = executionControl = ExecuteInterrupt; } } } break; case ExecuteStepOut: if ( m_interruptFunctionLevel == -1 ) { if ( m_repeatExecutionTypeCount > 0 ) { --m_repeatExecutionTypeCount; } else if ( m_repeatExecutionTypeCount == 0 ) { m_interruptFunctionLevel = -2; if ( m_executionControl != ExecuteAbort && m_executionControl != ExecuteAbortInjectedProgram ) { m_executionControl = executionControl = ExecuteInterrupt; } } } break; case ExecuteRun: // case ExecuteNotRunning: case ExecuteInterrupt: case ExecuteRunInjectedProgram: default: break; } return executionControl; } void DebuggerAgent::emitChanges() { m_engineSemaphore->acquire(); QMutexLocker locker( m_mutex ); QScriptContext *context = engine()->currentContext(); m_currentContext = context; if ( context ) { // Construct backtrace/variable change objects // The Frame object is already created and added to the model with empty values // (in DebuggerAgent::functionEntry()) const bool isGlobal = context->thisObject().equals( engine()->globalObject() ); const Frame frame( QScriptContextInfo(context), isGlobal ); const BacktraceChange backtraceChange( UpdateBacktraceFrame, frame ); const VariableChange variableChange = VariableChange::fromContext( context ); // Emit changes emit variablesChanged( variableChange ); emit backtraceChanged( backtraceChange ); } m_engineSemaphore->release(); } void DebuggerAgent::positionChange( qint64 scriptId, int lineNumber, int columnNumber ) { Q_UNUSED( scriptId ); // Lock the engine if not already locked (should normally be locked before script execution, // but it may get unlocked before the script is really done, eg. waiting idle for network // requests to finish) QScriptContext *currentContext = engine()->currentContext(); // Unlock now, maybe trying to lock above was successful or the engine was already locked m_engineSemaphore->release(); // Lock member variables and initialize - m_mutex->lockInline(); + m_mutex->lock(); ExecutionControl executionControl = m_executionControl; const bool isAborting = executionControl == ExecuteAbort || m_state == Aborting; DEBUGGER_EVENT_POS_CHANGED("Position changed to line" << lineNumber << "colum" << columnNumber << "in file" << m_scriptIdToFileName[scriptId] << "- Execution type:" << executionControl); const bool injectedProgram = isInjectedScriptEvaluating(); if ( !injectedProgram && m_state == NotRunning ) { // Execution has just started - m_mutex->unlockInline(); + m_mutex->unlock(); fireup(); - m_mutex->lockInline(); + m_mutex->lock(); } const int oldLineNumber = m_lineNumber; const int oldColumnNumber = m_columnNumber; DebuggerState state = m_state; // Decide if execution should be interrupted (breakpoints, execution control value, eg. step into). if ( !injectedProgram && !isAborting ) { // Update current execution position, // before emitting breakpointReached() and positionChanged() m_currentScriptId = scriptId; m_lineNumber = lineNumber; m_columnNumber = columnNumber; // Check if execution should be interrupted at the current line, because of a // runUntilLineNumber command if ( m_runUntilLineNumber == lineNumber ) { if ( executionControl != ExecuteAbort && executionControl != ExecuteAbortInjectedProgram ) { m_executionControl = executionControl = ExecuteInterrupt; m_runUntilLineNumber = -1; } } // Check for breakpoints at the current line if ( m_debugFlags.testFlag(InterruptOnBreakpoints) ) { Breakpoint *breakpoint = 0; bool conditionError = false; if ( findActiveBreakpoint(lineNumber, breakpoint, &conditionError) ) { // Reached a breakpoint, applyBreakpoints() may have written // a new value in m_executionControl (ExecuteInterrupt) if ( executionControl != ExecuteAbort && executionControl != ExecuteAbortInjectedProgram ) { m_executionControl = executionControl = ExecuteInterrupt; } - m_mutex->unlockInline(); + m_mutex->unlock(); emit breakpointReached( *breakpoint ); } else if ( conditionError ) { // There was an error with the condition of the breakpoint // Interrupt to let the user fix the condition code m_executionControl = executionControl = ExecuteInterrupt; - m_mutex->unlockInline(); + m_mutex->unlock(); } else { // No breakpoint reached m_executionControl = executionControl = applyExecutionControl( executionControl, currentContext ); - m_mutex->unlockInline(); + m_mutex->unlock(); } } else { m_executionControl = executionControl = applyExecutionControl( executionControl, currentContext ); - m_mutex->unlockInline(); + m_mutex->unlock(); } if ( executionControl == ExecuteInterrupt ) { emit positionChanged( lineNumber, columnNumber, oldLineNumber, oldColumnNumber ); // Interrupt script execution DEBUGGER_EVENT("Interrupt now"); doInterrupt( injectedProgram ); // Script execution continued, update values - m_mutex->lockInline(); + m_mutex->lock(); executionControl = m_executionControl; state = m_state; - m_mutex->unlockInline(); + m_mutex->unlock(); } } else { // Do not update execution position or check for breakpoints, // if in an injected program or aborting - m_mutex->unlockInline(); + m_mutex->unlock(); } // Check if debugging should be aborted if ( executionControl == ExecuteAbort ) { - m_mutex->lockInline(); + m_mutex->lock(); m_injectedScriptId = -1; m_injectedScriptState = InjectedScriptNotRunning; - m_mutex->unlockInline(); + m_mutex->unlock(); // TODO const bool locked = m_engineSemaphore->tryAcquire( 1, 250 ); engine()->abortEvaluation(); if ( !locked ) { qWarning() << "Could not lock the engine"; } shutdown(); } else if ( executionControl == ExecuteAbortInjectedProgram ) { // Restore member variables - m_mutex->lockInline(); + m_mutex->lock(); DEBUGGER_EVENT("Abort injected program"); m_injectedScriptState = InjectedScriptAborting; // Handled in doInterrupt m_executionControl = m_previousExecutionControl; - m_mutex->unlockInline(); + m_mutex->unlock(); // TEST // Interrupt execution of injected script code, // it should be aborted by terminating the executing thread doInterrupt( true ); } else if ( state != NotRunning && state != Aborting ) { // Protect further script execution m_engineSemaphore->acquire(); } } QScriptContextInfo DebuggerAgent::contextInfo() { if ( m_engineSemaphore->tryAcquire(1, 200) ) { const QScriptContextInfo info( engine()->currentContext() ); m_engineSemaphore->release(); return info; } else { kDebug() << "Engine is locked (not interrupted), cannot get context info"; return QScriptContextInfo(); } } bool DebuggerAgent::findActiveBreakpoint( int lineNumber, Breakpoint *&foundBreakpoint, bool *conditionError ) { QMutexLocker locker( m_mutex ); if ( m_currentScriptId == -1 ) { kDebug() << "scriptId == -1"; return false; } // Test for a breakpoint at the new line number QHash< uint, Breakpoint > &breakpoints = currentBreakpoints(); if ( !breakpoints.contains(lineNumber) ) { // No breakpoint at the current execution position return false; } Breakpoint &breakpoint = breakpoints[ lineNumber ]; if ( !breakpoint.isValid() ) { // No breakpoint for the current file found return false; } else if ( breakpoint.isEnabled() ) { // Found a breakpoint, test breakpoint condition if any if ( !breakpoint.condition().isEmpty() ) { if ( !breakpoint.testCondition(this, conditionError) ) { // Breakpoint reached but it's condition was not satisfied return false; } } // The found breakpoint is enabled DEBUGGER_EVENT( "Breakpoint reached:" << lineNumber << breakpoint.fileName() ); breakpoint.reached(); // Increase hit count, etc. // Condition satisfied or no condition, active breakpoint found foundBreakpoint = &breakpoint; return true; } else { DEBUGGER_EVENT( "Breakpoint at" << lineNumber << "reached but it is disabled" << breakpoint.fileName() ); return false; } } void DebuggerAgent::setState( DebuggerState newState ) { QMutexLocker locker( m_mutex ); if ( newState == m_state ) { return; } const DebuggerState oldState = m_state; DEBUGGER_STATE_CHANGE( oldState, newState ); m_state = newState; emit stateChanged( newState, oldState ); } void DebuggerAgent::doInterrupt( bool injectedProgram ) { - m_mutex->lockInline(); + m_mutex->lock(); DEBUGGER_EVENT("Interrupt"); if ( m_injectedScriptState == InjectedScriptAborting ) { DEBUGGER_EVENT("Abort evaluation of injected script"); m_injectedScriptState = InjectedScriptNotRunning; m_injectedScriptId = -1; - m_mutex->unlockInline(); + m_mutex->unlock(); // Emit signal to inform that the evaluation was aborted emit evaluationInContextAborted( i18nc("@info", "Evaluation did not finish in time. " "Maybe there is an infinite loop?") ); } else { m_checkRunningTimer->start( CHECK_RUNNING_WHILE_INTERRUPTED_INTERVAL ); setState( Interrupted ); const QDateTime timestamp = QDateTime::currentDateTime(); const int lineNumber = m_lineNumber; const QString fileName = m_scriptIdToFileName[ m_currentScriptId ]; - m_mutex->unlockInline(); + m_mutex->unlock(); // Emit changes in the backtrace/variables emitChanges(); if ( !injectedProgram ) { emit interrupted( lineNumber, fileName, timestamp ); } } forever { // Wait here until the debugger gets continued - m_interruptMutex->lockInline(); + m_interruptMutex->lock(); m_interruptWaiter->wait( m_interruptMutex/*, 500*/ ); - m_interruptMutex->unlockInline(); + m_interruptMutex->unlock(); // Continued, update the execution control value, which might have changed - m_mutex->lockInline(); + m_mutex->lock(); ExecutionControl executionControl = m_executionControl; DEBUGGER_EVENT("Woke up from interrupt, to do now:" << executionControl); - m_mutex->unlockInline(); + m_mutex->unlock(); bool continueExecution = false; switch ( executionControl ) { case ExecuteAbort: { // Continued to be aborted const bool locked = m_engineSemaphore->tryAcquire( 1, 250 ); engine()->abortEvaluation(); if ( !locked ) { qWarning() << "Could not lock the engine"; } // Shut the debugger down shutdown(); return; } case ExecuteAbortInjectedProgram: // Restore member variables - m_mutex->lockInline(); + m_mutex->lock(); DEBUGGER_EVENT("Abort injected program"); m_injectedScriptState = InjectedScriptAborting; // Handled in doInterrupt m_executionControl = m_previousExecutionControl; - m_mutex->unlockInline(); + m_mutex->unlock(); // Interrupt execution of injected script code, // it should be aborted by terminating the executing thread doInterrupt( true ); return; // Check if execution should be interrupted again, ie. if it was just woken to do something case ExecuteInterrupt: { - m_mutex->lockInline(); + m_mutex->lock(); DEBUGGER_EVENT("Still interrupted"); // A hint was set in functionExit() to trigger a call to emitChanges() after script // code was evaluated in the script context (in another thread) and might have changed // variables. In scriptUnload() a 0-timer gets started to call wakeFromInterrupt() so // that the variables in the script context and in the script execution thread are // available here. TODO if ( m_injectedScriptState == InjectedScriptUpdateVariablesInParentContext ) { m_injectedScriptId = -1; } setState( Interrupted ); - m_mutex->unlockInline(); + m_mutex->unlock(); // Update variables/backtrace emitChanges(); // Let directly connected slots be executed in this agent's thread, // while execution is interrupted emit doSomething(); break; } default: // Continue script execution continueExecution = true; break; } if ( continueExecution ) { break; } } - m_mutex->lockInline(); + m_mutex->lock(); ExecutionControl executionControl = m_executionControl; m_checkRunningTimer->start( CHECK_RUNNING_INTERVAL ); const QDateTime timestamp = QDateTime::currentDateTime(); - m_mutex->unlockInline(); + m_mutex->unlock(); setState( Running ); if ( executionControl != ExecuteRunInjectedProgram ) { emit continued( timestamp, executionControl != ExecuteContinue && executionControl != ExecuteRun ); } } int DebuggerAgent::currentFunctionLineNumber() const { QMutexLocker locker( m_mutex ); if ( !m_currentContext ) { return -1; } QScriptContext *context = m_currentContext; while ( context ) { if ( context->thisObject().isFunction() ) { return QScriptContextInfo( context ).lineNumber(); } context = context->parentContext(); } return -1; } // TODO Remove? void DebuggerAgent::checkExecution() { checkHasExited(); } bool DebuggerAgent::checkHasExited() { QMutexLocker locker( m_mutex ); if ( m_state == NotRunning ) { return true; } else if ( isInterrupted() ) { // If script execution is interrupted it is not finished return false; } bool isEvaluating; if ( m_engineSemaphore->tryAcquire(1, 500) ) { isEvaluating = engine()->isEvaluating(); m_engineSemaphore->release(); } else { qWarning() << "Cannot lock the engine"; if ( m_state == Aborting ) { engine()->abortEvaluation(); } shutdown(); return false; } if ( m_state != NotRunning && !isEvaluating ) { shutdown(); return true; } else { return false; } } void DebuggerAgent::fireup() { QMutexLocker locker( m_mutex ); DEBUGGER_EVENT("Execution started"); // First store start time const QDateTime timestamp = QDateTime::currentDateTime(); setState( Running ); m_lastRunAborted = false; m_hasUncaughtException = false; m_uncaughtExceptionLineNumber = -1; m_checkRunningTimer->start( CHECK_RUNNING_INTERVAL ); emit started( timestamp ); } void DebuggerAgent::shutdown() { QMutexLocker locker( m_mutex ); m_checkRunningTimer->stop(); if ( m_state == NotRunning ) { kDebug() << "Not running"; return; } DEBUGGER_EVENT("Execution stopped"); // First store end time const QDateTime timestamp = QDateTime::currentDateTime(); m_functionDepth = 0; const bool isPositionChanged = m_lineNumber != -1 || m_columnNumber != -1; // Context will be invalid m_currentContext = 0; const DebuggerState oldState = m_state; // Engine mutex is still locked here if ( oldState == Aborting ) { if ( engine()->isEvaluating() ) { kDebug() << "Still evaluating, abort"; engine()->abortEvaluation(); } DEBUGGER_EVENT("Was aborted"); m_engineSemaphore->release(); emit aborted(); m_engineSemaphore->acquire(); } engine()->clearExceptions(); setState( NotRunning ); ScriptApi::Network *scriptNetwork = qobject_cast< ScriptApi::Network* >( engine()->globalObject().property("network").toQObject() ); Q_ASSERT( scriptNetwork ); const bool hasRunningRequests = scriptNetwork->hasRunningRequests(); m_engineSemaphore->release(); if ( isPositionChanged ) { const int oldLineNumber = m_lineNumber; const int oldColumnNumber = m_columnNumber; m_lineNumber = -1; m_columnNumber = -1; emit positionChanged( -1, -1, oldLineNumber, oldColumnNumber ); } const int uncaughtExceptionLineNumber = m_uncaughtExceptionLineNumber; const QString uncaughtException = m_uncaughtException.toString(); const QStringList backtrace = m_uncaughtExceptionBacktrace; emit stopped( timestamp, oldState == Aborting, hasRunningRequests, uncaughtExceptionLineNumber, uncaughtException, backtrace ); // Restore locked state of the engine mutex after execution ends // (needs to be locked before execution starts with eg. QScriptEngine::evaluate()) m_engineSemaphore->acquire(); } void DebuggerAgent::exceptionCatch( qint64 scriptId, const QScriptValue &exception ) { kDebug() << scriptId << exception.toString(); } void DebuggerAgent::exceptionThrow( qint64 scriptId, const QScriptValue &exceptionValue, bool hasHandler ) { Q_UNUSED( scriptId ); if ( !hasHandler ) { - m_mutex->lockInline(); + m_mutex->lock(); if ( m_injectedScriptState == InjectedScriptEvaluating ) { // Exception was thrown from injected code - m_mutex->unlockInline(); + m_mutex->unlock(); } else { int uncaughtExceptionLineNumber = m_lineNumber; m_hasUncaughtException = true; m_uncaughtExceptionLineNumber = uncaughtExceptionLineNumber; m_uncaughtException = exceptionValue; m_uncaughtExceptionBacktrace = engine()->uncaughtExceptionBacktrace(); const DebugFlags debugFlags = m_debugFlags; const QString fileName = m_scriptIdToFileName[scriptId]; DEBUGGER_EVENT("Uncatched exception in" << QFileInfo(fileName).fileName() << "line" << uncaughtExceptionLineNumber << exceptionValue.toString()); - m_mutex->unlockInline(); + m_mutex->unlock(); emit exception( uncaughtExceptionLineNumber, exceptionValue.toString(), fileName ); if ( debugFlags.testFlag(InterruptOnExceptions) ) { // Interrupt at the exception doInterrupt(); } abortDebugger(); } } } QVariant DebuggerAgent::extension( QScriptEngineAgent::Extension extension, const QVariant &argument ) { return QScriptEngineAgent::extension( extension, argument ); } QString DebuggerAgent::currentSourceFile() const { QMutexLocker locker( m_mutex ); return m_currentScriptId == -1 ? QString() : m_scriptIdToFileName[m_currentScriptId]; } int DebuggerAgent::lineNumber() const { QMutexLocker locker( m_mutex ); return m_lineNumber; } int DebuggerAgent::columnNumber() const { QMutexLocker locker( m_mutex ); return m_columnNumber; } bool DebuggerAgent::hasUncaughtException() const { QMutexLocker locker( m_mutex ); return m_hasUncaughtException; } int DebuggerAgent::uncaughtExceptionLineNumber() const { QMutexLocker locker( m_mutex ); return m_uncaughtExceptionLineNumber; } QScriptValue DebuggerAgent::uncaughtException() const { QMutexLocker locker( m_mutex ); return m_uncaughtException; } ExecutionControl DebuggerAgent::currentExecutionControlType() const { QMutexLocker locker( m_mutex ); return m_executionControl; } DebugFlags DebuggerAgent::debugFlags() const { QMutexLocker locker( m_mutex ); return m_debugFlags; } void DebuggerAgent::setDebugFlags( DebugFlags debugFlags ) { QMutexLocker locker( m_mutex ); m_debugFlags = debugFlags; } void DebuggerAgent::slotOutput( const QString &outputString, const QScriptContextInfo &contextInfo ) { emit output( outputString, contextInfo ); } } // namespace Debugger diff --git a/engine/timetablemate/src/debugger/debuggerjobs.cpp b/engine/timetablemate/src/debugger/debuggerjobs.cpp index 3be25b5..00da1ab 100644 --- a/engine/timetablemate/src/debugger/debuggerjobs.cpp +++ b/engine/timetablemate/src/debugger/debuggerjobs.cpp @@ -1,1296 +1,1296 @@ /* * Copyright 2012 Friedrich Pülz * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "debuggerjobs.h" // Own includes #include "debugger.h" #include "debuggeragent.h" #include "breakpointmodel.h" #include "variablemodel.h" #include "backtracemodel.h" // PublicTransport engine includes #include // For ServiceProviderScript::SCRIPT_FUNCTION_... // KDE includes #include #include #include #include // Qt includes #include #include #include #include #include #include namespace Debugger { DebuggerJob::DebuggerJob( const ScriptData &scriptData, const QString &useCase, QObject *parent ) : ThreadWeaver::Job(parent), m_agent(0), m_data(scriptData), m_debugger(0), m_success(true), m_aborted(false), m_mutex(new QMutex(QMutex::Recursive)), m_waitLoop(0), m_engineSemaphore(new QSemaphore(1)), m_useCase(useCase), m_quit(false), m_scriptObjectTargetThread(0), m_currentLoop(0), m_stoppedSignalWasProcessed(true) { Q_ASSERT_X( scriptData.isValid(), "DebuggerJob constructor", "ScriptData contains invalid data, no provider data and/or script code loaded" ); QMutexLocker locker( m_mutex ); if ( m_useCase.isEmpty() ) { QTimer::singleShot( 0, this, SLOT(setDefaultUseCase()) ); } } DebuggerJob::DebuggerJob( DebuggerAgent *debugger, QSemaphore *engineSemaphore, const ScriptData &scriptData, const ScriptObjects &objects, const QString &useCase, QObject *parent ) : ThreadWeaver::Job(parent), m_agent(debugger), m_data(scriptData), m_objects(objects), m_success(true), m_aborted(false), m_mutex(new QMutex(QMutex::Recursive)), m_waitLoop(0), m_engineSemaphore(engineSemaphore), m_useCase(useCase), m_quit(false), m_scriptObjectTargetThread(0), m_stoppedSignalWasProcessed(true) { Q_ASSERT_X( scriptData.isValid(), "DebuggerJob constructor", "ScriptData contains invalid data, no provider data and/or script code loaded" ); Q_ASSERT_X( objects.isValid(), "DebuggerJob constructor", "ScriptObjects contains invalid objects, use ScriptObjects::createObjects()" ); QMutexLocker locker( m_mutex ); if ( m_useCase.isEmpty() ) { QTimer::singleShot( 0, this, SLOT(setDefaultUseCase()) ); } } DebuggerJob::~DebuggerJob() { if ( !isFinished() ) { requestAbort(); } - m_mutex->lockInline(); + m_mutex->lock()(); if ( m_agent ) { DEBUG_JOBS("Wait for the debugger to finish"); m_agent->finish(); } - m_mutex->unlockInline(); + m_mutex->unlock()(); delete m_mutex; delete m_engineSemaphore; } void DebuggerJob::connectScriptObjects( bool doConnect ) { QMutexLocker locker( m_mutex ); if ( doConnect ) { connect( m_objects.helper.data(), SIGNAL(messageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)), this, SIGNAL(scriptMessageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)) ); } else { disconnect( m_objects.helper.data(), SIGNAL(messageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)), this, SIGNAL(scriptMessageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)) ); } } void DebuggerJob::disconnectScriptObjects() { QMutexLocker locker( m_mutex ); // Disconnect all signals of script objects from this job // to prevent crashes after execution was aborted if ( !m_objects.helper.isNull() ) { disconnect( m_objects.helper.data(), 0, this, 0 ); } if ( !m_objects.network.isNull() ) { disconnect( m_objects.network.data(), 0, this, 0 ); } if ( !m_objects.result.isNull() ) { disconnect( m_objects.result.data(), 0, this, 0 ); } m_objects.clear(); m_agent = 0; } LoadScriptJob::~LoadScriptJob() { } EvaluateInContextJob::~EvaluateInContextJob() { } ExecuteConsoleCommandJob::~ExecuteConsoleCommandJob() { } void DebuggerJob::setDefaultUseCase() { QMutexLocker locker( m_mutex ); if ( m_useCase.isEmpty() ) { // Do not change use case, if it was already set using setUseCase() m_useCase = defaultUseCase(); } } bool DebuggerJob::success() const { QMutexLocker locker( m_mutex ); return m_success; } ScriptObjects DebuggerJob::objects() const { QMutexLocker locker( m_mutex ); return m_objects; } void DebuggerJob::requestAbort() { QMutexLocker locker( m_mutex ); if ( m_quit || !m_agent ) { DEBUG_JOBS("Is already aborting/finished"); return; } DEBUG_JOBS("Request abort"); m_quit = true; if ( m_currentLoop ) { QEventLoop *loop = m_currentLoop; m_currentLoop = 0; loop->quit(); } if ( !isFinished() && m_objects.network->hasRunningRequests() ) { m_objects.network->abortAllRequests(); } if ( m_agent ) { m_agent->abortDebugger(); } } QString DebuggerJob::typeToString( JobType type ) { switch ( type ) { case LoadScript: return "LoadScriptJob"; case EvaluateInContext: return "EvaluateInContextJob"; case ExecuteConsoleCommand: return "ExecuteConsoleCommandJob"; case CallScriptFunction: return "CallScriptFunctionJob"; case TestFeatures: return "TestFeaturesJob"; case TimetableDataRequest: return "TimetableDataRequestJob"; default: return QString("Unknown job type %1").arg(type); } } QString EvaluateInContextJob::toString() const { QMutexLocker locker( m_mutex ); QString programString = m_program; if ( programString.length() > 100 ) { programString = programString.left(100) + "..."; } return QString("%1 (%2)").arg( DebuggerJob::toString() ).arg( programString ); } QString ExecuteConsoleCommandJob::toString() const { QMutexLocker locker( m_mutex ); return QString("%1 (%2)").arg( DebuggerJob::toString() ).arg( m_command.toString() ); } void DebuggerJob::run() { #ifdef DEBUG_JOB_START_END kDebug() << "\nDebuggerJob::run(): Start" << toString(); #endif debuggerRun(); disconnectScriptObjects(); #ifdef DEBUG_JOB_START_END debugJobEnd(); #endif } #ifdef DEBUG_JOB_START_END void DebuggerJob::debugJobEnd() const { QMutexLocker locker( m_mutex ); kDebug() << "DebuggerJob::run():" << (m_success ? "Success" : "Fail") << toString(); } #endif void DebuggerJob::attachAgent( DebuggerAgent *agent ) { // Initialize and connect the new agent. // The agent lives in the thread of this job, while the job itself lives in the GUI thread. // Therefore the connections below are queued connections. m_mutex->lock(); m_agent = agent; agent->setMainScriptFileName( m_data.provider.scriptFileName() ); agent->setDebugFlags( NeverInterrupt ); Debugger *debugger = m_debugger; m_mutex->unlock(); connectAgent( agent ); // Add existing breakpoints if ( debugger ) { BreakpointModel *breakpointModel = debugger->breakpointModel(); for ( int i = 0; i < breakpointModel->rowCount(); ++i ) { agent->addBreakpoint( *breakpointModel->breakpointFromRow(i) ); } } } void DebuggerJob::connectAgent( DebuggerAgent *agent, bool doConnect, bool connectModels ) { QMutexLocker locker( m_mutex ); if ( doConnect ) { connect( agent, SIGNAL(started(QDateTime)), this, SIGNAL(started(QDateTime)) ); connect( agent, SIGNAL(stopped(QDateTime,bool,bool,int,QString,QStringList)), this, SLOT(slotStopped(QDateTime,bool,bool,int,QString,QStringList)) ); connect( agent, SIGNAL(aborted()), this, SIGNAL(aborted()) ); connect( agent, SIGNAL(positionChanged(int,int,int,int)), this, SIGNAL(positionChanged(int,int,int,int)) ); connect( agent, SIGNAL(stateChanged(DebuggerState,DebuggerState)), this, SIGNAL(stateChanged(DebuggerState,DebuggerState)) ); connect( agent, SIGNAL(breakpointReached(Breakpoint)), this, SIGNAL(breakpointReached(Breakpoint)) ); connect( agent, SIGNAL(exception(int,QString,QString)), this, SIGNAL(exception(int,QString,QString)) ); connect( agent, SIGNAL(interrupted(int,QString,QDateTime)), this, SIGNAL(interrupted(int,QString,QDateTime)) ); connect( agent, SIGNAL(continued(QDateTime,bool)), this, SIGNAL(continued(QDateTime,bool)) ); connect( agent, SIGNAL(output(QString,QScriptContextInfo)), this, SIGNAL(output(QString,QScriptContextInfo)) ); connect( agent, SIGNAL(informationMessage(QString)), this, SIGNAL(informationMessage(QString)) ); connect( agent, SIGNAL(errorMessage(QString)), this, SIGNAL(errorMessage(QString)) ); connect( agent, SIGNAL(evaluationInContextFinished(QScriptValue)), this, SIGNAL(evaluationInContextFinished(QScriptValue)) ); connect( agent, SIGNAL(evaluationInContextAborted(QString)), this, SIGNAL(evaluationInContextAborted(QString)) ); if ( connectModels ) { connect( agent, SIGNAL(variablesChanged(VariableChange)), this, SIGNAL(variablesChanged(VariableChange)) ); connect( agent, SIGNAL(backtraceChanged(BacktraceChange)), this, SIGNAL(backtraceChanged(BacktraceChange)) ); connect( agent, SIGNAL(breakpointReached(Breakpoint)), this, SIGNAL(breakpointReached(Breakpoint)) ); } } else { disconnect( agent, SIGNAL(started(QDateTime)), this, SIGNAL(started(QDateTime)) ); disconnect( agent, SIGNAL(stopped(QDateTime,bool,bool,int,QString,QStringList)), this, SLOT(slotStopped(QDateTime,bool,bool,int,QString,QStringList)) ); disconnect( agent, SIGNAL(aborted()), this, SIGNAL(aborted()) ); disconnect( agent, SIGNAL(positionChanged(int,int,int,int)), this, SIGNAL(positionChanged(int,int,int,int)) ); disconnect( agent, SIGNAL(stateChanged(DebuggerState,DebuggerState)), this, SIGNAL(stateChanged(DebuggerState,DebuggerState)) ); disconnect( agent, SIGNAL(breakpointReached(Breakpoint)), this, SIGNAL(breakpointReached(Breakpoint)) ); disconnect( agent, SIGNAL(exception(int,QString,QString)), this, SIGNAL(exception(int,QString,QString)) ); disconnect( agent, SIGNAL(interrupted(int,QString,QDateTime)), this, SIGNAL(interrupted(int,QString,QDateTime)) ); disconnect( agent, SIGNAL(continued(QDateTime,bool)), this, SIGNAL(continued(QDateTime,bool)) ); disconnect( agent, SIGNAL(output(QString,QScriptContextInfo)), this, SIGNAL(output(QString,QScriptContextInfo)) ); disconnect( agent, SIGNAL(informationMessage(QString)), this, SIGNAL(informationMessage(QString)) ); disconnect( agent, SIGNAL(errorMessage(QString)), this, SIGNAL(errorMessage(QString)) ); disconnect( agent, SIGNAL(evaluationInContextFinished(QScriptValue)), this, SIGNAL(evaluationInContextFinished(QScriptValue)) ); disconnect( agent, SIGNAL(evaluationInContextAborted(QString)), this, SIGNAL(evaluationInContextAborted(QString)) ); if ( connectModels ) { disconnect( agent, SIGNAL(variablesChanged(VariableChange)), this, SIGNAL(variablesChanged(VariableChange)) ); disconnect( agent, SIGNAL(backtraceChanged(BacktraceChange)), this, SIGNAL(backtraceChanged(BacktraceChange)) ); disconnect( agent, SIGNAL(breakpointReached(Breakpoint)), this, SIGNAL(breakpointReached(Breakpoint)) ); } } } void DebuggerJob::slotStopped( const QDateTime ×tamp, bool aborted, bool hasRunningRequests, int uncaughtExceptionLineNumber, const QString &uncaughtException, const QStringList &backtrace ) { ScriptStoppedFlags flags = aborted ? ScriptWasAborted : ScriptStopped; if ( hasRunningRequests ) { flags |= ScriptHasRunningRequests; } emit stopped( timestamp, flags, uncaughtExceptionLineNumber, uncaughtException, backtrace ); } DebuggerAgent *DebuggerJob::createAgent() { // m_engineSemaphore is expected to be locked here // Create engine and agent and connect them m_mutex->lock(); QScriptEngine *engine = new QScriptEngine(); DebuggerAgent *agent = new DebuggerAgent( engine, m_engineSemaphore, true ); m_mutex->unlock(); engine->setAgent( agent ); attachAgent( agent ); // Initialize the script QMutexLocker locker( m_mutex ); const ScriptData data = m_data; m_objects.createObjects( data ); if ( !m_objects.attachToEngine(engine, data) ) { kDebug() << "Cannot attach script objects to engine" << m_objects.lastError; engine->setAgent( 0 ); delete agent; delete engine; m_explanation = m_objects.lastError; m_success = false; return 0; } Q_ASSERT( m_objects.isValid() ); // Make this job responsive while running the script engine->setProcessEventsInterval( 50 ); return agent; } void DebuggerJob::destroyAgent() { // m_engineSemaphore is expected to be locked here m_mutex->lock(); DebuggerAgent *agent = m_agent; QScriptEngine *engine = agent->engine(); m_mutex->unlock(); m_mutex->lock(); if ( !m_stoppedSignalWasProcessed ) { qWarning() << "No stopped() signal was processed, aborted?"; setJobDone( true ); m_agent->engine()->abortEvaluation(); } DEBUG_JOBS( "Destroying agent" << m_data.provider.id() << agent << engine ); if ( engine->isEvaluating() ) { qWarning() << "Still evaluating..." << engine; } m_agent = 0; engine->setAgent( 0 ); m_objects.clear(); delete agent; engine->deleteLater(); m_mutex->unlock(); } void DebuggerJob::setJobDone( bool done ) { // The stopped() signal was processed in a connected Debugger instance QMutexLocker locker( m_mutex ); m_stoppedSignalWasProcessed = done; if ( done && m_waitLoop ) { m_waitLoop->exit(); } } bool DebuggerJob::waitFor( QObject *sender, const char *signal, WaitForType type ) { // The called function returned, but asynchronous network requests may have been started. // Wait for all network requests to finish, because slots in the script may get called const int finishWaitTime = 1000; int finishWaitCounter = 0; - m_mutex->lockInline(); + m_mutex->lock()(); bool success = m_success; bool quit = m_quit; if ( !success || quit ) { - m_mutex->unlockInline(); + m_mutex->unlock()(); return true; } DebuggerAgent *agent = m_agent; ScriptObjects objects = m_objects; bool stoppedSignalWasProcessed = m_stoppedSignalWasProcessed; - m_mutex->unlockInline(); + m_mutex->unlock()(); while ( !agent->wasLastRunAborted() && finishWaitCounter < 50 && sender ) { if ( (type == WaitForNetwork && !objects.network->hasRunningRequests()) || (type == WaitForScriptFinish && !agent->engine()->isEvaluating() && stoppedSignalWasProcessed) || (type == WaitForInjectedScriptFinish && !agent->isInjectedScriptEvaluating()) || (type == WaitForInterrupt && agent->isInterrupted()) || (type == WaitForNothing && finishWaitCounter > 0) ) { break; } QEventLoop loop; connect( sender, signal, &loop, SLOT(quit()) ); QTimer::singleShot( finishWaitTime, &loop, SLOT(quit()) ); // Store a pointer to the event loop, to be able to quit it from the destructor - m_mutex->lockInline(); + m_mutex->lock()(); m_currentLoop = &loop; - m_mutex->unlockInline(); + m_mutex->unlock()(); // Engine continues execution here / waits for a signal loop.exec(); qApp->processEvents(); - m_mutex->lockInline(); + m_mutex->lock()(); if ( !m_currentLoop || m_quit ) { - m_mutex->unlockInline(); + m_mutex->unlock()(); // Job was aborted destroyAgent(); return false; } m_currentLoop = 0; stoppedSignalWasProcessed = m_stoppedSignalWasProcessed; - m_mutex->unlockInline(); + m_mutex->unlock()(); ++finishWaitCounter; } if ( finishWaitCounter >= 50 ) { DEBUG_JOBS("Timeout"); if ( type == WaitForScriptFinish ) { // Script not finished DEBUG_JOBS("Script not finished, abort"); agent->engine()->abortEvaluation(); } else if ( type == WaitForNetwork ) { objects.network->abortAllRequests(); } } return finishWaitCounter < 50; } void LoadScriptJob::debuggerRun() { - m_mutex->lockInline(); + m_mutex->lock()(); QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax( m_data.program->sourceCode() ); if ( syntax.state() == QScriptSyntaxCheckResult::Error ) { DEBUG_JOBS("Syntax error at" << syntax.errorLineNumber() << syntax.errorColumnNumber() << syntax.errorMessage()); if ( syntax.errorColumnNumber() < 0 ) { m_explanation = i18nc("@info/plain", "Syntax error at line %1: %2", syntax.errorLineNumber(), syntax.errorMessage()); } else { m_explanation = i18nc("@info/plain", "Syntax error at line %1, column %2: %3", syntax.errorLineNumber(), syntax.errorColumnNumber(), syntax.errorMessage()); } m_success = false; - m_mutex->unlockInline(); + m_mutex->unlock()(); return; } // Create new engine and agent - m_mutex->unlockInline(); + m_mutex->unlock()(); m_engineSemaphore->acquire(); DebuggerAgent *agent = createAgent(); - m_mutex->lockInline(); + m_mutex->lock()(); if ( !agent || m_quit ) { - m_mutex->unlockInline(); + m_mutex->unlock()(); if ( agent ) { destroyAgent(); } m_engineSemaphore->release(); return; } const ScriptData data = m_data; const ScriptObjects objects = m_objects; const DebugFlags debugFlags = m_debugFlags; - m_mutex->unlockInline(); + m_mutex->unlock()(); agent->setDebugFlags( debugFlags ); agent->setExecutionControlType( debugFlags.testFlag(InterruptAtStart) ? ExecuteInterrupt : ExecuteRun ); agent->engine()->evaluate( *data.program ); Q_ASSERT_X( !objects.network->hasRunningRequests() || !agent->engine()->isEvaluating(), "LoadScriptJob::debuggerRun()", "Evaluating the script should not start any asynchronous requests, bad script" ); // Wait for the stopped signal const bool aborted = agent->wasLastRunAborted(); if ( aborted ) { kDebug() << "Aborted" << m_data.provider.id(); m_stoppedSignalWasProcessed = true; // Do not crash on assert in destroyAgent() destroyAgent(); QMutexLocker locker( m_mutex ); m_explanation = i18nc("@info/plain", "Was aborted"); m_success = false; m_aborted = true; m_engineSemaphore->release(); return; } m_mutex->lock(); if ( !m_stoppedSignalWasProcessed ) { // Wait for the stopped() signal to be processed by the connected Debugger instance m_waitLoop = new QEventLoop(); m_mutex->unlock(); m_waitLoop->exec(); // The stopped() signal was processed m_mutex->lock(); delete m_waitLoop; m_waitLoop = 0; Q_ASSERT( m_stoppedSignalWasProcessed ); m_mutex->unlock(); } else { m_mutex->unlock(); } // Check that the getTimetable() function is implemented in the script const QString functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE; const QScriptValue function = agent->engine()->globalObject().property( functionName ); if ( !function.isFunction() ) { QMutexLocker locker( m_mutex ); DEBUG_JOBS("Did not find" << functionName << "function in the script!"); m_explanation = i18nc("@info/plain", "Did not find required '%1' function in the script.", functionName); m_success = false; } const QStringList functions = QStringList() << ServiceProviderScript::SCRIPT_FUNCTION_FEATURES << ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE << ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS << ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS << ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA; QStringList globalFunctions; foreach ( const QString &function, functions ) { const QScriptValue value = agent->engine()->globalObject().property( function ); if ( value.isFunction() ) { globalFunctions << function; } } const QStringList includedFiles = agent->engine()->globalObject().property( "includedFiles" ).toVariant().toStringList(); m_engineSemaphore->release(); m_mutex->lock(); if ( !m_success || m_quit ) { m_success = false; } else if ( agent->hasUncaughtException() ) { m_engineSemaphore->acquire(); const QString uncaughtException = agent->uncaughtException().toString(); DEBUG_JOBS("Error in the script" << agent->uncaughtExceptionLineNumber() << uncaughtException); m_engineSemaphore->release(); m_includedFiles = includedFiles; m_globalFunctions = globalFunctions; m_explanation = i18nc("@info/plain", "Error in the script: " "%1.", uncaughtException); m_success = false; } else if ( agent->wasLastRunAborted() ) { // Script was aborted m_explanation = i18nc("@info/plain", "Aborted"); m_success = false; } else { m_includedFiles = includedFiles; m_globalFunctions = globalFunctions; m_success = true; } m_mutex->unlock(); m_engineSemaphore->acquire(); destroyAgent(); m_engineSemaphore->release(); } bool ExecuteConsoleCommandJob::runInForeignAgent( DebuggerAgent *agent ) { // Execute the command - m_mutex->lockInline(); + m_mutex->lock()(); const ConsoleCommand command = m_command; - m_mutex->unlockInline(); + m_mutex->unlock()(); QString returnValue; bool success = agent->executeCommand( command, &returnValue ); QMutexLocker locker( m_mutex ); m_success = success; m_returnValue = returnValue; return true; } bool ForeignAgentJob::waitForFinish( DebuggerAgent *agent, const ScriptObjects &objects ) { // Wait for the evaluation to finish, // does not include waiting for running asynchronous network requests to finish if ( !waitFor(agent, SIGNAL(evaluationInContextFinished(QScriptValue)), WaitForInjectedScriptFinish) ) { return false; } // The called function returned, but asynchronous network requests may have been started. // Wait for all network requests to finish, because slots in the script may get called if ( !waitFor(objects.network.data(), SIGNAL(allRequestsFinished()), WaitForNetwork) ) { return false; } // Wait for the evaluation to finish after having received network data, // ie. wait until execution gets interrupted again in the parent thread, if any if ( m_parentJob && !waitFor(agent, SIGNAL(interrupted(int,QString,QDateTime)), WaitForInterrupt) ) { return false; } return true; } void ForeignAgentJob::debuggerRun() { - m_mutex->lockInline(); + m_mutex->lock()(); const ScriptData data = m_data; DebuggerAgent *agent = m_agent; QScriptEngine *engine = 0; ScriptObjects objects = m_objects; m_success = true; const QPointer< DebuggerJob > parentJob = m_parentJob; - m_mutex->unlockInline(); + m_mutex->unlock()(); const bool useExistingEngine = agent; QThread *scriptObjectThread = 0; DebugFlags previousDebugFlags = NeverInterrupt; ExecutionControl previousExecutionControl = ExecuteInterrupt; if ( useExistingEngine ) { engine = agent->engine(); // Let the running job move script objects to this thread QEventLoop loop; connect( parentJob, SIGNAL(movedScriptObjectsToThread(QThread*)), &loop, SLOT(quit()) ); scriptObjectThread = objects.currentThread(); parentJob->moveScriptObjectsToThread( QThread::currentThread() ); loop.exec(); // Do not interrupt in injected script code, // store previous debug flags set by the parent job previousDebugFlags = agent->debugFlags(); previousExecutionControl = agent->currentExecutionControlType(); agent->setDebugFlags( NeverInterrupt ); agent->setExecutionControlType( ExecuteRun ); connectAgent( agent ); } else { // Create new engine and agent m_engineSemaphore->acquire(); - m_mutex->lockInline(); + m_mutex->lock()(); agent = createAgent(); objects = m_objects; - m_mutex->unlockInline(); + m_mutex->unlock()(); if ( !agent ) { m_engineSemaphore->release(); return; } engine = agent->engine(); // Load script engine->evaluate( *data.program ); Q_ASSERT_X( !engine->isEvaluating(), "CallScriptFunctionJob::debuggerRun()", "Evaluating the script should not start any asynchronous requests, bad script" ); m_engineSemaphore->release(); } m_engineSemaphore->acquire(); connect( agent, SIGNAL(evaluationInContextAborted(QString)), this, SLOT(evaluationAborted(QString)) ); // Start the job in the foreign agent, implemented by derived classes // and wait until it has finished (this includes waiting for running network requests) if ( !runInForeignAgent(agent) || !waitForFinish(agent, objects) ) { destroyAgent(); m_engineSemaphore->release(); return; } disconnect( agent, SIGNAL(evaluationInContextAborted(QString)), this, SLOT(evaluationAborted(QString)) ); // Handle uncaught exceptions if ( agent->hasUncaughtException() ) { handleUncaughtException( agent ); } m_engineSemaphore->release(); // Cleanup if ( useExistingEngine ) { QMutexLocker locker( m_mutex ); // Move script objects back to the parent thread DEBUG_JOBS("Move script objects back to the parent thread"); m_engineSemaphore->acquire(); objects.moveToThread( scriptObjectThread ); connectAgent( agent, false ); m_engineSemaphore->release(); // Restore previous debug flags, interrupts were disabled for this job agent->setDebugFlags( previousDebugFlags ); agent->setExecutionControlType( previousExecutionControl ); m_agent = 0; m_objects.clear(); m_engineSemaphore = 0; // Do not delete, used by the other thread parentJob->gotScriptObjectsBack(); } else { m_engineSemaphore->acquire(); destroyAgent(); m_engineSemaphore->release(); } } void EvaluateInContextJob::handleUncaughtException( DebuggerAgent *agent ) { QMutexLocker locker( m_mutex ); const QString program = m_program; const QString context = m_context; EvaluationResult result = m_result; handleError( agent->engine(), i18nc("@info/plain", "Error in the script when evaluating '%1' " "with code %2: %3", context, program, agent->uncaughtException().toString()), &result ); m_result = result; } bool EvaluateInContextJob::runInForeignAgent( DebuggerAgent *agent ) { - m_mutex->lockInline(); + m_mutex->lock()(); const QString program = m_program; const QString context = m_context; m_result.error = false; - m_mutex->unlockInline(); + m_mutex->unlock()(); // m_engineSemaphore is locked // Check syntax of the program to run DEBUG_JOBS("Evaluate in context" << context << program); QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax( program ); if ( syntax.state() == QScriptSyntaxCheckResult::Error ) { DEBUG_JOBS("Error in script code:" << syntax.errorLineNumber() << syntax.errorMessage()); QMutexLocker locker( m_mutex ); m_explanation = syntax.errorMessage().isEmpty() ? i18nc("@info/plain", "Syntax error") : i18nc("@info/plain", "Syntax error: %1.", syntax.errorMessage()); m_success = false; m_result.error = true; m_result.errorLineNumber = syntax.errorLineNumber(); m_result.errorMessage = m_explanation; return false; } // Evaluate script code EvaluationResult result; result.returnValue = agent->evaluateInContext( /*'{' + */program/* + '}'*/, context, &(result.error), &(result.errorLineNumber), &(result.errorMessage), &(result.backtrace), NeverInterrupt ).toString(); DEBUG_JOBS("Evaluate in context result:" << result.returnValue); QMutexLocker locker( m_mutex ); m_result = result; return true; } void DebuggerJob::evaluationAborted( const QString &errorMessage ) { QMutexLocker locker( m_mutex ); m_explanation = errorMessage; m_success = false; _evaluationAborted( errorMessage ); DebuggerAgent *agent = m_agent; agent->abortDebugger(); } void EvaluateInContextJob::_evaluationAborted( const QString &errorMessage ) { QMutexLocker locker( m_mutex ); m_result.error = true; m_result.errorMessage = errorMessage; } void DebuggerJob::handleError( QScriptEngine *engine, const QString &message, EvaluationResult *result ) { // m_engineSemaphore is locked handleError( engine->uncaughtExceptionLineNumber(), engine->uncaughtException().toString(), engine->uncaughtExceptionBacktrace(), message, result ); } void DebuggerJob::handleError( int uncaughtExceptionLineNumber, const QString &uncaughtException, const QStringList &backtrace, const QString &message, EvaluationResult *result ) { if ( result ) { result->error = true; result->errorMessage = uncaughtException; result->errorLineNumber = uncaughtExceptionLineNumber; result->backtrace = backtrace; } DEBUG_JOBS((!message.isEmpty() ? message : "Script error") << uncaughtExceptionLineNumber << uncaughtException << backtrace); QMutexLocker locker( m_mutex ); m_explanation = !message.isEmpty() ? message : i18nc("@info/plain", "Error in the script: %1.", uncaughtException); m_success = false; } void DebuggerJob::attachDebugger( Debugger *debugger ) { // Connect models BreakpointModel *breakpointModel = debugger->breakpointModel(); connect( this, SIGNAL(variablesChanged(VariableChange)), debugger->variableModel(), SLOT(applyChange(VariableChange)) ); connect( this, SIGNAL(backtraceChanged(BacktraceChange)), debugger->backtraceModel(), SLOT(applyChange(BacktraceChange)) ); connect( this, SIGNAL(breakpointsChanged(BreakpointChange)), breakpointModel, SLOT(applyChange(BreakpointChange)) ); connect( this, SIGNAL(breakpointReached(Breakpoint)), breakpointModel, SLOT(updateBreakpoint(Breakpoint)) ); connect( breakpointModel, SIGNAL(breakpointModified(Breakpoint)), this, SLOT(updateBreakpoint(Breakpoint)) ); connect( breakpointModel, SIGNAL(breakpointAdded(Breakpoint)), this, SLOT(addBreakpoint(Breakpoint)) ); connect( breakpointModel, SIGNAL(breakpointAboutToBeRemoved(Breakpoint)), this, SLOT(removeBreakpoint(Breakpoint)) ); QMutexLocker locker( m_mutex ); m_debugger = debugger; } const ConsoleCommand ExecuteConsoleCommandJob::command() const { QMutexLocker locker( m_mutex ); return m_command; } QVariant ExecuteConsoleCommandJob::returnValue() const { QMutexLocker locker( m_mutex ); return m_returnValue; } EvaluationResult EvaluateInContextJob::result() const { QMutexLocker locker( m_mutex ); return m_result; } QVariant EvaluateInContextJob::returnValue() const { QMutexLocker locker( m_mutex ); return m_result.returnValue; } QString LoadScriptJob::defaultUseCase() const { return i18nc("@info", "Load script"); } QString EvaluateInContextJob::defaultUseCase() const { QMutexLocker locker( m_mutex ); return i18nc("@info", "Evaluate %1", m_program.left(50)); } QString ExecuteConsoleCommandJob::defaultUseCase() const { QMutexLocker locker( m_mutex ); return i18nc("@info", "Execute command %1", m_command.toString()); } QStringList LoadScriptJob::globalFunctions() const { QMutexLocker locker( m_mutex ); return m_globalFunctions; } QStringList LoadScriptJob::includedFiles() const { QMutexLocker locker( m_mutex ); return m_includedFiles; } DebugFlags LoadScriptJob::debugFlags() const { QMutexLocker locker( m_mutex ); return m_debugFlags; } void DebuggerJob::setUseCase( const QString &useCase ) { QMutexLocker locker( m_mutex ); m_useCase = useCase; } QString DebuggerJob::useCase() const { QMutexLocker locker( m_mutex ); return m_useCase; } QString DebuggerJob::toString() const { QMutexLocker locker( m_mutex ); return typeToString( type() ) + ", " + m_data.provider.id(); } DebuggerAgent *DebuggerJob::debuggerAgent() const { QMutexLocker locker( m_mutex ); return m_agent; } QSemaphore *DebuggerJob::engineSemaphore() const { QMutexLocker locker( m_mutex ); return m_engineSemaphore; } bool DebuggerJob::wasAborted() const { QMutexLocker locker( m_mutex ); return m_aborted; } QString DebuggerJob::explanation() const { QMutexLocker locker( m_mutex ); return m_explanation; } const ServiceProviderData DebuggerJob::providerData() const { QMutexLocker locker( m_mutex ); return m_data.provider; } void DebuggerJob::debugStepInto( int repeat ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugStepInto( repeat ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::abortDebugger() { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->abortDebugger(); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::addBreakpoint( const Breakpoint &breakpoint ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->addBreakpoint( breakpoint ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::checkExecution() { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->checkExecution(); } } void DebuggerJob::debugContinue() { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugContinue(); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::debugInterrupt() { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugInterrupt(); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::debugRunUntilLineNumber( const QString &fileName, int lineNumber ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugRunUntilLineNumber( fileName, lineNumber ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::debugStepOut( int repeat ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugStepOut( repeat ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::debugStepOver( int repeat ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->debugStepOver( repeat ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::removeBreakpoint( const Breakpoint &breakpoint ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->removeBreakpoint( breakpoint ); } else { qWarning() << "Debugger already deleted"; } } void DebuggerJob::slotOutput( const QString &outputString, const QScriptContextInfo &contextInfo ) { QMutexLocker locker( m_mutex ); if ( m_agent ) { m_agent->slotOutput( outputString, contextInfo ); } } NextEvaluatableLineHint DebuggerJob::canBreakAt( const QString &fileName, int lineNumber ) const { QMutexLocker locker( m_mutex ); return !m_agent ? CannotFindNextEvaluatableLine : m_agent->canBreakAt(fileName, lineNumber); } int DebuggerJob::getNextBreakableLineNumber( const QString &fileName, int lineNumber ) const { QMutexLocker locker( m_mutex ); return !m_agent ? -1 : m_agent->getNextBreakableLineNumber(fileName, lineNumber); } void DebuggerJob::finish() const { DEBUG_JOBS("Wait for the job to finish..."); - m_mutex->lockInline(); + m_mutex->lock()(); if ( m_agent ) { m_agent->finish(); } if ( !isFinished() ) { QEventLoop loop; connect( this, SIGNAL(done(ThreadWeaver::Job*)), &loop, SLOT(quit()) ); - m_mutex->unlockInline(); + m_mutex->unlock()(); loop.exec(); } else { - m_mutex->unlockInline(); + m_mutex->unlock()(); } DEBUG_JOBS("...job finished"); } bool DebuggerJob::isAborting() const { QMutexLocker locker( m_mutex ); return !m_agent ? false : m_agent->isAborting(); } bool DebuggerJob::isInterrupted() const { QMutexLocker locker( m_mutex ); return !m_agent ? false : m_agent->isInterrupted(); } bool DebuggerJob::isRunning() const { QMutexLocker locker( m_mutex ); return !m_agent ? false : m_agent->isRunning(); } bool DebuggerJob::isEvaluating() const { QMutexLocker locker( m_mutex ); if ( !m_agent ) { return false; } m_engineSemaphore->acquire(); const bool result = !m_agent || !m_agent->engine() ? false : m_agent->engine()->isEvaluating(); m_engineSemaphore->release(); return result; } bool DebuggerJob::canEvaluate( const QString &program ) const { QMutexLocker locker( m_mutex ); m_engineSemaphore->acquire(); const bool result = m_agent->engine()->canEvaluate( program ); m_engineSemaphore->release(); return result; } DebuggerState DebuggerJob::state() const { QMutexLocker locker( m_mutex ); return !m_agent ? NotRunning : m_agent->state(); } QString DebuggerJob::currentSourceFile() const { QMutexLocker locker( m_mutex ); return !m_agent ? QString() : m_agent->currentSourceFile(); } int DebuggerJob::lineNumber() const { QMutexLocker locker( m_mutex ); return !m_agent ? -1 : m_agent->lineNumber(); } int DebuggerJob::columnNumber() const { QMutexLocker locker( m_mutex ); return !m_agent ? -1 : m_agent->columnNumber(); } bool DebuggerJob::hasUncaughtException() const { QMutexLocker locker( m_mutex ); return !m_agent ? false : m_agent->hasUncaughtException(); } int DebuggerJob::uncaughtExceptionLineNumber() const { QMutexLocker locker( m_mutex ); return !m_agent ? -1 : m_agent->uncaughtExceptionLineNumber(); } QScriptValue DebuggerJob::uncaughtException() const { QMutexLocker locker( m_mutex ); return !m_agent ? QScriptValue() : m_agent->uncaughtException(); } ForeignAgentJob::ForeignAgentJob( const ScriptData &scriptData, const QString &useCase, DebuggerJob *parentJob, QObject *parent ) : DebuggerJob(scriptData, useCase, parent), m_parentJob(QPointer(parentJob)) { if ( parentJob ) { QMutexLocker locker( m_mutex ); if ( m_engineSemaphore ) { delete m_engineSemaphore; } m_engineSemaphore = parentJob->engineSemaphore(); m_agent = parentJob->debuggerAgent(); m_objects = parentJob->objects(); } } ExecuteConsoleCommandJob::ExecuteConsoleCommandJob( const ScriptData &data, const ConsoleCommand &command, const QString &useCase, DebuggerJob *parentJob, QObject *parent ) : ForeignAgentJob(data, useCase, parentJob, parent), m_command(command) { } EvaluateInContextJob::EvaluateInContextJob( const ScriptData &data, const QString &program, const QString &context, const QString &useCase, DebuggerJob *parentJob, QObject *parent ) : ForeignAgentJob(data, useCase, parentJob, parent), m_program(program), m_context(QString(context).replace('\n', ' ')) { } void DebuggerJob::moveScriptObjectsToThread( QThread *thread ) { QMutexLocker locker( m_mutex ); m_scriptObjectTargetThread = thread; // The agent lives in the thread that gets executed by this job, // connect directly to do something in the thread of the agent connect( m_agent, SIGNAL(doSomething()), this, SLOT(doSomething()), Qt::DirectConnection ); m_agent->continueToDoSomething(); } void DebuggerJob::doSomething() { QMutexLocker locker( m_mutex ); Q_ASSERT( QThread::currentThread() == m_agent->thread() ); disconnect( m_agent, SIGNAL(doSomething()), this, SLOT(doSomething()) ); QThread *targetThread = m_scriptObjectTargetThread; if ( !targetThread ) { // Nothing to do return; } // Move script objects to thread m_engineSemaphore->acquire(); connectScriptObjects( false ); connectAgent( m_agent, false, false ); Q_ASSERT( m_objects.isValid() ); m_objects.moveToThread( targetThread ); m_scriptObjectTargetThread = 0; m_engineSemaphore->release(); emit movedScriptObjectsToThread( targetThread ); } void DebuggerJob::gotScriptObjectsBack() { QMutexLocker locker( m_mutex ); connectScriptObjects(); connectAgent( m_agent, true, false ); // Continue to update variables m_agent->continueToDoSomething(); } } // namespace Debugger diff --git a/engine/timetablemate/src/debugger/timetabledatarequestjob.cpp b/engine/timetablemate/src/debugger/timetabledatarequestjob.cpp index fcd0fb3..f187e2b 100644 --- a/engine/timetablemate/src/debugger/timetabledatarequestjob.cpp +++ b/engine/timetablemate/src/debugger/timetabledatarequestjob.cpp @@ -1,1126 +1,1125 @@ /* * Copyright 2012 Friedrich Pülz * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Header #include "timetabledatarequestjob.h" // Own includes #include "debuggeragent.h" // Public Transport engine includes #include #include #include #include // KDE includes #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include namespace Debugger { CallScriptFunctionJob::CallScriptFunctionJob( const ScriptData &scriptData, const QString &functionName, const QVariantList &arguments, const QString &useCase, DebugFlags debugFlags, QObject *parent ) : DebuggerJob(scriptData, useCase, parent), m_debugFlags(debugFlags), m_functionName(functionName), m_arguments(arguments) { } CallScriptFunctionJob::CallScriptFunctionJob( const ScriptData &scriptData, const QString &useCase, DebugFlags debugFlags, QObject *parent ) : DebuggerJob(scriptData, useCase, parent), m_debugFlags(debugFlags) { } TestFeaturesJob::TestFeaturesJob( const ScriptData &scriptData, const QString &useCase, DebugFlags debugFlags, QObject *parent ) : CallScriptFunctionJob(scriptData, ServiceProviderScript::SCRIPT_FUNCTION_FEATURES, QVariantList(), useCase, debugFlags, parent) { } TimetableDataRequestJob::TimetableDataRequestJob( const ScriptData &scriptData, const AbstractRequest *request, const QString &useCase, DebugFlags debugFlags, QObject *parent ) : CallScriptFunctionJob(scriptData, useCase, debugFlags, parent), m_request(request->clone()) { QMutexLocker locker( m_mutex ); switch ( request->parseMode() ) { case ParseForDepartures: case ParseForArrivals: m_functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE; break; case ParseForAdditionalData: m_functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA; break; case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: m_functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS; break; case ParseForStopSuggestions: m_functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS; break; default: // This should never happen, therefore no i18n m_explanation = "Unknown parse mode"; m_success = false; return; } } QScriptValueList CallScriptFunctionJob::createArgumentScriptValues( DebuggerAgent *debugger ) const { // m_engineSemaphore is locked QMutexLocker locker( m_mutex ); if ( !debugger ) { debugger = m_agent; } QScriptValueList args; QScriptEngine *engine = debugger->engine(); foreach ( const QVariant &argument, m_arguments ) { args << engine->toScriptValue( argument ); } return args; } QScriptValueList TimetableDataRequestJob::createArgumentScriptValues( DebuggerAgent *debugger ) const { // m_engineSemaphore is locked QMutexLocker locker( m_mutex ); if ( !debugger ) { debugger = m_agent; } return QScriptValueList() << m_request->toScriptValue( debugger->engine() ); } void TimetableDataRequestJob::finish( const QList &data ) { QMutexLocker locker( m_mutex ); m_timetableData = data; } CallScriptFunctionJob::~CallScriptFunctionJob() { } TimetableDataRequestJob::~TimetableDataRequestJob() { QMutexLocker locker( m_mutex ); delete m_request; } QVariant CallScriptFunctionJob::returnValue() const { QMutexLocker locker( m_mutex ); return m_returnValue; } QList< Enums::ProviderFeature > TestFeaturesJob::features() const { QMutexLocker locker( m_mutex ); return m_features; } QList TimetableDataRequestJob::timetableData() const { QMutexLocker locker( m_mutex ); return m_timetableData; } QSharedPointer< AbstractRequest > TimetableDataRequestJob::request() const { QMutexLocker locker( m_mutex ); return QSharedPointer< AbstractRequest >( m_request->clone() ); } QList< TimetableDataRequestMessage > CallScriptFunctionJob::additionalMessages() const { QMutexLocker locker( m_mutex ); return m_additionalMessages; } QString CallScriptFunctionJob::functionName() const { QMutexLocker locker( m_mutex ); return m_functionName; } DebugFlags CallScriptFunctionJob::debugFlags() const { QMutexLocker locker( m_mutex ); return m_debugFlags; } QString CallScriptFunctionJob::toString() const { QMutexLocker locker( m_mutex ); if ( m_returnValue.isValid() ) { QString valueString = m_returnValue.toString(); if ( valueString.length() > 100 ) { valueString = valueString.left(100) + "..."; } else if ( valueString.isEmpty() ) { valueString = "undefined"; } return QString("%1 (%2() = %3)").arg( DebuggerJob::toString() ) .arg( m_functionName ).arg( valueString ); } else { return QString("%1 (%2())").arg( DebuggerJob::toString() ).arg( m_functionName ); } } QString CallScriptFunctionJob::defaultUseCase() const { QMutexLocker locker( m_mutex ); return i18nc("@info", "Call function %1()", m_functionName); } QString TimetableDataRequestJob::defaultUseCase() const { QMutexLocker locker( m_mutex ); return i18nc("@info", "Call function %1( %2 )", m_functionName, m_request->argumentsString()); } bool TestFeaturesJob::testResults() { QMutexLocker locker( m_mutex ); if ( !m_returnValue.isValid() ) { return false; } const QVariantList items = m_returnValue.toList(); if ( items.isEmpty() ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "No provider features returned"), TimetableDataRequestMessage::Warning ); } else { int i = 1; foreach ( const QVariant &item, items ) { const Enums::ProviderFeature feature = static_cast< Enums::ProviderFeature >( item.toInt() ); if ( feature == Enums::InvalidProviderFeature ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Invalid ProviderFeature: '%1'", item.toString()), TimetableDataRequestMessage::Error ); } else { m_features << feature; m_additionalMessages << TimetableDataRequestMessage( QString("%1: %2").arg(i).arg(Enums::toString(feature)), TimetableDataRequestMessage::Information ); } ++i; } } return true; } void CallScriptFunctionJob::connectScriptObjects( bool doConnect ) { DebuggerJob::connectScriptObjects( doConnect ); QMutexLocker locker( m_mutex ); if ( doConnect ) { // Connect to request finished signals to store the time spent for network requests connect( m_objects.network.data(), SIGNAL(requestFinished(NetworkRequest::Ptr,QByteArray,bool,QString,QDateTime,int,int)), this, SLOT(requestFinished(NetworkRequest::Ptr,QByteArray,bool,QString,QDateTime,int,int)) ); connect( m_objects.network.data(), SIGNAL(synchronousRequestFinished(QString,QByteArray,bool,int,int,int)), this, SLOT(synchronousRequestFinished(QString,QByteArray,bool,int,int,int)) ); // Connect to the messageReceived() signal of the "helper" script object to collect // messages sent to "helper.error()", "helper.warning()" or "helper.information()" connect( m_objects.helper.data(), SIGNAL(messageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)), this, SLOT(slotScriptMessageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)) ); connect( m_objects.result.data(), SIGNAL(invalidDataReceived(Enums::TimetableInformation,QString,QScriptContextInfo,int,QVariantMap)), this, SLOT(invalidDataReceived(Enums::TimetableInformation,QString,QScriptContextInfo,int,QVariantMap)) ); // Connect to the stopped() signal of the agent to get notified when execution gets aborted connect( m_agent, SIGNAL(stopped(QDateTime,bool,bool,int,QString,QStringList)), this, SLOT(scriptStopped(QDateTime,bool,bool,int,QString,QStringList)) ); } else { disconnect( m_objects.network.data(), SIGNAL(requestFinished(NetworkRequest::Ptr,QByteArray,bool,QString,QDateTime,int,int)), this, SLOT(requestFinished(NetworkRequest::Ptr,QByteArray,bool,QString,QDateTime,int,int)) ); disconnect( m_objects.network.data(), SIGNAL(synchronousRequestFinished(QString,QByteArray,bool,int,int,int)), this, SLOT(synchronousRequestFinished(QString,QByteArray,bool,int,int,int)) ); disconnect( m_objects.helper.data(), SIGNAL(messageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)), this, SLOT(slotScriptMessageReceived(QString,QScriptContextInfo,QString,Helper::ErrorSeverity)) ); disconnect( m_objects.result.data(), SIGNAL(invalidDataReceived(Enums::TimetableInformation,QString,QScriptContextInfo,int,QVariantMap)), this, SLOT(invalidDataReceived(Enums::TimetableInformation,QString,QScriptContextInfo,int,QVariantMap)) ); disconnect( m_agent, SIGNAL(stopped(QDateTime,bool,bool,int,QString,QStringList)), this, SLOT(scriptStopped(QDateTime,bool,bool,int,QString,QStringList)) ); } } void CallScriptFunctionJob::debuggerRun() { m_mutex->lock(); if ( !m_success ) { // Job already marked as not successful, a derived class may have set it to false m_mutex->unlock(); return; } m_mutex->unlock(); // Create new engine and agent m_engineSemaphore->acquire(); DebuggerAgent *agent = createAgent(); if ( !agent ) { m_engineSemaphore->release(); return; } QScriptEngine *engine = agent->engine(); m_mutex->lock(); ScriptObjects objects = m_objects; const ScriptData data = m_data; const QString functionName = m_functionName; const DebugFlags debugFlags = m_debugFlags; m_mutex->unlock(); // Load script engine->evaluate( *data.program ); Q_ASSERT_X( !objects.network->hasRunningRequests() || !agent->engine()->isEvaluating(), "LoadScriptJob::debuggerRun()", "Evaluating the script should not start any asynchronous requests, bad script" ); QScriptValue function = engine->globalObject().property( functionName ); if ( !function.isFunction() ) { QMutexLocker locker( m_mutex ); destroyAgent(); m_engineSemaphore->release(); m_explanation = i18nc("@info/plain", "Did not find a '%1' function in the script.", functionName); m_success = false; return; } connectScriptObjects(); agent->setExecutionControlType( debugFlags.testFlag(InterruptAtStart) ? ExecuteInterrupt : ExecuteRun ); agent->setDebugFlags( debugFlags ); // Make this job responsive while running the script engine->setProcessEventsInterval( 50 ); // Call script function while the engine mutex is still locked const QScriptValueList arguments = createArgumentScriptValues( agent ); const QVariant returnValue = function.call( QScriptValue(), arguments ).toVariant(); // The called function returned, but asynchronous network requests may have been started. // Wait for all network requests to finish, because slots in the script may get called if ( !waitFor(objects.network.data(), SIGNAL(allRequestsFinished()), WaitForNetwork) ) { m_engineSemaphore->release(); return; } // Wait for script execution to finish if ( !waitFor(this, SIGNAL(stopped(QDateTime,ScriptStoppedFlags,int,QString,QStringList)), WaitForScriptFinish) ) { qWarning() << "Stopped signal not received"; m_engineSemaphore->release(); return; } qApp->processEvents(); const bool allNetworkRequestsFinished = !objects.network->hasRunningRequests(); const bool finishedSuccessfully = !agent->wasLastRunAborted(); // GlobalTimetableInfo globalInfo; // globalInfo.requestDate = QDate::currentDate(); // globalInfo.delayInfoAvailable = // !m_scriptResult->isHintGiven( ResultObject::NoDelaysForStop ); // Check for exceptions if ( finishedSuccessfully && agent->hasUncaughtException() ) { const QString uncaughtException = agent->uncaughtException().toString(); handleError( engine, i18nc("@info/plain", "Error in the script at line %1" "in function '%2': %3.", agent->uncaughtExceptionLineNumber(), functionName, uncaughtException) ); - m_mutex->lockInline(); + m_mutex->lock(); destroyAgent(); - m_mutex->unlockInline(); + m_mutex->unlock(); m_engineSemaphore->release(); return; } - // Unlock engine mutex after execution was finished m_engineSemaphore->release(); - m_mutex->lockInline(); + m_mutex->lock(); m_returnValue = returnValue; m_aborted = agent->wasLastRunAborted(); bool quit = m_quit; - m_mutex->unlockInline(); + m_mutex->unlock(); if ( !quit ) { if ( functionName == ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA ) { const QVariantMap map = returnValue.toMap(); TimetableData data; for ( QVariantMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it ) { data[ Global::timetableInformationFromString(it.key()) ] = it.value(); } if ( !data.isEmpty() ) { finish( QList() << data ); } } else { finish( objects.result->data() ); } } // Process signals from the debugger before it gets deleted qApp->processEvents(); QMutexLocker locker( m_mutex ); m_engineSemaphore->acquire(); destroyAgent(); m_engineSemaphore->release(); if ( !m_success || m_quit ) { m_success = false; } else if ( allNetworkRequestsFinished && finishedSuccessfully ) { // No uncaught exceptions, all network requests finished if ( m_aborted ) { m_success = false; m_explanation = i18nc("@info/plain", "Execution was aborted"); } else { m_success = testResults(); } } else if ( finishedSuccessfully ) { // The script finish successfully, but not all network requests finished m_explanation = i18nc("@info/plain", "Not all network requests were finished in time"); m_success = false; } else if ( m_aborted ) { // Script was aborted m_explanation = i18nc("@info/plain", "Aborted"); m_success = false; } else { // The script did not finish successfully, // ignore if not all network requests were finished here m_explanation = i18nc("@info/plain", "The script did not finish in time, " "there may be an infinite loop."); m_success = false; } } void CallScriptFunctionJob::scriptStopped( const QDateTime ×tamp, bool aborted, bool hasRunningRequests, int uncaughtExceptionLineNumber, const QString &uncaughtException, const QStringList &backtrace ) { Q_UNUSED( timestamp ); Q_UNUSED( hasRunningRequests ); if ( aborted ) { handleError( uncaughtExceptionLineNumber, uncaughtException, backtrace, i18nc("@info/plain", "Aborted") ); } } void CallScriptFunctionJob::slotScriptMessageReceived( const QString &message, const QScriptContextInfo &context, const QString &failedParseText, Helper::ErrorSeverity severity ) { Q_UNUSED( failedParseText ); TimetableDataRequestMessage::Type type; switch ( severity ) { case Helper::Warning: type = TimetableDataRequestMessage::Warning; break; case Helper::Fatal: type = TimetableDataRequestMessage::Error; break; default: case Helper::Information: type = TimetableDataRequestMessage::Information; break; } QMutexLocker locker( m_mutex ); const TimetableDataRequestMessage newMessage( i18nc("@info/plain", "Error in file %1, line %2: " "%3", QFileInfo(context.fileName()).fileName(), context.lineNumber(), message), type, context.fileName(), context.lineNumber() ); if ( !m_additionalMessages.isEmpty() && m_additionalMessages.last() == newMessage ) { ++m_additionalMessages.last().repetitions; } else { m_additionalMessages << newMessage; } } void CallScriptFunctionJob::invalidDataReceived( Enums::TimetableInformation information, const QString &message, const QScriptContextInfo &context, int index, const QVariantMap& map ) { Q_UNUSED( information ); Q_UNUSED( map ); QMutexLocker locker( m_mutex ); m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Invalid data in result %1, line %2: %3", index + 1, context.lineNumber(), message), TimetableDataRequestMessage::Error, context.fileName(), context.lineNumber() ); } void CallScriptFunctionJob::requestFinished( const NetworkRequest::Ptr &request, const QByteArray &data, bool error, const QString &errorString, const QDateTime ×tamp, int statusCode, int size ) { Q_UNUSED( data ); emit asynchronousRequestWaitFinished( timestamp, statusCode, size ); QMutexLocker locker( m_mutex ); if ( error ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Download failed (%1): %2", errorString, request->url()), TimetableDataRequestMessage::Information, QString(), -1, TimetableDataRequestMessage::OpenLink, request->url() ); } else { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Download finished (status %1): %2, %3", statusCode, KGlobal::locale()->formatByteSize(size), request->url()), TimetableDataRequestMessage::Information, QString(), -1, TimetableDataRequestMessage::OpenLink, request->url() ); } } void CallScriptFunctionJob::synchronousRequestFinished( const QString &url, const QByteArray &data, bool cancelled, int statusCode, int waitingTime, int size ) { Q_UNUSED(data); emit synchronousRequestWaitFinished( statusCode, waitingTime, size ); QMutexLocker locker( m_mutex ); if ( cancelled ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Download cancelled/failed (status %1): %2", statusCode, url), TimetableDataRequestMessage::Warning, QString(), -1, TimetableDataRequestMessage::OpenLink, url ); } else { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Download finished (status %1): %2, %3", statusCode, KGlobal::locale()->formatByteSize(size), url), TimetableDataRequestMessage::Information, QString(), -1, TimetableDataRequestMessage::OpenLink, url ); } } bool TimetableDataRequestJob::testResults() { QMutexLocker locker( m_mutex ); const AbstractRequest *request = m_request; const DepartureRequest *departureRequest = dynamic_cast< const DepartureRequest* >( request ); if ( departureRequest ) { return testDepartureData( departureRequest ); } const StopSuggestionRequest *stopSuggestionRequest = dynamic_cast< const StopSuggestionRequest* >( request ); if ( stopSuggestionRequest ) { return testStopSuggestionData( stopSuggestionRequest ); } const StopsByGeoPositionRequest *stopsByGeoPositionRequest = dynamic_cast< const StopsByGeoPositionRequest* >( request ); if ( stopsByGeoPositionRequest ) { return testStopSuggestionData( stopsByGeoPositionRequest ); } const JourneyRequest *journeyRequest = dynamic_cast< const JourneyRequest* >( request ); if ( journeyRequest ) { return testJourneyData( journeyRequest ); } const AdditionalDataRequest *additinalDataRequest = dynamic_cast< const AdditionalDataRequest* >( request ); if ( additinalDataRequest ) { return testAdditionalData( additinalDataRequest ); } return false; } TimetableDataRequestMessage CallScriptFunctionJob::message( MessageType messageType, Enums::TimetableInformation info1, Enums::TimetableInformation info2, int count1, int count2, TimetableDataRequestMessage::Type type, const QString &fileName, int lineNumber ) { QString msg; switch ( messageType ) { case NotSameNumberOfItems: msg = i18nc("@info/plain", "'%1' should contain the same number of elements like '%3'. " "Found %2 values for '%1' and %4 values for '%3'", Global::timetableInformationToString(info1), count1, Global::timetableInformationToString(info2), count2); break; case NotOneItemLessThan: msg = i18nc("@info/plain", "'%1' should contain one element less than '%3'. " "Found %2 values for '%1' and %4 values for '%3'", Global::timetableInformationToString(info1), count1, Global::timetableInformationToString(info2), count2); break; default: qWarning() << "Unknown message type" << messageType; break; } return TimetableDataRequestMessage( msg, type, fileName, lineNumber ); } bool TimetableDataRequestJob::testAdditionalData( const AdditionalDataRequest *request ) { Q_UNUSED(request); QMutexLocker locker( m_mutex ); if ( m_timetableData.isEmpty() ) { m_explanation = i18nc("@info/plain", "No additional data found"); return false; } const TimetableData timetableData = m_timetableData.first(); for ( TimetableData::ConstIterator it = timetableData.constBegin(); it != timetableData.constEnd(); ++it ) { const bool isValid = Global::checkTimetableInformation( it.key(), it.value() ); QString info; if ( it->canConvert(QVariant::List) ) { const QVariantList list = it->toList(); QStringList elements; foreach ( const QVariant &item, list ) { elements << item.toString(); } info = "[" + elements.join(", ") + "]"; } else { info = it->toString(); } m_additionalMessages << TimetableDataRequestMessage( QString("%1%2: %3") .arg(Global::timetableInformationToString(it.key())) .arg(isValid ? QString() : (' ' + i18nc("@info/plain", "(invalid)"))) .arg(info), isValid ? TimetableDataRequestMessage::Information : TimetableDataRequestMessage::Warning ); } return true; } bool TimetableDataRequestJob::testDepartureData( const DepartureRequest *request ) { QMutexLocker locker( m_mutex ); if ( m_timetableData.isEmpty() ) { if ( request->parseMode() == ParseForArrivals ) { m_explanation = i18nc("@info/plain", "No arrivals found"); } else { m_explanation = i18nc("@info/plain", "No departures found"); } return false; } // Get global information QStringList globalInfos; if ( m_returnValue.isValid() ) { globalInfos = m_returnValue.toStringList(); } // Get result set int countInvalid = 0; QDate curDate; QTime lastTime; for ( int i = 0; i < m_timetableData.count(); ++ i ) { TimetableData timetableData = m_timetableData[ i ]; QDateTime departureDateTime = timetableData.value( Enums::DepartureDateTime ).toDateTime(); if ( !departureDateTime.isValid() ) { QDate date = timetableData.value( Enums::DepartureDate ).toDate(); QTime departureTime; if ( timetableData.values().contains(departureTime) ) { QVariant timeValue = timetableData.value( Enums::DepartureTime ); if ( timeValue.canConvert(QVariant::Time) ) { departureTime = timeValue.toTime(); } else { departureTime = QTime::fromString( timeValue.toString(), "hh:mm:ss" ); if ( !departureTime.isValid() ) { departureTime = QTime::fromString( timeValue.toString(), "hh:mm" ); } } } if ( !date.isValid() ) { if ( curDate.isNull() ) { // First departure if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 ) date = QDate::currentDate().addDays( -1 ); else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < 3 ) date = QDate::currentDate().addDays( 1 ); else date = QDate::currentDate(); } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) { // Time too much ealier than last time, estimate it's tomorrow date = curDate.addDays( 1 ); } else { date = curDate; } } departureDateTime = QDateTime( date, departureTime ); timetableData.insert( Enums::DepartureDateTime, departureDateTime ); } curDate = departureDateTime.date(); lastTime = departureDateTime.time(); const bool isValid = timetableData.contains(Enums::TransportLine) && timetableData.contains(Enums::Target) && timetableData.contains(Enums::DepartureDateTime); if ( !isValid ) { ++countInvalid; m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "Data missing in result %1, required are TransportLine, " "Target and at least a departure time (better also a date)", i), TimetableDataRequestMessage::Error ); } m_additionalMessages << TimetableDataRequestMessage( QString("%1: %2 (%3, %4), %5").arg(i + 1) .arg(timetableData[Enums::TransportLine].toString()) .arg(Enums::toString(static_cast(timetableData[Enums::TypeOfVehicle].toInt()))) .arg(timetableData[Enums::DepartureDateTime].toDateTime().time().toString(Qt::DefaultLocaleShortDate)) .arg(timetableData[Enums::Target].toString()), isValid ? TimetableDataRequestMessage::Information : TimetableDataRequestMessage::Warning ); } bool success = true; for ( int i = 0; i < m_timetableData.count(); ++i ) { TimetableData values = m_timetableData[i]; // If RouteStops data is available test it and associated TimetableInformation values if ( values.contains(Enums::RouteStops) && !values[Enums::RouteStops].toStringList().isEmpty() ) { // Check if RouteTimesDeparture has one element less than RouteStops // and if RouteTimesDepartureDelay has the same number of elements as RouteStops (if set) const QStringList routeStops = values[Enums::RouteStops].toStringList(); if ( values.contains(Enums::RouteTimes) && !values[Enums::RouteTimes].toList().isEmpty() ) { const QVariantList routeTimes = values[Enums::RouteTimes].toList(); if ( routeTimes.count() != routeStops.count() ) { m_additionalMessages << message( NotSameNumberOfItems, Enums::RouteTimes, Enums::RouteStops, routeTimes.count(), routeStops.count() ); success = false; } if ( values.contains(Enums::RouteTimesDepartureDelay) && !values[Enums::RouteTimesDepartureDelay].toStringList().isEmpty() ) { const QStringList routeTimesDepartureDelay = values[Enums::RouteTimesDepartureDelay].toStringList(); if ( routeTimesDepartureDelay.count() != routeTimes.count() ) { m_additionalMessages << message( NotSameNumberOfItems, Enums::RouteTimesDepartureDelay, Enums::RouteTimes, routeTimesDepartureDelay.count(), routeTimes.count() ); success = false; } } } if ( values.contains(Enums::RouteTypesOfVehicles) && !values[Enums::RouteTypesOfVehicles].toList().isEmpty() ) { const QVariantList routeTypesOfVehicles = values[Enums::RouteTypesOfVehicles].toList(); if ( routeTypesOfVehicles.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RouteTypesOfVehicles, Enums::RouteStops, routeTypesOfVehicles.count(), routeStops.count() ); success = false; } } // Check if RoutePlatformsDeparture has one element less than RouteStops if ( values.contains(Enums::RoutePlatformsDeparture) && !values[Enums::RoutePlatformsDeparture].toStringList().isEmpty() ) { const QStringList routePlatformsDeparture = values[Enums::RoutePlatformsDeparture].toStringList(); if ( routePlatformsDeparture.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RoutePlatformsDeparture, Enums::RouteStops, routePlatformsDeparture.count(), routeStops.count() ); success = false; } } // Check if RoutePlatformsArrival has one element less than RouteStops if ( values.contains(Enums::RoutePlatformsArrival) && !values[Enums::RoutePlatformsArrival].toStringList().isEmpty() ) { const QStringList routePlatformsArrival = values[Enums::RoutePlatformsArrival].toStringList(); if ( routePlatformsArrival.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RoutePlatformsArrival, Enums::RouteStops, routePlatformsArrival.count(), routeStops.count() ); success = false; } } } else { // values does not contain RouteStops QList< Enums::TimetableInformation > infos; infos << Enums::RouteTimes << Enums::RoutePlatformsDeparture << Enums::RoutePlatformsArrival << Enums::RouteExactStops << Enums::RouteStopsShortened << Enums::RouteTypesOfVehicles << Enums::RouteTransportLines; foreach ( Enums::TimetableInformation info, infos ) { if ( values.contains(info) ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "'%1' data ignored, because data for 'RouteStops' is missing.", Global::timetableInformationToString(info)), TimetableDataRequestMessage::Warning ); success = false; } } } } // Show results if ( request->parseMode() == ParseForArrivals ) { m_explanation = i18ncp("@info/plain", "Got %1 arrival", "Got %1 arrivals", m_timetableData.count()); } else { m_explanation = i18ncp("@info/plain", "Got %1 departure", "Got %1 departures", m_timetableData.count()); } if ( countInvalid > 0 ) { if ( request->parseMode() == ParseForArrivals ) { m_explanation += ", " + i18ncp("@info/plain", "%1 arrival is invalid", "%1 arrivals are invalid", countInvalid); } else { m_explanation += ", " + i18ncp("@info/plain", "%1 departure is invalid", "%1 departures are invalid", countInvalid); } return false; } if ( globalInfos.contains("no delays", Qt::CaseInsensitive) ) { // No delay information available for the given stop m_explanation += ", " + i18nc("@info/plain", "Got the information from the script that there " "is no delay information available for the given stop."); } return success; } bool TimetableDataRequestJob::testStopSuggestionData( const StopSuggestionRequest *request ) { Q_UNUSED( request ); QMutexLocker locker( m_mutex ); if ( m_timetableData.isEmpty() ) { m_explanation = i18nc("@info/plain", "No stop suggestions found"); return false; } // Get global information QStringList globalInfos; if ( m_returnValue.isValid() ) { globalInfos = m_returnValue.toStringList(); } // Test timetable data int countInvalid = 0; int i = 1; foreach ( const TimetableData &timetableData, m_timetableData ) { const bool isValid = !timetableData[Enums::StopName].toString().isEmpty(); if ( !isValid ) { ++countInvalid; } QString info = timetableData[Enums::StopName].toString(); if ( info.length() > 50 ) { info = info.left(50) + "..."; } if ( timetableData.contains(Enums::StopID) ) { info += ", ID: " + timetableData[Enums::StopID].toString(); } if ( timetableData.contains(Enums::StopWeight) ) { info += ", weight: " + timetableData[Enums::StopWeight].toString(); } if ( timetableData.contains(Enums::StopCity) ) { info += ", city: " + timetableData[Enums::StopCity].toString(); } if ( timetableData.contains(Enums::StopLongitude) ) { info += ", longitude: " + timetableData[Enums::StopLongitude].toString(); } if ( timetableData.contains(Enums::StopLatitude) ) { info += ", latitude: " + timetableData[Enums::StopLatitude].toString(); } m_additionalMessages << TimetableDataRequestMessage( QString("%1%2: %3").arg(i) .arg(isValid ? QString() : (' ' + i18nc("@info/plain", "(invalid)"))) .arg(info), isValid ? TimetableDataRequestMessage::Information : TimetableDataRequestMessage::Warning ); ++i; } // Show results m_explanation = i18ncp("@info/plain", "Got %1 stop suggestion", "Got %1 stop suggestions", m_timetableData.count()); if ( countInvalid > 0 ) { m_explanation += ", " + i18ncp("@info/plain", "%1 stop suggestion is invalid", "%1 stop suggestions are invalid", countInvalid); return false; } return true; } bool TimetableDataRequestJob::testJourneyData( const JourneyRequest *request ) { Q_UNUSED( request ); QMutexLocker locker( m_mutex ); if ( m_timetableData.isEmpty() ) { m_explanation = i18nc("@info/plain", "No journeys found"); return false; } // Get global information QStringList globalInfos; if ( m_returnValue.isValid() ) { globalInfos = m_returnValue.toStringList(); } m_explanation = i18ncp("@info/plain", "Got %1 journey", "Got %1 journeys", m_timetableData.count()); // Get result set int countInvalid = 0; QDate curDate; QTime lastTime; for ( int i = 0; i < m_timetableData.count(); ++ i ) { TimetableData timetableData = m_timetableData[ i ]; QDateTime departureDateTime = timetableData.value( Enums::DepartureDateTime ).toDateTime(); if ( !departureDateTime.isValid() ) { QDate date = timetableData.value( Enums::DepartureDate ).toDate(); QTime departureTime; if ( timetableData.values().contains(Enums::DepartureTime) ) { QVariant timeValue = timetableData[ Enums::DepartureTime ]; if ( timeValue.canConvert(QVariant::Time) ) { departureTime = timeValue.toTime(); } else { departureTime = QTime::fromString( timeValue.toString(), "hh:mm:ss" ); if ( !departureTime.isValid() ) { departureTime = QTime::fromString( timeValue.toString(), "hh:mm" ); } } } if ( !date.isValid() ) { if ( curDate.isNull() ) { // First departure if ( QTime::currentTime().hour() < 3 && departureTime.hour() > 21 ) date = QDate::currentDate().addDays( -1 ); else if ( QTime::currentTime().hour() > 21 && departureTime.hour() < 3 ) date = QDate::currentDate().addDays( 1 ); else date = QDate::currentDate(); } else if ( lastTime.secsTo(departureTime) < -5 * 60 ) { // Time too much ealier than last time, estimate it's tomorrow date = curDate.addDays( 1 ); } else { date = curDate; } } departureDateTime = QDateTime( date, departureTime ); timetableData.insert( Enums::DepartureDateTime, departureDateTime ); } QDateTime arrivalDateTime = timetableData[ Enums::ArrivalDateTime ].toDateTime(); if ( !departureDateTime.isValid() ) { QDate date = timetableData[ Enums::ArrivalDate ].toDate(); QTime arrivalTime; if ( timetableData.contains(Enums::ArrivalTime) ) { QVariant timeValue = timetableData[ Enums::ArrivalTime ]; if ( timeValue.canConvert(QVariant::Time) ) { arrivalTime = timeValue.toTime(); } else { arrivalTime = QTime::fromString( timeValue.toString(), "hh:mm:ss" ); if ( !arrivalTime.isValid() ) { arrivalTime = QTime::fromString( timeValue.toString(), "hh:mm" ); } } } if ( !date.isValid() ) { date = departureDateTime.date(); } arrivalDateTime = QDateTime( date, arrivalTime ); if ( arrivalDateTime < departureDateTime ) { arrivalDateTime = arrivalDateTime.addDays( 1 ); } timetableData.insert( Enums::ArrivalDateTime, arrivalDateTime ); } curDate = departureDateTime.date(); lastTime = departureDateTime.time(); bool isValid = timetableData.contains(Enums::StartStopName) && timetableData.contains(Enums::TargetStopName) && timetableData.contains(Enums::DepartureDateTime) && timetableData.contains(Enums::ArrivalDateTime); if ( !isValid ) { ++countInvalid; } m_additionalMessages << TimetableDataRequestMessage( QString("%1: %2 (%3) - %4 (%5), %6 route stops").arg(i + 1) .arg(timetableData[Enums::StartStopName].toString()) .arg(timetableData[Enums::DepartureDateTime].toDateTime().time().toString(Qt::DefaultLocaleShortDate)) .arg(timetableData[Enums::TargetStopName].toString()) .arg(timetableData[Enums::ArrivalDateTime].toDateTime().time().toString(Qt::DefaultLocaleShortDate)) .arg(timetableData[Enums::RouteStops].toStringList().count()), isValid ? TimetableDataRequestMessage::Information : TimetableDataRequestMessage::Error ); } bool success = true; for ( int i = 0; i < m_timetableData.count(); ++i ) { TimetableData values = m_timetableData[i]; if ( values.contains(Enums::RouteStops) && !values[Enums::RouteStops].toStringList().isEmpty() ) { // Check if RouteTimesDeparture has one element less than RouteStops // and if RouteTimesDepartureDelay has the same number of elements as RouteStops (if set) const QStringList routeStops = values[Enums::RouteStops].toStringList(); if ( values.contains(Enums::RouteTimesDeparture) && !values[Enums::RouteTimesDeparture].toStringList().isEmpty() ) { const QStringList routeTimesDeparture = values[Enums::RouteTimesDeparture].toStringList(); if ( routeTimesDeparture.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RouteTimesDeparture, Enums::RouteStops, routeTimesDeparture.count(), routeStops.count() ); success = false; } if ( values.contains(Enums::RouteTimesDepartureDelay) && !values[Enums::RouteTimesDepartureDelay].toStringList().isEmpty() ) { const QStringList routeTimesDepartureDelay = values[Enums::RouteTimesDepartureDelay].toStringList(); if ( routeTimesDepartureDelay.count() != routeTimesDeparture.count() ) { m_additionalMessages << message( NotSameNumberOfItems, Enums::RouteTimesDepartureDelay, Enums::RouteTimesDeparture, routeTimesDepartureDelay.count(), routeTimesDeparture.count() ); success = false; } } } // Check if RoutePlatformsDeparture has one element less than RouteStops if ( values.contains(Enums::RoutePlatformsDeparture) && !values[Enums::RoutePlatformsDeparture].toStringList().isEmpty() ) { const QStringList routePlatformsArrival = values[Enums::RoutePlatformsArrival].toStringList(); if ( routePlatformsArrival.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RoutePlatformsArrival, Enums::RouteStops, routePlatformsArrival.count(), routeStops.count() ); success = false; } } // Check if RouteTimesArrival has one element less than RouteStops // and if RouteTimesArrivalDelay has the same number of elements as RouteStops (if set) if ( values.contains(Enums::RouteTimesArrival) && !values[Enums::RouteTimesArrival].toStringList().isEmpty() ) { const QStringList routeTimesArrival = values[Enums::RouteTimesArrival].toStringList(); if ( routeTimesArrival.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RouteTimesArrival, Enums::RouteStops, routeTimesArrival.count(), routeStops.count() ); success = false; } if ( values.contains(Enums::RouteTimesArrivalDelay) && !values[Enums::RouteTimesArrivalDelay].toStringList().isEmpty() ) { const QStringList routeTimesArrivalDelay = values[Enums::RouteTimesArrivalDelay].toStringList(); if ( routeTimesArrivalDelay.count() != routeTimesArrival.count() ) { m_additionalMessages << message( NotSameNumberOfItems, Enums::RouteTimesArrivalDelay, Enums::RouteTimesArrival, routeTimesArrivalDelay.count(), routeTimesArrival.count() ); success = false; } } } // Check if RoutePlatformsArrival has one element less than RouteStops if ( values.contains(Enums::RoutePlatformsArrival) && !values[Enums::RoutePlatformsArrival].toStringList().isEmpty() ) { const QStringList routePlatformsArrival = values[Enums::RoutePlatformsArrival].toStringList(); if ( routePlatformsArrival.count() != routeStops.count() - 1 ) { m_additionalMessages << message( NotOneItemLessThan, Enums::RoutePlatformsArrival, Enums::RouteStops, routePlatformsArrival.count(), routeStops.count() ); success = false; } } } else { // values does not contain RouteStops else { // values does not contain RouteStops QList< Enums::TimetableInformation > infos; infos << Enums::RouteTimes << Enums::RoutePlatformsDeparture << Enums::RoutePlatformsArrival << Enums::RouteTimesArrival << Enums::RouteTimesArrivalDelay << Enums::RouteTimesDeparture << Enums::RouteTimesDepartureDelay << Enums::RouteStopsShortened << Enums::RouteTypesOfVehicles << Enums::RouteTransportLines; foreach ( Enums::TimetableInformation info, infos ) { if ( values.contains(info) ) { m_additionalMessages << TimetableDataRequestMessage( i18nc("@info/plain", "'%1' data ignored, because data for 'RouteStops' is missing.", Global::timetableInformationToString(info)), TimetableDataRequestMessage::Warning ); success = false; } } } } return success; } } // namespace Debugger