History

Moving running scripts functionality to scripting library

Brad Davis committed 10 years ago

Commit d28f80a1 parent ddd9b0820 files · +664 −569Browse files

Showing the changes to libraries/script-engine/src/ScriptEngines.cpp (1 of 20 files). Show all

Unified · Split

libraries/script-engine/src/ScriptEngines.cpp+434−0
@@ -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__
25static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
26#else
27// Temporary fix to Qt bug: http://stackoverflow.com/questions/16194475
28static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).append("/script.js");
29#endif
30
31ScriptsModel& getScriptsModel() {
32 static ScriptsModel scriptsModel;
33 return scriptsModel;
34}
35
36ScriptEngines::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
45QObject* scriptsModel();
46
47void ScriptEngines::registerScriptInitializer(ScriptInitializer initializer) {
48 _scriptInitializers.push_back(initializer);
49}
50
51void ScriptEngines::addScriptEngine(ScriptEngine* engine) {
52 _allScriptsMutex.lock();
53 _allKnownScriptEngines.insert(engine);
54 _allScriptsMutex.unlock();
55}
56
57void 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
68void 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
123QVariantList 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
150QVariantList ScriptEngines::getPublic() {
151 return getPublicChildNodes(NULL);
152}
153
154QVariantList 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
174QVariantList 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
195static const QString SETTINGS_KEY = "Settings";
196static const QString DEFAULT_SCRIPTS_JS_URL = "http://s3.amazonaws.com/hifi-public/scripts/defaultScripts.js";
197
198void ScriptEngines::loadDefaultScripts() {
199 loadScript(DEFAULT_SCRIPTS_JS_URL);
200}
201
202void ScriptEngines::loadOneScript(const QString& scriptFilename) {
203 loadScript(scriptFilename);
204}
205
206void 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
230void ScriptEngines::clearScripts() {
231 // clears all scripts from the settingsSettings settings;
232 Settings settings;
233 settings.beginWriteArray(SETTINGS_KEY);
234 settings.remove("");
235}
236
237void 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
255QStringList ScriptEngines::getRunningScripts() {
256 QReadLocker lock(&_scriptEnginesHashLock);
257 return _scriptEnginesHash.keys();
258}
259
260void 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
290bool 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
311QString ScriptEngines::getScriptsLocation() const {
312 return _scriptsLocationHandle.get();
313}
314
315void ScriptEngines::setScriptsLocation(const QString& scriptsLocation) {
316 _scriptsLocationHandle.set(scriptsLocation);
317 _scriptsModel.updateScriptsLocation(scriptsLocation);
318}
319
320void ScriptEngines::reloadAllScripts() {
321 DependencyManager::get<ScriptCache>()->clearCache();
322 emit scriptsReloading();
323 stopAllScripts(true);
324}
325
326ScriptEngine* 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
360ScriptEngine* 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
373void 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
386void 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
403void 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
423void ScriptEngines::onScriptEngineError(const QString& scriptFilename) {
424 qCDebug(scriptengine) << "Application::loadScript(), script failed to load...";
425 emit scriptLoadError(scriptFilename, "");
426}
427
428QString ScriptEngines::getPreviousScriptLocation() const {
429 return _previousScriptLocation.get();
430}
431
432void ScriptEngines::setPreviousScriptLocation(const QString& previousScriptLocation) {
433 _previousScriptLocation.set(previousScriptLocation);
434}