diff --git a/engine/script/script_thread.cpp b/engine/script/script_thread.cpp index a232a00..c15fcce 100644 --- a/engine/script/script_thread.cpp +++ b/engine/script/script_thread.cpp @@ -1,907 +1,907 @@ /* * 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 "script_thread.h" // Own includes #include "config.h" #include "scriptapi.h" #include "script/serviceproviderscript.h" #include "serviceproviderdata.h" #include "request.h" #include "serviceproviderglobal.h" // KDE includes #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include ScriptJob::ScriptJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, QObject* parent ) - : ThreadWeaver::Job(parent), m_engine(0), m_mutex(new QMutex(QMutex::Recursive)), + : ThreadWeaver::Job(), m_engine(0), m_mutex(new QMutex(QMutex::Recursive)), m_data(data), m_eventLoop(0), m_published(0), m_quit(false), m_success(true) { Q_ASSERT_X( data.isValid(), "ScriptJob constructor", "Needs valid script data" ); // Use global storage, may contain non-persistent data from earlier requests. // Does not need to live in this thread, it does not create any new QObjects. QMutexLocker locker( m_mutex ); m_objects.storage = scriptStorage; qRegisterMetaType( "DepartureRequest" ); qRegisterMetaType( "ArrivalRequest" ); qRegisterMetaType( "JourneyRequest" ); qRegisterMetaType( "StopSuggestionRequest" ); qRegisterMetaType( "StopsByGeoPositionRequest" ); qRegisterMetaType( "AdditionalDataRequest" ); } ScriptJob::~ScriptJob() { // Abort, if still running requestAbort(); // Do some cleanup if not done already cleanup(); delete m_mutex; } void ScriptJob::requestAbort() { m_mutex->lock(); if ( !m_engine ) { // Is already finished m_mutex->unlock(); return; } else if ( m_quit ) { // Is already aborting, wait for the script engine to get destroyed QEventLoop loop; connect( m_engine, SIGNAL(destroyed(QObject*)), &loop, SLOT(quit()) ); m_mutex->unlock(); // Run the event loop, waiting for the engine to get destroyed or a 1 second timeout QTimer::singleShot( 1000, &loop, SLOT(quit()) ); loop.exec(); return; } // Is still running, remember to abort m_quit = true; // Abort running network requests, if any if ( m_objects.network->hasRunningRequests() ) { m_objects.network->abortAllRequests(); } // Abort script evaluation if ( m_engine ) { m_engine->abortEvaluation(); } // Wake waiting event loops if ( m_eventLoop ) { QEventLoop *loop = m_eventLoop; m_eventLoop = 0; loop->quit(); } m_mutex->unlock(); // Wait until signals are processed qApp->processEvents(); } ScriptAgent::ScriptAgent( QScriptEngine* engine, QObject *parent ) : QObject(parent), QScriptEngineAgent(engine) { } void ScriptAgent::functionExit( qint64 scriptId, const QScriptValue& returnValue ) { Q_UNUSED( scriptId ); Q_UNUSED( returnValue ); QTimer::singleShot( 250, this, SLOT(checkExecution()) ); } void ScriptAgent::checkExecution() { if ( !engine()->isEvaluating() ) { emit scriptFinished(); } } -void ScriptJob::run() +void ScriptJob::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) { m_mutex->lock(); if ( !loadScript(m_data.program) ) { kDebug() << "Script could not be loaded correctly"; m_mutex->unlock(); return; } QScriptEngine *engine = m_engine; ScriptObjects objects = m_objects; const QString scriptFileName = m_data.provider.scriptFileName(); const QScriptValueList arguments = QScriptValueList() << request()->toScriptValue( engine ); const ParseDocumentMode parseMode = request()->parseMode(); m_mutex->unlock(); // Add call to the appropriate function QString functionName; switch ( parseMode ) { case ParseForDepartures: case ParseForArrivals: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE; break; case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS; break; case ParseForStopSuggestions: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS; break; case ParseForAdditionalData: functionName = ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA; break; default: kDebug() << "Parse mode unsupported:" << parseMode; break; } if ( functionName.isEmpty() ) { // This should never happen, therefore no i18n handleError( "Unknown parse mode" ); return; } // Check if the script function is implemented QScriptValue function = engine->globalObject().property( functionName ); if ( !function.isFunction() ) { handleError( i18nc("@info/plain", "Function %1 not implemented by " "the script %1", functionName, scriptFileName) ); return; } // Store start time of the script QElapsedTimer timer; timer.start(); // Call script function QScriptValue result = function.call( QScriptValue(), arguments ); if ( engine->hasUncaughtException() ) { // TODO Get filename where the exception occured, maybe use ScriptAgent for that handleError( i18nc("@info/plain", "Error in script function %1, " "line %2: %3.", functionName, engine->uncaughtExceptionLineNumber(), engine->uncaughtException().toString()) ); return; } // Inform about script run time DEBUG_ENGINE_JOBS( "Script finished in" << (timer.elapsed() / 1000.0) << "seconds: " << scriptFileName << parseMode ); GlobalTimetableInfo globalInfo; globalInfo.requestDate = QDate::currentDate(); globalInfo.delayInfoAvailable = !objects.result->isHintGiven( ResultObject::NoDelaysForStop ); // The called function returned, but asynchronous network requests may have been started. // Slots in the script may be connected to those requests and start script execution again. // In the execution new network requests may get started and so on. // // Wait until all network requests are finished and the script is not evaluating at the same // time. Define a global timeout, each waitFor() call subtracts the elapsed time. // // NOTE Network requests by default use a 30 seconds timeout, the global timeout should // be bigger. Synchronous requests that were started by signals of running asynchronous // requests are accounted as script execution time here. Synchronous requests that were // started before are already done. const int startTimeout = 60000; int timeout = startTimeout; while ( objects.network->hasRunningRequests() || engine->isEvaluating() ) { // Wait until all asynchronous network requests are finished if ( !waitFor(objects.network.data(), SIGNAL(allRequestsFinished()), WaitForNetwork, &timeout) ) { if ( timeout <= 0 ) { // Timeout expired while waiting for network requests to finish, // abort all requests that are still running objects.network->abortAllRequests(); QMutexLocker locker( m_mutex ); m_success = false; m_errorString = i18nc("@info", "Timeout expired after %1 while waiting for " "network requests to finish", KGlobal::locale()->prettyFormatDuration(startTimeout)); } cleanup(); return; } // Wait for script execution to finish, ie. slots connected to the last finished request(s) ScriptAgent agent( engine ); if ( !waitFor(&agent, SIGNAL(scriptFinished()), WaitForScriptFinish, &timeout) ) { if ( timeout <= 0 ) { // Timeout expired while waiting for script execution to finish, abort evaluation engine->abortEvaluation(); QMutexLocker locker( m_mutex ); m_success = false; m_errorString = i18nc("@info", "Timeout expired after %1 while waiting for script " "execution to finish", KGlobal::locale()->prettyFormatDuration(startTimeout)); } cleanup(); return; } // Check for new exceptions after waiting for script execution to finish (again) if ( engine->hasUncaughtException() ) { // TODO Get filename where the exception occured, maybe use ScriptAgent for that handleError( i18nc("@info/plain", "Error in script function %1, " "line %2: %3.", functionName, engine->uncaughtExceptionLineNumber(), engine->uncaughtException().toString()) ); return; } } // Publish remaining items, if any publish(); // Cleanup cleanup(); } void ScriptJob::cleanup() { QMutexLocker locker( m_mutex ); if ( !m_objects.storage.isNull() ) { m_objects.storage->checkLifetime(); } if ( !m_objects.result.isNull() ) { disconnect( m_objects.result.data(), 0, this, 0 ); } if ( m_engine ) { m_engine->deleteLater(); m_engine = 0; } } void ScriptJob::handleError( const QString &errorMessage ) { QMutexLocker locker( m_mutex ); kDebug() << "Error:" << errorMessage; kDebug() << "Backtrace:" << m_engine->uncaughtExceptionBacktrace().join("\n"); m_errorString = errorMessage; m_success = false; cleanup(); } QScriptValue networkRequestToScript( QScriptEngine *engine, const NetworkRequestPtr &request ) { return engine->newQObject( const_cast(request), QScriptEngine::QtOwnership, QScriptEngine::PreferExistingWrapperObject ); } void networkRequestFromScript( const QScriptValue &object, NetworkRequestPtr &request ) { request = qobject_cast( object.toQObject() ); } bool importExtension( QScriptEngine *engine, const QString &extension ) { if ( !ServiceProviderScript::allowedExtensions().contains(extension, Qt::CaseInsensitive) ) { if ( engine->availableExtensions().contains(extension, Qt::CaseInsensitive) ) { kDebug() << "Extension" << extension << "is not allowed currently"; } else { kDebug() << "Extension" << extension << "could not be found"; kDebug() << "Available extensions:" << engine->availableExtensions(); } kDebug() << "Allowed extensions:" << ServiceProviderScript::allowedExtensions(); return false; } else { if ( engine->importExtension(extension).isUndefined() ) { return true; } else { if ( engine->hasUncaughtException() ) { kDebug() << "Could not import extension" << extension << engine->uncaughtExceptionLineNumber() << engine->uncaughtException().toString(); kDebug() << "Backtrace:" << engine->uncaughtExceptionBacktrace().join("\n"); } return false; } } } bool ScriptJob::waitFor( QObject *sender, const char *signal, WaitForType type, int *timeout ) { if ( *timeout <= 0 ) { return false; } // Do not wait if the job was aborted or failed QMutexLocker locker( m_mutex ); if ( !m_success || m_quit ) { return false; } QScriptEngine *engine = m_engine; ScriptObjects objects = m_objects; // Test if the target condition is already met if ( (type == WaitForNetwork && objects.network->hasRunningRequests()) || (type == WaitForScriptFinish && engine->isEvaluating()) ) { // Not finished yet, wait for the given signal, // that should get emitted when the target condition is met QEventLoop loop; connect( sender, signal, &loop, SLOT(quit()) ); // Add a timeout to not wait forever (eg. because of an infinite loop in the script). // Use a QTimer variable to be able to check if the timeout caused the event loop to quit // rather than the given signal QTimer timer; timer.setSingleShot( true ); connect( &timer, SIGNAL(timeout()), &loop, SLOT(quit()) ); // Store a pointer to the event loop, to be able to quit it on abort. // Then start the timeout. m_eventLoop = &loop; QElapsedTimer elapsedTimer; elapsedTimer.start(); timer.start( *timeout ); m_mutex->unlock(); // Start the event loop waiting for the given signal / timeout. // QScriptEngine continues execution here or network requests continue to get handled and // may call script slots on finish. loop.exec(); // Test if the timeout has expired, ie. if the timer is no longer running (single shot) m_mutex->lock(); const bool timeoutExpired = !timer.isActive(); // Update remaining time of the timeout if ( timeoutExpired ) { *timeout = 0; } else { *timeout -= elapsedTimer.elapsed(); } // Test if the job was aborted while waiting if ( !m_eventLoop || m_quit ) { // Job was aborted m_eventLoop = 0; return false; } m_eventLoop = 0; // Generate errors if the timeout has expired and abort network requests / script execution if ( timeoutExpired ) { return false; } } // The target condition was met in time or was already met return true; } QScriptValue include( QScriptContext *context, QScriptEngine *engine ) { // Check argument count, include() call in global context? const QScriptContextInfo contextInfo( context->parentContext() ); if ( context->argumentCount() < 1 ) { context->throwError( i18nc("@info/plain", "One argument expected for include()") ); return engine->undefinedValue(); } else if ( context->parentContext() && context->parentContext()->parentContext() ) { QScriptContext *parentContext = context->parentContext()->parentContext(); bool error = false; while ( parentContext ) { const QScriptContextInfo parentContextInfo( parentContext ); if ( !parentContextInfo.fileName().isEmpty() && parentContextInfo.fileName() == contextInfo.fileName() ) { // Parent context is in the same file, error error = true; break; } parentContext = parentContext->parentContext(); } if ( error ) { context->throwError( i18nc("@info/plain", "include() calls must be in global context") ); return engine->undefinedValue(); } } // Check if this include() call is before all other statements QVariantHash includeData = context->callee().data().toVariant().toHash(); if ( includeData.contains(contextInfo.fileName()) ) { const quint16 maxIncludeLine = includeData[ contextInfo.fileName() ].toInt(); if ( contextInfo.lineNumber() > maxIncludeLine ) { context->throwError( i18nc("@info/plain", "include() calls must be the first statements") ); return engine->undefinedValue(); } } // Get argument and check that it's not pointing to another directory const QString fileName = context->argument(0).toString(); if ( fileName.contains('/') ) { context->throwError( i18nc("@info/plain", "Cannot include files from other directories") ); return engine->undefinedValue(); } // Find the script to be included const QString subDirectory = ServiceProviderGlobal::installationSubDirectory(); const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, subDirectory + fileName ); // Check if the script was already included QStringList includedFiles = engine->globalObject().property( "includedFiles" ).toVariant().toStringList(); if ( includedFiles.contains(filePath) ) { qWarning() << "File already included" << filePath; return engine->undefinedValue(); } // Try to open the file to be included QFile scriptFile( filePath ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { context->throwError( i18nc("@info/plain", "Cannot find file to be included: " "%1", fileName) ); return engine->undefinedValue(); } // Read the file QTextStream stream( &scriptFile ); const QString program = stream.readAll(); scriptFile.close(); if ( !includeData.contains(scriptFile.fileName()) ) { includeData[ scriptFile.fileName() ] = maxIncludeLine( program ); QScriptValue includeFunction = engine->globalObject().property("include"); Q_ASSERT( includeFunction.isValid() ); includeFunction.setData( qScriptValueFromValue(engine, includeData) ); engine->globalObject().setProperty( "include", includeFunction, QScriptValue::KeepExistingFlags ); } // Set script context QScriptContext *parent = context->parentContext(); if ( parent ) { context->setActivationObject( parent->activationObject() ); context->setThisObject( parent->thisObject() ); } // Store included files in global property "includedFiles" includedFiles << filePath; includedFiles.removeDuplicates(); QScriptValue::PropertyFlags flags = QScriptValue::ReadOnly | QScriptValue::Undeletable; engine->globalObject().setProperty( "includedFiles", engine->newVariant(includedFiles), flags ); // Evaluate script return engine->evaluate( program, filePath ); } quint16 maxIncludeLine( const QString &program ) { // Get first lines of script code until the first statement which is not an include() call // The regular expression matches in blocks: Multiline comments (needs non-greedy regexp), // one line comments, whitespaces & newlines, include("...") calls and semi-colons (needed, // because the regexp is non-greedy) QRegExp programRegExp( "\\s*(?:\\s*//[^\\n]*\\n|/\\*.*\\*/|[\\s\\n]+|include\\s*\\(\\s*\"[^\"]*\"\\s*\\)|;)+" ); programRegExp.setMinimal( true ); int pos = 0, nextPos = 0; QString programBegin; while ( (pos = programRegExp.indexIn(program, pos)) == nextPos ) { const QString capture = programRegExp.cap(); programBegin.append( capture ); pos += programRegExp.matchedLength(); nextPos = pos; } return programBegin.count( '\n' ); } bool ScriptJob::loadScript( const QSharedPointer< QScriptProgram > &script ) { if ( !script ) { kDebug() << "Invalid script data"; return false; } // Create script engine QMutexLocker locker( m_mutex ); m_engine = new QScriptEngine(); foreach ( const QString &extension, m_data.provider.scriptExtensions() ) { if ( !importExtension(m_engine, extension) ) { m_errorString = i18nc("@info/plain", "Could not load script extension " "%1.", extension); m_success = false; cleanup(); return false; } } // Create and attach script objects. // The Storage object is already created and will not be replaced by a new instance. // It lives in the GUI thread and gets used in all thread jobs to not erase non-persistently // stored data after each request. m_objects.createObjects( m_data ); m_objects.attachToEngine( m_engine, m_data ); // Connect the publish() signal directly (the result object lives in the thread that gets // run by this job, this job itself lives in the GUI thread). Connect directly to ensure // the script objects and the request are still valid (prevent crashes). connect( m_objects.result.data(), SIGNAL(publish()), this, SLOT(publish()), Qt::DirectConnection ); // Load the script program m_mutex->unlock(); m_engine->evaluate( *script ); m_mutex->lock(); if ( m_engine->hasUncaughtException() ) { kDebug() << "Error in the script" << m_engine->uncaughtExceptionLineNumber() << m_engine->uncaughtException().toString(); kDebug() << "Backtrace:" << m_engine->uncaughtExceptionBacktrace().join("\n"); m_errorString = i18nc("@info/plain", "Error in script, line %1: %2.", m_engine->uncaughtExceptionLineNumber(), m_engine->uncaughtException().toString()); m_success = false; cleanup(); return false; } else { return true; } } bool ScriptJob::hasDataToBePublished() const { QMutexLocker locker( m_mutex ); return !m_objects.isValid() ? false : m_objects.result->count() > m_published; } void ScriptJob::publish() { // This slot gets run in the thread of this job // Only publish, if there is data which is not already published if ( hasDataToBePublished() ) { m_mutex->lock(); GlobalTimetableInfo globalInfo; const QList< TimetableData > data = m_objects.result->data().mid( m_published ); const ResultObject::Features features = m_objects.result->features(); const ResultObject::Hints hints = m_objects.result->hints(); const QString lastUserUrl = m_objects.network->lastUserUrl(); const bool couldNeedForcedUpdate = m_published > 0; const MoreItemsRequest *moreItemsRequest = dynamic_cast(request()); const AbstractRequest *_request = moreItemsRequest ? moreItemsRequest->request().data() : request(); const ParseDocumentMode parseMode = request()->parseMode(); m_lastUrl = m_objects.network->lastUrl(); // TODO Store all URLs m_lastUserUrl = m_objects.network->lastUserUrl(); m_published += data.count(); // Unlock mutex after copying the request object, then emit it with an xxxReady() signal switch ( parseMode ) { case ParseForDepartures: { Q_ASSERT(dynamic_cast(_request)); const DepartureRequest departureRequest = *dynamic_cast(_request); m_mutex->unlock(); emit departuresReady( data, features, hints, lastUserUrl, globalInfo, departureRequest, couldNeedForcedUpdate ); break; } case ParseForArrivals: { Q_ASSERT(dynamic_cast(_request)); const ArrivalRequest arrivalRequest = *dynamic_cast(_request); m_mutex->unlock(); emit arrivalsReady( data, features, hints, lastUserUrl, globalInfo, arrivalRequest, couldNeedForcedUpdate ); break; } case ParseForJourneysByDepartureTime: case ParseForJourneysByArrivalTime: { Q_ASSERT(dynamic_cast(_request)); const JourneyRequest journeyRequest = *dynamic_cast(_request); m_mutex->unlock(); emit journeysReady( data, features, hints, lastUserUrl, globalInfo, journeyRequest, couldNeedForcedUpdate ); break; } case ParseForStopSuggestions: { Q_ASSERT(dynamic_cast(_request)); const StopSuggestionRequest stopSuggestionRequest = *dynamic_cast< const StopSuggestionRequest* >( _request ); m_mutex->unlock(); emit stopSuggestionsReady( data, features, hints, lastUserUrl, globalInfo, stopSuggestionRequest, couldNeedForcedUpdate ); break; } case ParseForAdditionalData: { if ( data.count() > 1 ) { qWarning() << "The script added more than one result in an additional data request"; kDebug() << "All received additional data for item" << dynamic_cast(_request)->itemNumber() << ':' << data; m_errorString = i18nc("@info/plain", "The script added more than one result in " "an additional data request."); m_success = false; cleanup(); m_mutex->unlock(); return; } // Additional data gets requested per timetable item, only one result expected const TimetableData additionalData = data.first(); if ( additionalData.isEmpty() ) { qWarning() << "Did not find any additional data."; m_errorString = i18nc("@info/plain", "TODO."); // TODO m_success = false; cleanup(); m_mutex->unlock(); return; } Q_ASSERT(dynamic_cast(_request)); const AdditionalDataRequest additionalDataRequest = *dynamic_cast< const AdditionalDataRequest* >( _request ); m_mutex->unlock(); emit additionalDataReady( additionalData, features, hints, lastUserUrl, globalInfo, additionalDataRequest, couldNeedForcedUpdate ); break; } default: m_mutex->unlock(); kDebug() << "Parse mode unsupported:" << parseMode; break; } } } class DepartureJobPrivate { public: DepartureJobPrivate( const DepartureRequest &request ) : request(request) {}; DepartureRequest request; }; DepartureJob::DepartureJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const DepartureRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new DepartureJobPrivate(request)) { } DepartureJob::~DepartureJob() { delete d; } const AbstractRequest *DepartureJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } QString ScriptJob::sourceName() const { QMutexLocker locker( m_mutex ); return request()->sourceName(); } const AbstractRequest *ScriptJob::cloneRequest() const { QMutexLocker locker( m_mutex ); return request()->clone(); } class ArrivalJobPrivate { public: ArrivalJobPrivate( const ArrivalRequest &request ) : request(request) {}; ArrivalRequest request; }; ArrivalJob::ArrivalJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const ArrivalRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new ArrivalJobPrivate(request)) { } ArrivalJob::~ArrivalJob() { delete d; } const AbstractRequest *ArrivalJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class JourneyJobPrivate { public: JourneyJobPrivate( const JourneyRequest &request ) : request(request) {}; JourneyRequest request; }; JourneyJob::JourneyJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const JourneyRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new JourneyJobPrivate(request)) { } JourneyJob::~JourneyJob() { delete d; } const AbstractRequest *JourneyJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class StopSuggestionsJobPrivate { public: StopSuggestionsJobPrivate( const StopSuggestionRequest &request ) : request(request) {}; StopSuggestionRequest request; }; StopSuggestionsJob::StopSuggestionsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopSuggestionRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new StopSuggestionsJobPrivate(request)) { } StopSuggestionsJob::~StopSuggestionsJob() { delete d; } const AbstractRequest *StopSuggestionsJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class StopsByGeoPositionJobPrivate { public: StopsByGeoPositionJobPrivate( const StopsByGeoPositionRequest &request ) : request(request) {}; StopsByGeoPositionRequest request; }; StopsByGeoPositionJob::StopsByGeoPositionJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopsByGeoPositionRequest& request, QObject* parent ) : ScriptJob(data, scriptStorage, parent), d(new StopsByGeoPositionJobPrivate(request)) { } StopsByGeoPositionJob::~StopsByGeoPositionJob() { delete d; } const AbstractRequest *StopsByGeoPositionJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class AdditionalDataJobPrivate { public: AdditionalDataJobPrivate( const AdditionalDataRequest &request ) : request(request) {}; AdditionalDataRequest request; }; AdditionalDataJob::AdditionalDataJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const AdditionalDataRequest &request, QObject *parent ) : ScriptJob(data, scriptStorage, parent), d(new AdditionalDataJobPrivate(request)) { } AdditionalDataJob::~AdditionalDataJob() { delete d; } const AbstractRequest *AdditionalDataJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } class MoreItemsJobPrivate { public: MoreItemsJobPrivate( const MoreItemsRequest &request ) : request(request) {}; MoreItemsRequest request; }; MoreItemsJob::MoreItemsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const MoreItemsRequest &request, QObject *parent ) : ScriptJob(data, scriptStorage, parent), d(new MoreItemsJobPrivate(request)) { } MoreItemsJob::~MoreItemsJob() { delete d; } const AbstractRequest *MoreItemsJob::request() const { QMutexLocker locker( m_mutex ); return &d->request; } bool ScriptJob::success() const { QMutexLocker locker( m_mutex ); return m_success; } int ScriptJob::publishedItems() const { QMutexLocker locker( m_mutex ); return m_published; } QString ScriptJob::errorString() const { QMutexLocker locker( m_mutex ); return m_errorString; } QString ScriptJob::lastDownloadUrl() const { QMutexLocker locker( m_mutex ); return m_lastUrl; } QString ScriptJob::lastUserUrl() const { QMutexLocker locker( m_mutex ); return m_lastUserUrl; } diff --git a/engine/script/script_thread.h b/engine/script/script_thread.h index f4b31c4..5987813 100644 --- a/engine/script/script_thread.h +++ b/engine/script/script_thread.h @@ -1,351 +1,351 @@ /* * 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. */ /** @file * @brief This file contains a thread which executes service provider plugin scripts. * * @author Friedrich Pülz */ #ifndef SCRIPTTHREAD_HEADER #define SCRIPTTHREAD_HEADER // Own includes #include "enums.h" #include "scriptapi.h" #include "scriptobjects.h" #include "serviceproviderdata.h" // KDE includes #include // Qt includes #include // Base class #include class QMutex; class AbstractRequest; class DepartureRequest; class ArrivalRequest; class JourneyRequest; class StopSuggestionRequest; class StopsByGeoPositionRequest; class AdditionalDataRequest; class MoreItemsRequest; class ServiceProviderData; namespace ScriptApi { class Storage; class Network; class Helper; class ResultObject; } class QScriptProgram; class QScriptEngine; class QEventLoop; using namespace ScriptApi; /** @brief Stores information about a departure/arrival/journey/stop suggestion. */ typedef QHash TimetableData; typedef NetworkRequest* NetworkRequestPtr; QScriptValue networkRequestToScript( QScriptEngine *engine, const NetworkRequestPtr &request ); void networkRequestFromScript( const QScriptValue &object, NetworkRequestPtr &request ); bool importExtension( QScriptEngine *engine, const QString &extension ); /** * @brief Script function to include external script files. * * Calls to this function need to be the first statements in the global context of the script file, * otherwise an exception gets thrown. It expects one argument, the name of the file to be included, * without it's path. The file needs to be in the same directory as the main script. * If the file is already included this function does nothing. A list of included files gets stored * in the engine's global object, in the "includedFiles" property, as QStringList. * @see maxIncludeLine() **/ QScriptValue include( QScriptContext *context, QScriptEngine *engine ); /** @brief Get the maximum line number for valid include() calls in @p program. */ quint16 maxIncludeLine( const QString &program ); /** * @brief A QScriptEngineAgent that signals when a script finishes. * * After a function exit the agent waits a little bit and checks if the script is still executing * using QScriptEngineAgent::isEvaluating(). **/ class ScriptAgent : public QObject, public QScriptEngineAgent { Q_OBJECT public: /** @brief Creates a new ScriptAgent instance. */ ScriptAgent( QScriptEngine* engine = 0, QObject *parent = 0 ); /** Overwritten to get noticed when a script might have finished. */ virtual void functionExit( qint64 scriptId, const QScriptValue& returnValue ); signals: /** @brief Emitted, when the script is no longer running */ void scriptFinished(); protected slots: void checkExecution(); }; /** @brief Implements the script function 'importExtension()'. */ bool importExtension( QScriptEngine *engine, const QString &extension ); /** * @brief Executes a script. **/ -class ScriptJob : public ThreadWeaver::Job { +class ScriptJob : public QObject, public ThreadWeaver::Job { Q_OBJECT public: /** * @brief Creates a new ScriptJob. * * @param script The script to executes. * @param data Information about the service provider. * @param scriptStorage The shared Storage object. **/ explicit ScriptJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, QObject* parent = 0 ); /** @brief Destructor. */ virtual ~ScriptJob(); /** @brief Abort the job. */ virtual void requestAbort(); /** @brief Overwritten from ThreadWeaver::Job to return whether or not the job was successful. */ virtual bool success() const; /** @brief Return the number of items which are already published. */ int publishedItems() const; /** @brief Return a string describing the error, if success() returns false. */ QString errorString() const; /** @brief Return the URL of the last finished request. */ QString lastDownloadUrl() const; /** @brief Return an URL for the last finished request that should be shown to users. */ QString lastUserUrl() const; /** @brief Get the data source name associated with this job. */ QString sourceName() const; /** @brief Return a copy of the object containing inforamtion about the request of this job. */ const AbstractRequest *cloneRequest() const; signals: /** @brief Signals ready TimetableData items. */ void departuresReady( const QList &departures, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Signals ready TimetableData items. */ void arrivalsReady( const QList &arrivals, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Signals ready TimetableData items. */ void journeysReady( const QList &journeys, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Signals ready TimetableData items. */ void stopSuggestionsReady( const QList &stops, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const StopSuggestionRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Signals ready additional data for a TimetableData item. */ void additionalDataReady( const TimetableData &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const AdditionalDataRequest &request, bool couldNeedForcedUpdate = false ); protected slots: /** @brief Handle the ResultObject::publish() signal by emitting dataReady(). */ void publish(); protected: /** @brief Perform the job. */ - virtual void run(); + virtual void run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread); /** @brief Return a pointer to the object containing information about the request of this job. */ virtual const AbstractRequest* request() const = 0; /** @brief Load @p script into the engine and insert some objects/functions. */ bool loadScript( const QSharedPointer< QScriptProgram > &script ); bool waitFor( QObject *sender, const char *signal, WaitForType type, int *timeout ); bool hasDataToBePublished() const; void handleError( const QString &errorMessage ); void cleanup(); QScriptEngine *m_engine; QMutex *m_mutex; ScriptData m_data; ScriptObjects m_objects; QEventLoop *m_eventLoop; int m_published; bool m_quit; bool m_success; QString m_errorString; QString m_lastUrl; QString m_lastUserUrl; }; class DepartureJobPrivate; class DepartureJob : public ScriptJob { Q_OBJECT public: explicit DepartureJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const DepartureRequest& request, QObject* parent = 0); virtual ~DepartureJob(); protected: virtual const AbstractRequest *request() const; private: const DepartureJobPrivate *d; }; class ArrivalJobPrivate; class ArrivalJob : public ScriptJob { Q_OBJECT public: explicit ArrivalJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const ArrivalRequest& request, QObject* parent = 0); virtual ~ArrivalJob(); protected: virtual const AbstractRequest* request() const; private: const ArrivalJobPrivate *d; }; class JourneyJobPrivate; class JourneyJob : public ScriptJob { Q_OBJECT public: explicit JourneyJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const JourneyRequest& request, QObject* parent = 0); virtual ~JourneyJob(); protected: virtual const AbstractRequest* request() const; private: const JourneyJobPrivate *d; }; class StopSuggestionsJobPrivate; class StopSuggestionsJob : public ScriptJob { Q_OBJECT public: explicit StopSuggestionsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopSuggestionRequest& request, QObject* parent = 0); virtual ~StopSuggestionsJob(); protected: virtual const AbstractRequest* request() const; private: const StopSuggestionsJobPrivate *d; }; class StopsByGeoPositionJobPrivate; class StopsByGeoPositionJob : public ScriptJob { Q_OBJECT public: explicit StopsByGeoPositionJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const StopsByGeoPositionRequest& request, QObject* parent = 0); virtual ~StopsByGeoPositionJob(); protected: virtual const AbstractRequest* request() const; private: const StopsByGeoPositionJobPrivate *d; }; class AdditionalDataJobPrivate; class AdditionalDataJob : public ScriptJob { Q_OBJECT public: explicit AdditionalDataJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const AdditionalDataRequest& request, QObject* parent = 0); virtual ~AdditionalDataJob(); protected: virtual const AbstractRequest* request() const; private: const AdditionalDataJobPrivate *d; }; class MoreItemsJobPrivate; class MoreItemsJob : public ScriptJob { Q_OBJECT public: explicit MoreItemsJob( const ScriptData &data, const QSharedPointer< Storage > &scriptStorage, const MoreItemsRequest& request, QObject* parent = 0); virtual ~MoreItemsJob(); protected: virtual const AbstractRequest* request() const; private: const MoreItemsJobPrivate *d; }; #endif // Multiple inclusion guard diff --git a/engine/script/serviceproviderscript.cpp b/engine/script/serviceproviderscript.cpp index bcf8540..effd906 100644 --- a/engine/script/serviceproviderscript.cpp +++ b/engine/script/serviceproviderscript.cpp @@ -1,692 +1,695 @@ /* * 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 "serviceproviderscript.h" // Own includes #include "scriptapi.h" #include "script_thread.h" #include "serviceproviderglobal.h" #include "serviceproviderdata.h" #include "serviceprovidertestdata.h" #include "departureinfo.h" #include "request.h" // KDE includes #include #include #include #include #include #include #include // Qt includes #include #include #include #include #include #include #include #include #include #include #include const char *ServiceProviderScript::SCRIPT_FUNCTION_FEATURES = "features"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETTIMETABLE = "getTimetable"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETJOURNEYS = "getJourneys"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETSTOPSUGGESTIONS = "getStopSuggestions"; const char *ServiceProviderScript::SCRIPT_FUNCTION_GETADDITIONALDATA = "getAdditionalData"; ServiceProviderScript::ServiceProviderScript( const ServiceProviderData *data, QObject *parent, const QSharedPointer &cache ) : ServiceProvider(data, parent), m_scriptStorage(QSharedPointer(new Storage(data->id()))) { m_scriptState = WaitingForScriptUsage; m_scriptFeatures = readScriptFeatures( cache.isNull() ? ServiceProviderGlobal::cache() : cache ); qRegisterMetaType< QList >( "QList" ); qRegisterMetaType< TimetableData >( "TimetableData" ); qRegisterMetaType< QList >( "QList" ); qRegisterMetaType< GlobalTimetableInfo >( "GlobalTimetableInfo" ); qRegisterMetaType< ParseDocumentMode >( "ParseDocumentMode" ); qRegisterMetaType< NetworkRequest* >( "NetworkRequest*" ); qRegisterMetaType< NetworkRequest::Ptr >( "NetworkRequest::Ptr" ); qRegisterMetaType< QIODevice* >( "QIODevice*" ); qRegisterMetaType< DataStreamPrototype* >( "DataStreamPrototype*" ); qRegisterMetaType< ResultObject::Features >( "ResultObject::Features" ); // TEST, now done using Q_DECLARE_METATYPE? qRegisterMetaType< ResultObject::Hints >( "ResultObject::Hints" ); } ServiceProviderScript::~ServiceProviderScript() { abortAllRequests(); } void ServiceProviderScript::abortAllRequests() { // Abort all running jobs and wait for them to finish for proper cleanup foreach ( ScriptJob *job, m_runningJobs ) { kDebug() << "Abort job" << job; // Create an event loop to wait for the job to finish, // quit the loop when the done() signal gets emitted by the job QEventLoop loop; connect( job, SIGNAL(done(ThreadWeaver::Job*)), &loop, SLOT(quit()) ); // Disconnect all slots connected to the job and then abort it disconnect( job, 0, this, 0 ); job->requestAbort(); // Wait for the job to get aborted if ( !job->isFinished() ) { // The job is not finished, wait for it, maximally one second QTimer::singleShot( 1000, &loop, SLOT(quit()) ); loop.exec(); } // The job has finished or the timeout was reached if ( !job->isFinished() ) { // The job is still not finished, the timeout was reached before qWarning() << "Job not aborted before timeout, delete it" << job; } job->deleteLater(); } m_runningJobs.clear(); } QStringList ServiceProviderScript::allowedExtensions() { return QStringList() << "kross" << "qt" << "qt.core" << "qt.xml"; } bool ServiceProviderScript::lazyLoadScript() { // Read script QFile scriptFile( m_data->scriptFileName() ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { kDebug() << "Script could not be opened for reading" << m_data->scriptFileName() << scriptFile.errorString(); return false; } const QByteArray ba = scriptFile.readAll(); scriptFile.close(); const QString scriptContents = QString::fromUtf8( ba ); // Initialize the script m_scriptData = ScriptData( m_data, QSharedPointer< QScriptProgram >( new QScriptProgram(scriptContents, m_data->scriptFileName())) ); return true; } bool ServiceProviderScript::runTests( QString *errorMessage ) const { if ( !QFile::exists(m_data->scriptFileName()) ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Script file not found: %1", m_data->scriptFileName()); } return false; } QFile scriptFile( m_data->scriptFileName() ); if ( !scriptFile.open(QIODevice::ReadOnly) ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Could not open script file: %1", m_data->scriptFileName()); } return false; } QTextStream stream( &scriptFile ); const QString program = stream.readAll(); scriptFile.close(); if ( program.isEmpty() ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Script file is empty: %1", m_data->scriptFileName()); } return false; } const QScriptSyntaxCheckResult syntax = QScriptEngine::checkSyntax( program ); if ( syntax.state() != QScriptSyntaxCheckResult::Valid ) { if ( errorMessage ) { *errorMessage = i18nc("@info/plain", "Syntax error in script file, line %1: %2", syntax.errorLineNumber(), syntax.errorMessage().isEmpty() ? i18nc("@info/plain", "Syntax error") : syntax.errorMessage()); } return false; } // No errors found return true; } bool ServiceProviderScript::isTestResultUnchanged( const QString &providerId, const QSharedPointer< KConfig > &cache ) { const KConfigGroup providerGroup = cache->group( providerId ); if ( !providerGroup.hasGroup("script") ) { // Not a scripted provider or modified time not stored yet return true; } // Check if included files have been marked as modified since the cache was last updated const KConfigGroup providerScriptGroup = providerGroup.group( "script" ); const bool includesUpToDate = providerScriptGroup.readEntry( "includesUpToDate", false ); if ( !includesUpToDate ) { // An included file was modified return false; } // Check if the script file was modified since the cache was last updated const QDateTime scriptModifiedTime = providerScriptGroup.readEntry("modifiedTime", QDateTime()); const QString scriptFilePath = providerScriptGroup.readEntry( "scriptFileName", QString() ); const QFileInfo scriptFileInfo( scriptFilePath ); if ( scriptFileInfo.lastModified() != scriptModifiedTime ) { kDebug() << "Script was modified:" << scriptFileInfo.fileName(); return false; } // Check all included files and update "includesUpToDate" fields in using providers if ( checkIncludedFiles(cache, providerId) ) { // An included file of the provider was modified return false; } return true; } bool ServiceProviderScript::isTestResultUnchanged( const QSharedPointer &cache ) const { return isTestResultUnchanged( id(), cache ); } QList ServiceProviderScript::readScriptFeatures( const QSharedPointer &cache ) { // Check if the script file was modified since the cache was last updated KConfigGroup providerGroup = cache->group( m_data->id() ); if ( providerGroup.hasGroup("script") && isTestResultUnchanged(cache) && providerGroup.hasKey("features") ) { // Return feature list stored in the cache bool ok; QStringList featureStrings = providerGroup.readEntry("features", QStringList()); featureStrings.removeOne("(none)"); QList features = ServiceProviderGlobal::featuresFromFeatureStrings( featureStrings, &ok ); if ( ok ) { // The stored feature list only contains valid strings return features; } else { qWarning() << "Invalid feature string stored for provider" << m_data->id(); } } // No actual cached information about the service provider kDebug() << "No up-to-date cache information for service provider" << m_data->id(); QList features; QStringList includedFiles; bool ok = lazyLoadScript(); QString errorMessage; if ( !ok ) { errorMessage = i18nc("@info/plain", "Cannot open script file %1", m_data->scriptFileName()); } else { // Create script engine QScriptEngine engine; foreach ( const QString &import, m_data->scriptExtensions() ) { if ( !importExtension(&engine, import) ) { ok = false; errorMessage = i18nc("@info/plain", "Cannot import script extension %1", import); break; } } if ( ok ) { ScriptObjects objects; objects.createObjects( m_scriptData ); objects.attachToEngine( &engine, m_scriptData ); engine.evaluate( *m_scriptData.program ); QVariantList result; if ( !engine.hasUncaughtException() ) { result = engine.globalObject().property( SCRIPT_FUNCTION_FEATURES ).call().toVariant().toList(); } if ( engine.hasUncaughtException() ) { kDebug() << "Error in the script" << engine.uncaughtExceptionLineNumber() << engine.uncaughtException().toString(); kDebug() << "Backtrace:" << engine.uncaughtExceptionBacktrace().join("\n"); ok = false; errorMessage = i18nc("@info/plain", "Uncaught exception in script " "%1, line %2: %3", QFileInfo(QScriptContextInfo(engine.currentContext()).fileName()).fileName(), engine.uncaughtExceptionLineNumber(), engine.uncaughtException().toString()); } else { includedFiles = engine.globalObject().property( "includedFiles" ) .toVariant().toStringList(); // Test if specific functions exist in the script if ( engine.globalObject().property(SCRIPT_FUNCTION_GETSTOPSUGGESTIONS).isValid() ) { features << Enums::ProvidesStopSuggestions; } if ( engine.globalObject().property(SCRIPT_FUNCTION_GETJOURNEYS).isValid() ) { features << Enums::ProvidesJourneys; } if ( engine.globalObject().property(SCRIPT_FUNCTION_GETADDITIONALDATA).isValid() ) { features << Enums::ProvidesAdditionalData; } // Test if features() script function is available if ( !engine.globalObject().property(SCRIPT_FUNCTION_FEATURES).isValid() ) { kDebug() << "The script has no" << SCRIPT_FUNCTION_FEATURES << "function"; } else { // Use values returned by features() script functions // to get additional features of the service provider foreach ( const QVariant &value, result ) { features << static_cast( value.toInt() ); } } } } } // Update script modified time in cache KConfigGroup scriptGroup = providerGroup.group( "script" ); scriptGroup.writeEntry( "scriptFileName", m_data->scriptFileName() ); scriptGroup.writeEntry( "modifiedTime", QFileInfo(m_data->scriptFileName()).lastModified() ); // Remove provider from cached data for include file(s) no longer used by the provider KConfigGroup globalScriptGroup = cache->group( "script" ); const QStringList globalScriptGroupNames = globalScriptGroup.groupList(); foreach ( const QString &globalScriptGroupName, globalScriptGroupNames ) { if ( !globalScriptGroupName.startsWith(QLatin1String("include_")) ) { continue; } QString includedFile = globalScriptGroupName; includedFile.remove( 0, 8 ); // Remove "include_" from beginning const QFileInfo fileInfo( includedFile ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); if ( usingProviders.contains(m_data->id()) && !includedFiles.contains(includedFile) ) { // This provider is marked as using the include file, but it no longer uses that file usingProviders.removeOne( m_data->id() ); includeFileGroup.writeEntry( "usingProviders", usingProviders ); } } // Check if included file(s) were modified foreach ( const QString &includedFile, includedFiles ) { // Add this provider to the list of providers using the current include file const QFileInfo fileInfo( includedFile ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); if ( !usingProviders.contains(m_data->id()) ) { usingProviders << m_data->id(); includeFileGroup.writeEntry( "usingProviders", usingProviders ); } // Check if the include file was modified checkIncludedFile( cache, fileInfo ); } // Update modified times of included files scriptGroup.writeEntry( "includesUpToDate", true ); // Was just updated // Set error in default cache group if ( !ok ) { ServiceProviderTestData newTestData = ServiceProviderTestData::read( id(), cache ); newTestData.setSubTypeTestStatus( ServiceProviderTestData::Failed, errorMessage ); newTestData.write( id(), cache ); } return features; } bool ServiceProviderScript::checkIncludedFile( const QSharedPointer &cache, const QFileInfo &fileInfo, const QString &providerId ) { // Use a config group in the global script group for each included file. // It stores the last modified time and a list of IDs of providers using the include file KConfigGroup globalScriptGroup = cache->group( "script" ); KConfigGroup includeFileGroup = globalScriptGroup.group( "include_" + fileInfo.filePath() ); const QDateTime lastModified = includeFileGroup.readEntry( "modifiedTime", QDateTime() ); // Update "includesUpToDate" field of using providers, if the include file was modified if ( lastModified != fileInfo.lastModified() ) { // The include file was modified, update all using providers (later). // isTestResultUnchanged() returns false if "includesUpToDate" is false const QStringList usingProviders = includeFileGroup.readEntry( "usingProviders", QStringList() ); foreach ( const QString &usingProvider, usingProviders ) { if ( cache->hasGroup(usingProvider) ) { KConfigGroup usingProviderScriptGroup = cache->group( usingProvider ).group( "script" ); usingProviderScriptGroup.writeEntry( "includesUpToDate", false ); } } includeFileGroup.writeEntry( "modifiedTime", fileInfo.lastModified() ); return providerId.isEmpty() || usingProviders.contains(providerId); } else { return false; } } bool ServiceProviderScript::checkIncludedFiles( const QSharedPointer &cache, const QString &providerId ) { bool modified = false; KConfigGroup globalScriptGroup = cache->group( "script" ); const QStringList globalScriptGroups = globalScriptGroup.groupList(); foreach ( const QString &globalScriptGroup, globalScriptGroups ) { if ( !globalScriptGroup.startsWith(QLatin1String("include_")) ) { continue; } QString includedFile = globalScriptGroup; includedFile.remove( 0, 8 ); // Remove "include_" from beginning const QFileInfo fileInfo( includedFile ); if ( checkIncludedFile(cache, fileInfo, providerId) ) { // The include file was modified and is used by this provider modified = true; } } return modified; } QList ServiceProviderScript::features() const { return m_scriptFeatures; } void ServiceProviderScript::departuresReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool couldNeedForcedUpdate ) { // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any departures" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the departure document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); DepartureInfoList departures; foreach( const PublicTransportInfoPtr &info, results ) { departures << info.dynamicCast(); } emit departuresReceived( this, url, departures, globalInfo, request ); if ( couldNeedForcedUpdate ) { emit forceUpdate(); } } } void ServiceProviderScript::arrivalsReady( const QList< TimetableData > &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool couldNeedForcedUpdate ) { // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any arrivals" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the arrival document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); ArrivalInfoList arrivals; foreach( const PublicTransportInfoPtr &info, results ) { arrivals << info.dynamicCast(); } emit arrivalsReceived( this, url, arrivals, globalInfo, request ); if ( couldNeedForcedUpdate ) { emit forceUpdate(); } } } void ServiceProviderScript::journeysReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( couldNeedForcedUpdate ); // TODO use hints if ( data.isEmpty() ) { kDebug() << "The script didn't find any journeys" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18n("Error while parsing the journey document."), url, &request ); } else { // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results = (m_publishedData[request.sourceName()] << newResults); JourneyInfoList journeys; foreach( const PublicTransportInfoPtr &info, results ) { journeys << info.dynamicCast(); } emit journeysReceived( this, url, journeys, globalInfo, request ); } } void ServiceProviderScript::stopSuggestionsReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const StopSuggestionRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( couldNeedForcedUpdate ); // TODO use hints kDebug() << "Received" << data.count() << "items"; // Create PublicTransportInfo objects for new data and combine with already published data PublicTransportInfoList newResults; ResultObject::dataList( data, &newResults, request.parseMode(), m_data->defaultVehicleType(), &globalInfo, features, hints ); PublicTransportInfoList results( m_publishedData[request.sourceName()] << newResults ); kDebug() << "Results:" << results; StopInfoList stops; foreach( const PublicTransportInfoPtr &info, results ) { stops << info.dynamicCast(); } emit stopsReceived( this, url, stops, request ); } void ServiceProviderScript::additionalDataReady( const TimetableData &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const AdditionalDataRequest &request, bool couldNeedForcedUpdate ) { Q_UNUSED( features ); Q_UNUSED( hints ); Q_UNUSED( globalInfo ); Q_UNUSED( couldNeedForcedUpdate ); if ( data.isEmpty() ) { kDebug() << "The script didn't find any new data" << request.sourceName(); emit requestFailed( this, ErrorParsingFailed, i18nc("@info/plain", "No additional data found."), url, &request ); } else { emit additionalDataReceived( this, url, data, request ); } } -void ServiceProviderScript::jobStarted( ThreadWeaver::Job* job ) +void ServiceProviderScript::jobStarted( ThreadWeaver::JobPointer job ) { - ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); + ScriptJob *scriptJob = static_cast< ScriptJob* >( job.data() ); Q_ASSERT( scriptJob ); // Warn if there is published data for the request, // but not for additional data requests because they point to existing departure data sources. // There may be multiple AdditionalDataJob requests for the same data source but different // timetable items in the source. const QString sourceName = scriptJob->sourceName(); - if ( !qobject_cast(scriptJob) && + if ( !static_cast(scriptJob) && m_publishedData.contains(sourceName) && !m_publishedData[sourceName].isEmpty() ) { qWarning() << "Data source already exists for job" << scriptJob << sourceName; } } -void ServiceProviderScript::jobDone( ThreadWeaver::Job* job ) +void ServiceProviderScript::jobDone( ThreadWeaver::JobPointer job ) { - ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); + ScriptJob *scriptJob = static_cast< ScriptJob* >( job.data() ); Q_ASSERT( scriptJob ); m_publishedData.remove( scriptJob->sourceName() ); m_runningJobs.removeOne( scriptJob ); scriptJob->deleteLater(); } -void ServiceProviderScript::jobFailed( ThreadWeaver::Job* job ) +void ServiceProviderScript::jobFailed( ThreadWeaver::JobPointer job ) { - ScriptJob *scriptJob = qobject_cast< ScriptJob* >( job ); + ScriptJob *scriptJob = static_cast< ScriptJob* >( job.data() ); Q_ASSERT( scriptJob ); emit requestFailed( this, ErrorParsingFailed, scriptJob->errorString(), scriptJob->lastDownloadUrl(), scriptJob->cloneRequest() ); } void ServiceProviderScript::requestDepartures( const DepartureRequest &request ) { if ( lazyLoadScript() ) { DepartureJob *job = new DepartureJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)), this, SLOT(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestArrivals( const ArrivalRequest &request ) { if ( lazyLoadScript() ) { ArrivalJob *job = new ArrivalJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)), this, SLOT(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestJourneys( const JourneyRequest &request ) { if ( lazyLoadScript() ) { JourneyJob *job = new JourneyJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)), this, SLOT(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestStopSuggestions( const StopSuggestionRequest &request ) { if ( lazyLoadScript() ) { StopSuggestionsJob *job = new StopSuggestionsJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)), this, SLOT(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ) { if ( lazyLoadScript() ) { StopsByGeoPositionJob *job = new StopsByGeoPositionJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)), this, SLOT(stopSuggestionsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,StopSuggestionRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestAdditionalData( const AdditionalDataRequest &request ) { if ( lazyLoadScript() ) { AdditionalDataJob *job = new AdditionalDataJob( m_scriptData, m_scriptStorage, request, this ); connect( job, SIGNAL(additionalDataReady(TimetableData,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,AdditionalDataRequest,bool)), this, SLOT(additionalDataReady(TimetableData,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,AdditionalDataRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::requestMoreItems( const MoreItemsRequest &moreItemsRequest ) { if ( lazyLoadScript() ) { // Create a MoreItemsJob and connect ready signals for more departures/arrivals/journeys MoreItemsJob *job = new MoreItemsJob( m_scriptData, m_scriptStorage, moreItemsRequest, this ); connect( job, SIGNAL(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)), this, SLOT(departuresReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,DepartureRequest,bool)) ); connect( job, SIGNAL(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)), this, SLOT(arrivalsReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,ArrivalRequest,bool)) ); connect( job, SIGNAL(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)), this, SLOT(journeysReady(QList,ResultObject::Features,ResultObject::Hints,QString,GlobalTimetableInfo,JourneyRequest,bool)) ); enqueue( job ); } } void ServiceProviderScript::enqueue( ScriptJob *job ) { + QVector runningJobs; m_runningJobs << job; - connect( job, SIGNAL(started(ThreadWeaver::Job*)), this, SLOT(jobStarted(ThreadWeaver::Job*)) ); - connect( job, SIGNAL(done(ThreadWeaver::Job*)), this, SLOT(jobDone(ThreadWeaver::Job*)) ); - connect( job, SIGNAL(failed(ThreadWeaver::Job*)), this, SLOT(jobFailed(ThreadWeaver::Job*)) ); - //FIXME: Pass the correct arguments to enqueue() - //ThreadWeaver::Queue::instance()->enqueue(QVector::fromList(m_runningJobs)); + foreach (ScriptJob *scriptJob, m_runningJobs) { + runningJobs.push_back(ThreadWeaver::JobPointer(scriptJob)); + } + connect( job, SIGNAL(started(ThreadWeaver::JobPointer)), this, SLOT(jobStarted(ThreadWeaver::JobPointer)) ); + connect( job, SIGNAL(done(ThreadWeaver::JobPointer)), this, SLOT(jobDone(ThreadWeaver::JobPointer)) ); + connect( job, SIGNAL(failed(ThreadWeaver::JobPointer)), this, SLOT(jobFailed(ThreadWeaver::JobPointer)) ); + ThreadWeaver::Queue::instance()->enqueue( runningJobs ); } void ServiceProviderScript::import( const QString &import, QScriptEngine *engine ) { engine->importExtension( import ); } int ServiceProviderScript::minFetchWait( UpdateFlags updateFlags ) const { // If an update was requested manually wait minimally one minute, // otherwise wait minimally 15 minutes between automatic updates return qMax( updateFlags.testFlag(UpdateWasRequestedManually) ? 60 : 15 * 60, ServiceProvider::minFetchWait() ); } diff --git a/engine/script/serviceproviderscript.h b/engine/script/serviceproviderscript.h index e8c64d0..80f7794 100644 --- a/engine/script/serviceproviderscript.h +++ b/engine/script/serviceproviderscript.h @@ -1,274 +1,276 @@ /* * 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. */ /** @file * @brief This file contains the base class for service providers using script files. * * @author Friedrich Pülz */ #ifndef SERVICEPROVIDERSCRIPT_HEADER #define SERVICEPROVIDERSCRIPT_HEADER +#include + // Own includes #include "../serviceprovider.h" // Base class #include "scriptapi.h" #include "scriptobjects.h" class ScriptJob; namespace ScriptApi { class Storage; } namespace ThreadWeaver { class Job; } class QScriptEngine; class QScriptProgram; class QFileInfo; /** @brief Stores information about a departure/arrival/journey/stop suggestion. */ typedef QHash TimetableData; using namespace ScriptApi; /** * @brief The base class for all scripted service providers. * * Scripts are executed in separate threads and can start synchronous/asynchronous network requests. * Scripts are written in QtScript, ie. ECMAScript/JavaScript. See @ref scriptApi for more * information. * * @note Other script languages supported by Kross can be used, the "kross" extension is then * needed. To use eg. Python code in the script, the following code can be used: * @code * var action = Kross.action( "MyPythonScript" ); // Create Kross action * action.addQObject( action, "MyAction" ); // Propagate action to itself * action.setInterpreter( "python" ); // Set the interpreter to use, eg. "python", "ruby" * action.setCode("import MyAction ; print 'This is Python. name=>',MyAction.interpreter()"); * action.trigger(); // Run the script * @endcode */ class ServiceProviderScript : public ServiceProvider { Q_OBJECT public: /** @brief States of the script, used for loading the script only when needed. */ enum ScriptState { WaitingForScriptUsage = 0x00, /**< The script was not loaded, * because it was not needed yet. */ ScriptLoaded = 0x01, /**< The script has been loaded. */ ScriptHasErrors = 0x02 /**< The script has errors. */ }; public: void import( const QString &import, QScriptEngine *engine ); /** @brief The name of the script function to get a list of used TimetableInformation's. */ static const char *SCRIPT_FUNCTION_FEATURES; /** @brief The name of the script function to download and parse departures/arrivals. */ static const char *SCRIPT_FUNCTION_GETTIMETABLE; /** @brief The name of the script function to download and parse journeys. */ static const char *SCRIPT_FUNCTION_GETJOURNEYS; /** @brief The name of the script function to download and parse stop suggestions. */ static const char *SCRIPT_FUNCTION_GETSTOPSUGGESTIONS; /** @brief The name of the script function to download and parse additional timetable data. */ static const char *SCRIPT_FUNCTION_GETADDITIONALDATA; /** @brief Gets a list of extensions that are allowed to be imported by scripts. */ static QStringList allowedExtensions(); /** * @brief Creates a new ServiceProviderScript object with the given information. * * @param data Information about how to download and parse the documents of a service provider. * If this is 0 a default ServiceProviderData instance gets created. * * @note Can be used if you have a custom ServiceProviderData object. **/ ServiceProviderScript( const ServiceProviderData *data = 0, QObject *parent = 0, const QSharedPointer &cache = QSharedPointer(0) ); /** @brief Destructor. */ virtual ~ServiceProviderScript(); /** * @brief Whether or not the cached test result for @p providerId is unchanged. * * This function tests if the script file or included script files have been modified. * @param providerId The provider to check. * @param cache A shared pointer to the cache. * @see runTests() **/ static bool isTestResultUnchanged( const QString &providerId, const QSharedPointer &cache ); /** * @brief Whether or not the cached test result is unchanged. * * This function tests if the script file or included script files have been modified. * @param cache A shared pointer to the cache. * @see runTests() **/ virtual bool isTestResultUnchanged( const QSharedPointer &cache ) const; /** @brief Whether or not the script has been successfully loaded. */ bool isScriptLoaded() const { return m_scriptState == ScriptLoaded; }; /** @brief Whether or not the script has errors. */ bool hasScriptErrors() const { return m_scriptState == ScriptHasErrors; }; /** @brief Gets a list of features that this service provider supports through a script. */ virtual QList features() const; /** @brief Get the number of currently running requests. */ virtual int runningRequests() const { return m_runningJobs.count(); }; /** @brief Abort all currently running requests. */ virtual void abortAllRequests(); /** * @brief Request departures as described in @p request. * When the departures are completely received departuresReceived() gets emitted. **/ virtual void requestDepartures( const DepartureRequest &request ); /** * @brief Request arrivals as described in @p request. * When the arrivals are completely received arrivalsReceived() gets emitted. **/ virtual void requestArrivals( const ArrivalRequest &request ); /** * @brief Request journeys as described in @p request. * When the journeys are completely received journeysReceived() gets emitted. **/ virtual void requestJourneys( const JourneyRequest &request ); /** * @brief Request stop suggestions as described in @p request. * When the stop suggestions are completely received stopsReceived() gets emitted. **/ virtual void requestStopSuggestions( const StopSuggestionRequest &request ); /** * @brief Request stops by geo position as described in @p request. * When the stops are completely received stopsReceived() gets emitted. **/ virtual void requestStopsByGeoPosition( const StopsByGeoPositionRequest &request ); /** * @brief Requests additional data as described in @p request. * When the additional data is completely received additionDataReceived() gets emitted. **/ virtual void requestAdditionalData( const AdditionalDataRequest &request ); /** * @brief Request more items for a data source as described in @p moreItemsRequest. **/ virtual void requestMoreItems( const MoreItemsRequest &moreItemsRequest ); /** * @brief Get the minimum seconds to wait between two data-fetches from the service provider. * * For manual updates the result is minimally one minute, for automatic updates 15 minutes. * @param updateFlags Flags to take into consideration when calculating the result, eg. whether * or not the result gets used for a manual data source update. **/ virtual int minFetchWait( UpdateFlags updateFlags = DefaultUpdateFlags ) const; protected slots: /** @brief Departure @p data is ready, emits departuresReceived(). */ void departuresReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const DepartureRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Arrival @p data is ready, emits arrivalsReceived(). */ void arrivalsReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const ArrivalRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Journey @p data is ready, emits journeysReceived(). */ void journeysReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const JourneyRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Stop suggestion @p data is ready, emits stopsReceived(). */ void stopSuggestionsReady( const QList &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const StopSuggestionRequest &request, bool couldNeedForcedUpdate = false ); /** @brief Additional @p data is ready, emits additionalDataReceived(). */ void additionalDataReady( const TimetableData &data, ResultObject::Features features, ResultObject::Hints hints, const QString &url, const GlobalTimetableInfo &globalInfo, const AdditionalDataRequest &request, bool couldNeedForcedUpdate = false ); /** @brief A @p job was started. */ - void jobStarted( ThreadWeaver::Job *job ); + void jobStarted( ThreadWeaver::JobPointer job ); /** @brief A @p job was done. */ - void jobDone( ThreadWeaver::Job *job ); + void jobDone( ThreadWeaver::JobPointer job ); /** @brief A @p job failed. */ - void jobFailed( ThreadWeaver::Job *job ); + void jobFailed( ThreadWeaver::JobPointer job ); protected: /** @brief Load the script file. */ bool lazyLoadScript(); /** * @brief Get a list of features supported by this provider. * * Uses @p cache to store the result. If the cached result is still valid, ie. the used script * file(s) haven't changed, the feature list from the @p cache gets returned as is. **/ QList readScriptFeatures( const QSharedPointer &cache ); /** @brief Run script provider specific tests. */ virtual bool runTests( QString *errorMessage = 0 ) const; /** @brief Enqueue @p job in the job queue. */ void enqueue( ScriptJob *job ); private: static bool checkIncludedFiles( const QSharedPointer &cache, const QString &providerId = QString() ); static bool checkIncludedFile( const QSharedPointer &cache, const QFileInfo &fileInfo, const QString &providerId = QString() ); ScriptState m_scriptState; // The state of the script QList m_scriptFeatures; // Caches the features the script provides QHash< QString, PublicTransportInfoList > m_publishedData; ScriptData m_scriptData; QSharedPointer< Storage > m_scriptStorage; QList< ScriptJob* > m_runningJobs; }; #endif // Multiple inclusion guard