Showing the changes to libraries/script-engine/src/ScriptEngines.cpp (1 of 20 files). Show all
Unified · Split
| @@ -0,0 +1,434 @@ | |||
| 1 | // | ||
| 2 | // Created by Bradley Austin Davis on 2016/01/08 | ||
| 3 | // Copyright 2013-2016 High Fidelity, Inc. | ||
| 4 | // | ||
| 5 | // Distributed under the Apache License, Version 2.0. | ||
| 6 | // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html | ||
| 7 | // | ||
| 8 | |||
| 9 | #include "ScriptEngines.h" | ||
| 10 | |||
| 11 | #include <QtCore/QStandardPaths> | ||
| 12 | #include <QtCore/QCoreApplication> | ||
| 13 | |||
| 14 | #include <SettingHandle.h> | ||
| 15 | #include <UserActivityLogger.h> | ||
| 16 | |||
| 17 | #include "ScriptEngine.h" | ||
| 18 | #include "ScriptEngineLogging.h" | ||
| 19 | |||
| 20 | #define __STR2__(x) #x | ||
| 21 | #define __STR1__(x) __STR2__(x) | ||
| 22 | #define __LOC__ __FILE__ "("__STR1__(__LINE__)") : Warning Msg: " | ||
| 23 | |||
| 24 | #ifndef __APPLE__ | ||
| 25 | static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); | ||
| 26 | #else | ||
| 27 | // Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475 | ||
| 28 | static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).append("/script.js"); | ||
| 29 | #endif | ||
| 30 | |||
| 31 | ScriptsModel& getScriptsModel() { | ||
| 32 | static ScriptsModel scriptsModel; | ||
| 33 | return scriptsModel; | ||
| 34 | } | ||
| 35 | |||
| 36 | ScriptEngines::ScriptEngines() | ||
| 37 | : _scriptsLocationHandle("scriptsLocation", DESKTOP_LOCATION), | ||
| 38 | _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION){ | ||
| 39 | _scriptsModelFilter.setSourceModel(&_scriptsModel); | ||
| 40 | _scriptsModelFilter.sort(0, Qt::AscendingOrder); | ||
| 41 | _scriptsModelFilter.setDynamicSortFilter(true); | ||
| 42 | } | ||
| 43 | |||
| 44 | |||
| 45 | QObject* scriptsModel(); | ||
| 46 | |||
| 47 | void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) { | ||
| 48 | _scriptInitializers.push_back(initializer); | ||
| 49 | } | ||
| 50 | |||
| 51 | void ScriptEngines::addScriptEngine(ScriptEngine* engine) { | ||
| 52 | _allScriptsMutex.lock(); | ||
| 53 | _allKnownScriptEngines.insert(engine); | ||
| 54 | _allScriptsMutex.unlock(); | ||
| 55 | } | ||
| 56 | |||
| 57 | void ScriptEngines::removeScriptEngine(ScriptEngine* engine) { | ||
| 58 | // If we're not already in the middle of stopping all scripts, then we should remove ourselves | ||
| 59 | // from the list of running scripts. We don't do this if we're in the process of stopping all scripts | ||
| 60 | // because that method removes scripts from its list as it iterates them | ||
| 61 | if (!_stoppingAllScripts) { | ||
| 62 | _allScriptsMutex.lock(); | ||
| 63 | _allKnownScriptEngines.remove(engine); | ||
| 64 | _allScriptsMutex.unlock(); | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | void ScriptEngines::shutdownScripting() { | ||
| 69 | _allScriptsMutex.lock(); | ||
| 70 | _stoppingAllScripts = true; | ||
| 71 | ScriptEngine::_stoppingAllScripts = true; | ||
| 72 | qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); | ||
| 73 | |||
| 74 | QMutableSetIterator<ScriptEngine*> i(_allKnownScriptEngines); | ||
| 75 | while (i.hasNext()) { | ||
| 76 | ScriptEngine* scriptEngine = i.next(); | ||
| 77 | QString scriptName = scriptEngine->getFilename(); | ||
| 78 | |||
| 79 | // NOTE: typically all script engines are running. But there's at least one known exception to this, the | ||
| 80 | // "entities sandbox" which is only used to evaluate entities scripts to test their validity before using | ||
| 81 | // them. We don't need to stop scripts that aren't running. | ||
| 82 | if (scriptEngine->isRunning()) { | ||
| 83 | |||
| 84 | // If the script is running, but still evaluating then we need to wait for its evaluation step to | ||
| 85 | // complete. After that we can handle the stop process appropriately | ||
| 86 | if (scriptEngine->evaluatePending()) { | ||
| 87 | while (scriptEngine->evaluatePending()) { | ||
| 88 | |||
| 89 | // This event loop allows any started, but not yet finished evaluate() calls to complete | ||
| 90 | // we need to let these complete so that we can be guaranteed that the script engine isn't | ||
| 91 | // in a partially setup state, which can confuse our shutdown unwinding. | ||
| 92 | QEventLoop loop; | ||
| 93 | QObject::connect(scriptEngine, &ScriptEngine::evaluationFinished, &loop, &QEventLoop::quit); | ||
| 94 | loop.exec(); | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | // We disconnect any script engine signals from the application because we don't want to do any | ||
| 99 | // extra stopScript/loadScript processing that the Application normally does when scripts start | ||
| 100 | // and stop. We can safely short circuit this because we know we're in the "quitting" process | ||
| 101 | scriptEngine->disconnect(this); | ||
| 102 | |||
| 103 | // Calling stop on the script engine will set it's internal _isFinished state to true, and result | ||
| 104 | // in the ScriptEngine gracefully ending it's run() method. | ||
| 105 | scriptEngine->stop(); | ||
| 106 | |||
| 107 | // We need to wait for the engine to be done running before we proceed, because we don't | ||
| 108 | // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing | ||
| 109 | // any application state after we leave this stopAllScripts() method | ||
| 110 | qCDebug(scriptengine) << "waiting on script:" << scriptName; | ||
| 111 | scriptEngine->waitTillDoneRunning(); | ||
| 112 | qCDebug(scriptengine) << "done waiting on script:" << scriptName; | ||
| 113 | |||
| 114 | // If the script is stopped, we can remove it from our set | ||
| 115 | i.remove(); | ||
| 116 | } | ||
| 117 | } | ||
| 118 | _stoppingAllScripts = false; | ||
| 119 | _allScriptsMutex.unlock(); | ||
| 120 | qCDebug(scriptengine) << "DONE Stopping all scripts...."; | ||
| 121 | } | ||
| 122 | |||
| 123 | QVariantList getPublicChildNodes(TreeNodeFolder* parent) { | ||
| 124 | QVariantList result; | ||
| 125 | QList<TreeNodeBase*> treeNodes = getScriptsModel().getFolderNodes(parent); | ||
| 126 | for (int i = 0; i < treeNodes.size(); i++) { | ||
| 127 | TreeNodeBase* node = treeNodes.at(i); | ||
| 128 | if (node->getType() == TREE_NODE_TYPE_FOLDER) { | ||
| 129 | TreeNodeFolder* folder = static_cast<TreeNodeFolder*>(node); | ||
| 130 | QVariantMap resultNode; | ||
| 131 | resultNode.insert("name", node->getName()); | ||
| 132 | resultNode.insert("type", "folder"); | ||
| 133 | resultNode.insert("children", getPublicChildNodes(folder)); | ||
| 134 | result.append(resultNode); | ||
| 135 | continue; | ||
| 136 | } | ||
| 137 | TreeNodeScript* script = static_cast<TreeNodeScript*>(node); | ||
| 138 | if (script->getOrigin() == ScriptOrigin::SCRIPT_ORIGIN_LOCAL) { | ||
| 139 | continue; | ||
| 140 | } | ||
| 141 | QVariantMap resultNode; | ||
| 142 | resultNode.insert("name", node->getName()); | ||
| 143 | resultNode.insert("type", "script"); | ||
| 144 | resultNode.insert("url", script->getFullPath()); | ||
| 145 | result.append(resultNode); | ||
| 146 | } | ||
| 147 | return result; | ||
| 148 | } | ||
| 149 | |||
| 150 | QVariantList ScriptEngines::getPublic() { | ||
| 151 | return getPublicChildNodes(NULL); | ||
| 152 | } | ||
| 153 | |||
| 154 | QVariantList ScriptEngines::getLocal() { | ||
| 155 | QVariantList result; | ||
| 156 | QList<TreeNodeBase*> treeNodes = getScriptsModel().getFolderNodes(NULL); | ||
| 157 | for (int i = 0; i < treeNodes.size(); i++) { | ||
| 158 | TreeNodeBase* node = treeNodes.at(i); | ||
| 159 | if (node->getType() != TREE_NODE_TYPE_SCRIPT) { | ||
| 160 | continue; | ||
| 161 | } | ||
| 162 | TreeNodeScript* script = static_cast<TreeNodeScript*>(node); | ||
| 163 | if (script->getOrigin() != ScriptOrigin::SCRIPT_ORIGIN_LOCAL) { | ||
| 164 | continue; | ||
| 165 | } | ||
| 166 | QVariantMap resultNode; | ||
| 167 | resultNode.insert("name", node->getName()); | ||
| 168 | resultNode.insert("path", script->getFullPath()); | ||
| 169 | result.append(resultNode); | ||
| 170 | } | ||
| 171 | return result; | ||
| 172 | } | ||
| 173 | |||
| 174 | QVariantList ScriptEngines::getRunning() { | ||
| 175 | const int WINDOWS_DRIVE_LETTER_SIZE = 1; | ||
| 176 | QVariantList result; | ||
| 177 | auto runningScripts = getRunningScripts(); | ||
| 178 | foreach(const QString& runningScript, runningScripts) { | ||
| 179 | QUrl runningScriptURL = QUrl(runningScript); | ||
| 180 | if (runningScriptURL.scheme().size() <= WINDOWS_DRIVE_LETTER_SIZE) { | ||
| 181 | runningScriptURL = QUrl::fromLocalFile(runningScriptURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded))); | ||
| 182 | } | ||
| 183 | QVariantMap resultNode; | ||
| 184 | resultNode.insert("name", runningScriptURL.fileName()); | ||
| 185 | resultNode.insert("url", runningScriptURL.toDisplayString(QUrl::FormattingOptions(QUrl::FullyEncoded))); | ||
| 186 | // The path contains the exact path/URL of the script, which also is used in the stopScript function. | ||
| 187 | resultNode.insert("path", runningScript); | ||
| 188 | resultNode.insert("local", runningScriptURL.isLocalFile()); | ||
| 189 | result.append(resultNode); | ||
| 190 | } | ||
| 191 | return result; | ||
| 192 | } | ||
| 193 | |||
| 194 | |||
| 195 | static const QString SETTINGS_KEY = "Settings"; | ||
| 196 | static const QString DEFAULT_SCRIPTS_JS_URL = "http://s3.amazonaws.com/hifi-public/scripts/defaultScripts.js"; | ||
| 197 | |||
| 198 | void ScriptEngines::loadDefaultScripts() { | ||
| 199 | loadScript(DEFAULT_SCRIPTS_JS_URL); | ||
| 200 | } | ||
| 201 | |||
| 202 | void ScriptEngines::loadOneScript(const QString& scriptFilename) { | ||
| 203 | loadScript(scriptFilename); | ||
| 204 | } | ||
| 205 | |||
| 206 | void ScriptEngines::loadScripts() { | ||
| 207 | // check first run... | ||
| 208 | if (_firstRun.get()) { | ||
| 209 | qCDebug(scriptengine) << "This is a first run..."; | ||
| 210 | // clear the scripts, and set out script to our default scripts | ||
| 211 | clearScripts(); | ||
| 212 | loadDefaultScripts(); | ||
| 213 | _firstRun.set(false); | ||
| 214 | return; | ||
| 215 | } | ||
| 216 | |||
| 217 | // loads all saved scripts | ||
| 218 | Settings settings; | ||
| 219 | int size = settings.beginReadArray(SETTINGS_KEY); | ||
| 220 | for (int i = 0; i < size; ++i) { | ||
| 221 | settings.setArrayIndex(i); | ||
| 222 | QString string = settings.value("script").toString(); | ||
| 223 | if (!string.isEmpty()) { | ||
| 224 | loadScript(string); | ||
| 225 | } | ||
| 226 | } | ||
| 227 | settings.endArray(); | ||
| 228 | } | ||
| 229 | |||
| 230 | void ScriptEngines::clearScripts() { | ||
| 231 | // clears all scripts from the settingsSettings settings; | ||
| 232 | Settings settings; | ||
| 233 | settings.beginWriteArray(SETTINGS_KEY); | ||
| 234 | settings.remove(""); | ||
| 235 | } | ||
| 236 | |||
| 237 | void ScriptEngines::saveScripts() { | ||
| 238 | // Saves all currently running user-loaded scripts | ||
| 239 | Settings settings; | ||
| 240 | settings.beginWriteArray(SETTINGS_KEY); | ||
| 241 | settings.remove(""); | ||
| 242 | |||
| 243 | QStringList runningScripts = getRunningScripts(); | ||
| 244 | int i = 0; | ||
| 245 | for (auto it = runningScripts.begin(); it != runningScripts.end(); ++it) { | ||
| 246 | if (getScriptEngine(*it)->isUserLoaded()) { | ||
| 247 | settings.setArrayIndex(i); | ||
| 248 | settings.setValue("script", *it); | ||
| 249 | ++i; | ||
| 250 | } | ||
| 251 | } | ||
| 252 | settings.endArray(); | ||
| 253 | } | ||
| 254 | |||
| 255 | QStringList ScriptEngines::getRunningScripts() { | ||
| 256 | QReadLocker lock(&_scriptEnginesHashLock); | ||
| 257 | return _scriptEnginesHash.keys(); | ||
| 258 | } | ||
| 259 | |||
| 260 | void ScriptEngines::stopAllScripts(bool restart) { | ||
| 261 | QReadLocker lock(&_scriptEnginesHashLock); | ||
| 262 | if (restart) { | ||
| 263 | // Delete all running scripts from cache so that they are re-downloaded when they are restarted | ||
| 264 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 265 | for (QHash<QString, ScriptEngine*>::const_iterator it = _scriptEnginesHash.constBegin(); | ||
| 266 | it != _scriptEnginesHash.constEnd(); it++) { | ||
| 267 | if (!it.value()->isFinished()) { | ||
| 268 | scriptCache->deleteScript(it.key()); | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | // Stop and possibly restart all currently running scripts | ||
| 274 | for (QHash<QString, ScriptEngine*>::const_iterator it = _scriptEnginesHash.constBegin(); | ||
| 275 | it != _scriptEnginesHash.constEnd(); it++) { | ||
| 276 | if (it.value()->isFinished()) { | ||
| 277 | continue; | ||
| 278 | } | ||
| 279 | if (restart && it.value()->isUserLoaded()) { | ||
| 280 | connect(it.value(), &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { | ||
| 281 | reloadScript(scriptName); | ||
| 282 | }); | ||
| 283 | } | ||
| 284 | QMetaObject::invokeMethod(it.value(), "stop"); | ||
| 285 | //it.value()->stop(); | ||
| 286 | qCDebug(scriptengine) << "stopping script..." << it.key(); | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | bool ScriptEngines::stopScript(const QString& scriptHash, bool restart) { | ||
| 291 | bool stoppedScript = false; | ||
| 292 | { | ||
| 293 | QReadLocker lock(&_scriptEnginesHashLock); | ||
| 294 | if (_scriptEnginesHash.contains(scriptHash)) { | ||
| 295 | ScriptEngine* scriptEngine = _scriptEnginesHash[scriptHash]; | ||
| 296 | if (restart) { | ||
| 297 | auto scriptCache = DependencyManager::get<ScriptCache>(); | ||
| 298 | scriptCache->deleteScript(QUrl(scriptHash)); | ||
| 299 | connect(scriptEngine, &ScriptEngine::finished, this, [this](QString scriptName, ScriptEngine* engine) { | ||
| 300 | reloadScript(scriptName); | ||
| 301 | }); | ||
| 302 | } | ||
| 303 | scriptEngine->stop(); | ||
| 304 | stoppedScript = true; | ||
| 305 | qCDebug(scriptengine) << "stopping script..." << scriptHash; | ||
| 306 | } | ||
| 307 | } | ||
| 308 | return stoppedScript; | ||
| 309 | } | ||
| 310 | |||
| 311 | QString ScriptEngines::getScriptsLocation() const { | ||
| 312 | return _scriptsLocationHandle.get(); | ||
| 313 | } | ||
| 314 | |||
| 315 | void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) { | ||
| 316 | _scriptsLocationHandle.set(scriptsLocation); | ||
| 317 | _scriptsModel.updateScriptsLocation(scriptsLocation); | ||
| 318 | } | ||
| 319 | |||
| 320 | void ScriptEngines::reloadAllScripts() { | ||
| 321 | DependencyManager::get<ScriptCache>()->clearCache(); | ||
| 322 | emit scriptsReloading(); | ||
| 323 | stopAllScripts(true); | ||
| 324 | } | ||
| 325 | |||
| 326 | ScriptEngine* ScriptEngines::loadScript(const QString& scriptFilename, bool isUserLoaded, bool loadScriptFromEditor, bool activateMainWindow, bool reload) { | ||
| 327 | if (thread() != QThread::currentThread()) { | ||
| 328 | ScriptEngine* result { nullptr }; | ||
| 329 | QMetaObject::invokeMethod(this, "loadScript", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ScriptEngine*, result), | ||
| 330 | Q_ARG(QString, scriptFilename), | ||
| 331 | Q_ARG(bool, isUserLoaded), | ||
| 332 | Q_ARG(bool, loadScriptFromEditor), | ||
| 333 | Q_ARG(bool, activateMainWindow), | ||
| 334 | Q_ARG(bool, reload)); | ||
| 335 | return result; | ||
| 336 | } | ||
| 337 | QUrl scriptUrl(scriptFilename); | ||
| 338 | auto scriptEngine = getScriptEngine(scriptUrl.toString()); | ||
| 339 | if (scriptEngine) { | ||
| 340 | return scriptEngine; | ||
| 341 | } | ||
| 342 | |||
| 343 | scriptEngine = new ScriptEngine(NO_SCRIPT, "", true); | ||
| 344 | scriptEngine->setUserLoaded(isUserLoaded); | ||
| 345 | |||
| 346 | if (scriptFilename.isNull()) { | ||
| 347 | launchScriptEngine(scriptEngine); | ||
| 348 | } else { | ||
| 349 | // connect to the appropriate signals of this script engine | ||
| 350 | connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &ScriptEngines::onScriptEngineLoaded); | ||
| 351 | connect(scriptEngine, &ScriptEngine::errorLoadingScript, this, &ScriptEngines::onScriptEngineError); | ||
| 352 | |||
| 353 | // get the script engine object to load the script at the designated script URL | ||
| 354 | scriptEngine->loadURL(scriptUrl, reload); | ||
| 355 | } | ||
| 356 | |||
| 357 | return scriptEngine; | ||
| 358 | } | ||
| 359 | |||
| 360 | ScriptEngine* ScriptEngines::getScriptEngine(const QString& scriptHash) { | ||
| 361 | ScriptEngine* result = nullptr; | ||
| 362 | { | ||
| 363 | QReadLocker lock(&_scriptEnginesHashLock); | ||
| 364 | auto it = _scriptEnginesHash.find(scriptHash); | ||
| 365 | if (it != _scriptEnginesHash.end()) { | ||
| 366 | result = it.value(); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | return result; | ||
| 370 | } | ||
| 371 | |||
| 372 | // FIXME - change to new version of ScriptCache loading notification | ||
| 373 | void ScriptEngines::onScriptEngineLoaded(const QString& scriptFilename) { | ||
| 374 | UserActivityLogger::getInstance().loadedScript(scriptFilename); | ||
| 375 | ScriptEngine* scriptEngine = qobject_cast<ScriptEngine*>(sender()); | ||
| 376 | |||
| 377 | launchScriptEngine(scriptEngine); | ||
| 378 | |||
| 379 | { | ||
| 380 | QWriteLocker lock(&_scriptEnginesHashLock); | ||
| 381 | _scriptEnginesHash.insertMulti(scriptFilename, scriptEngine); | ||
| 382 | } | ||
| 383 | emit scriptCountChanged(); | ||
| 384 | } | ||
| 385 | |||
| 386 | void ScriptEngines::launchScriptEngine(ScriptEngine* scriptEngine) { | ||
| 387 | connect(scriptEngine, &ScriptEngine::finished, this, &ScriptEngines::onScriptFinished, Qt::DirectConnection); | ||
| 388 | connect(scriptEngine, &ScriptEngine::loadScript, [&](const QString& scriptName, bool userLoaded) { | ||
| 389 | loadScript(scriptName, userLoaded); | ||
| 390 | }); | ||
| 391 | connect(scriptEngine, &ScriptEngine::reloadScript, [&](const QString& scriptName, bool userLoaded) { | ||
| 392 | loadScript(scriptName, userLoaded, false, false, true); | ||
| 393 | }); | ||
| 394 | |||
| 395 | // register our application services and set it off on its own thread | ||
| 396 | for (auto initializer : _scriptInitializers) { | ||
| 397 | initializer(scriptEngine); | ||
| 398 | } | ||
| 399 | scriptEngine->runInThread(); | ||
| 400 | } | ||
| 401 | |||
| 402 | |||
| 403 | void ScriptEngines::onScriptFinished(const QString& scriptName, ScriptEngine* engine) { | ||
| 404 | bool removed = false; | ||
| 405 | { | ||
| 406 | QWriteLocker lock(&_scriptEnginesHashLock); | ||
| 407 | const QString& scriptURLString = QUrl(scriptName).toString(); | ||
| 408 | for (auto it = _scriptEnginesHash.find(scriptURLString); it != _scriptEnginesHash.end(); ++it) { | ||
| 409 | if (it.value() == engine) { | ||
| 410 | _scriptEnginesHash.erase(it); | ||
| 411 | removed = true; | ||
| 412 | break; | ||
| 413 | } | ||
| 414 | } | ||
| 415 | } | ||
| 416 | |||
| 417 | if (removed) { | ||
| 418 | emit scriptCountChanged(); | ||
| 419 | } | ||
| 420 | } | ||
| 421 | |||
| 422 | // FIXME - change to new version of ScriptCache loading notification | ||
| 423 | void ScriptEngines::onScriptEngineError(const QString& scriptFilename) { | ||
| 424 | qCDebug(scriptengine) << "Application::loadScript(), script failed to load..."; | ||
| 425 | emit scriptLoadError(scriptFilename, ""); | ||
| 426 | } | ||
| 427 | |||
| 428 | QString ScriptEngines::getPreviousScriptLocation() const { | ||
| 429 | return _previousScriptLocation.get(); | ||
| 430 | } | ||
| 431 | |||
| 432 | void ScriptEngines::setPreviousScriptLocation(const QString& previousScriptLocation) { | ||
| 433 | _previousScriptLocation.set(previousScriptLocation); | ||
| 434 | } | ||