Loading...
Searching...
No Matches
ExecutionHelpers.hpp
1#pragma once
2#include <JS/Qml/QmlObjects.hpp>
3#include <JS/Commands/EditScript.hpp>
4#include <score/command/Dispatchers/CommandDispatcher.hpp>
5#include <score/document/DocumentInterface.hpp>
6#include <Library/LibrarySettings.hpp>
7#include <score/application/ApplicationContext.hpp>
8#include <score/application/GUIApplicationContext.hpp>
9
10#include <ossia/detail/logger.hpp>
11#include <ossia-qt/invoke.hpp>
12#include <ossia-qt/qml_engine_functions.hpp>
13#include <JS/ConsolePanel.hpp>
14
15#include <algorithm>
16#include <vector>
17
18#include <QCryptographicHash>
19#include <QQmlAbstractUrlInterceptor>
20#include <QReadWriteLock>
21#include <QStandardPaths>
22#include <QPointer>
23#include <QDir>
24#include <QFile>
25#include <QQmlComponent>
26#include <QQmlEngine>
27#include <QUrl>
28
29
30namespace JS
31{
32
33inline void connectStateCommit(Script* script, ProcessModel* model)
34{
35 if(!model)
36 return;
37 QObject::connect(
38 script, &Script::commitState, model,
39 [context = QPointer{model}](const QString& key, const QJSValue& value) {
40 auto converted = ossia::qt::value_from_js(value);
41 QMetaObject::invokeMethod(
42 qApp, [context, key, value = std::move(converted)] {
43 if(!context)
44 return;
45 const auto& state = context->state();
46 if(auto it = state.find(key); it != state.end() && it->second == value)
47 return;
48 CommandDispatcher<> dispatcher{
49 score::IDocument::documentContext(*context).commandStack};
50 dispatcher.submit<UpdateStateElement>(*context, key, value);
51 },
52 Qt::QueuedConnection);
53 },
54 Qt::DirectConnection);
55}
56
57
58namespace detail
59{
71inline bool loaderIsCurrent(QQmlEngine& e, const QUrl& url, const QByteArray& str)
72{
73 using Record = std::pair<QPointer<QQmlEngine>, QHash<QUrl, QByteArray>>;
74 thread_local std::vector<Record> known;
75
76 auto it = std::find_if(
77 known.begin(), known.end(), [&](const Record& r) { return r.first == &e; });
78 if(it == known.end())
79 {
80 std::erase_if(known, [](const Record& r) { return r.first.isNull(); });
81 it = known.emplace(known.end(), &e, QHash<QUrl, QByteArray>{});
82 }
83
84 const auto digest = QCryptographicHash::hash(str, QCryptographicHash::Sha1);
85 auto& digests = it->second;
86 const auto known_digest = digests.constFind(url);
87 if(known_digest == digests.cend())
88 {
89 digests.insert(url, digest);
90 return true;
91 }
92 return *known_digest == digest;
93}
94}
95
103inline void loadJSObjectFromUrl(
104 const QUrl& url, const QByteArray& str, QQmlComponent& comp)
105{
106 if(auto* engine = comp.engine(); engine && detail::loaderIsCurrent(*engine, url, str))
107 comp.loadUrl(url);
108 else
109 comp.setData(str, url);
110}
111
113inline QString uiPathOf(const QString& rootPath)
114{
115 QString path = rootPath;
116 if(path.endsWith(".qml"))
117 path.insert(path.size() - 4, ".ui");
118 return path;
119}
120
127inline bool scriptIsItsFile(const QString& path, const QByteArray& str)
128{
129 QFile f{path};
130 return f.open(QIODevice::ReadOnly) && f.readAll().trimmed() == str.trimmed();
131}
132
133namespace detail
134{
138{
139 struct Entry
140 {
141 QString fileName;
142 QString originalDir;
143 };
144 QReadWriteLock lock;
145 QHash<QString, Entry> byDir;
146};
147
148inline StagedScripts& stagedScripts()
149{
150 static StagedScripts s;
151 return s;
152}
153}
154
159inline const QString& editStagingRoot()
160{
161 static const QString root = [] {
162 const auto cache = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
163 return cache.isEmpty() ? QString{} : cache + QStringLiteral("/qml-edits");
164 }();
165 return root;
166}
167
174class EditedScriptRedirect final : public QQmlAbstractUrlInterceptor
175{
176public:
177 QUrl intercept(const QUrl& url, DataType) override
178 {
179 const QString& root = editStagingRoot();
180 if(root.isEmpty() || !url.isLocalFile())
181 return url;
182
183 // Staged folders are the children of one root, so what is asked for is
184 // <root>/<staged folder>/<what the script wants>, however deep that goes.
185 const QString path = url.toLocalFile();
186 if(!path.startsWith(root) || path.size() <= root.size() + 1
187 || path[root.size()] != QLatin1Char('/'))
188 return url;
189
190 const QString rest = path.mid(root.size() + 1);
191 const int slash = rest.indexOf(QLatin1Char('/'));
192 if(slash <= 0)
193 return url;
194
195 const QString wanted = rest.mid(slash + 1);
196
197 auto& s = detail::stagedScripts();
198 QReadLocker _{&s.lock};
199 const auto it = s.byDir.constFind(root + QLatin1Char('/') + rest.left(slash));
200 if(it == s.byDir.cend() || wanted == it->fileName)
201 return url;
202 return QUrl::fromLocalFile(it->originalDir + QLatin1Char('/') + wanted);
203 }
204};
205
206namespace detail
207{
209struct RedirectHolder : QObject
210{
211 EditedScriptRedirect interceptor;
212 QQmlEngine* engine{};
213 ~RedirectHolder() override
214 {
215 if(engine)
216 engine->removeUrlInterceptor(&interceptor);
217 }
218};
219}
220
224inline void installEditRedirect(QQmlEngine& engine)
225{
226 static constexpr auto marker = "score_qml_edit_redirect";
227 if(engine.property(marker).toBool())
228 return;
229
230 auto* holder = new detail::RedirectHolder;
231 holder->engine = &engine;
232 holder->setParent(&engine);
233 engine.addUrlInterceptor(&holder->interceptor);
234 engine.setProperty(marker, true);
235}
236
240inline QString stagingFolderPrefix(const QString& originalPath)
241{
242 const QFileInfo fi{originalPath};
243 QString stem = fi.fileName();
244 for(QChar& c : stem)
245 if(!c.isLetterOrNumber())
246 c = QLatin1Char('_');
247 return stem + QLatin1Char('-')
248 + QString::fromLatin1(QCryptographicHash::hash(
249 fi.absolutePath().toUtf8(), QCryptographicHash::Sha1)
250 .toHex()
251 .left(8))
252 + QLatin1Char('-');
253}
254
257inline void sweepStagedFolders(
258 const QString& root, const QString& prefix, const QString& keep)
259{
260 QDir dir{root};
261 const auto stale = dir.entryList({prefix + "*"}, QDir::Dirs | QDir::NoDotAndDotDot);
262 auto& s = detail::stagedScripts();
263 for(const auto& name : stale)
264 {
265 if(name == keep)
266 continue;
267 const QString path = dir.filePath(name);
268 if(QDir{path}.removeRecursively())
269 {
270 QWriteLocker _{&s.lock};
271 s.byDir.remove(path);
272 }
273 }
274}
275
280inline QString stageEditedScript(const QString& originalPath, const QByteArray& str)
281{
282 const QString root = editStagingRoot();
283 if(root.isEmpty())
284 return {};
285
286 const QFileInfo fi{originalPath};
287 if(fi.fileName().isEmpty())
288 return {};
289
290 // Not the name it had: a folder lends its .qml files to whatever sits in it
291 // as types, and a script staged under its own name would shadow the very
292 // type it is declaring itself to be. A leading dot is not an identifier, so
293 // nothing is lent.
294 const QString name = QStringLiteral(".score-edit")
295 + (originalPath.endsWith(QStringLiteral(".ui.qml"))
296 ? QStringLiteral(".ui.qml")
297 : QStringLiteral(".qml"));
298
299 const QString prefix = stagingFolderPrefix(originalPath);
300 const QString folder
301 = prefix
302 + QString::fromLatin1(
303 QCryptographicHash::hash(str, QCryptographicHash::Sha1).toHex().left(16));
304 const QString dir = root + QLatin1Char('/') + folder;
305 const QString file = dir + QLatin1Char('/') + name;
306
307 if(!QFileInfo::exists(file))
308 {
309 if(!QDir{}.mkpath(dir))
310 return {};
311 QFile f{file};
312 if(!f.open(QIODevice::WriteOnly))
313 return {};
314 const bool written = f.write(str) == str.size();
315 f.close();
316 if(!written)
317 {
318 QDir{dir}.removeRecursively();
319 return {};
320 }
321 }
322
323 {
324 auto& s = detail::stagedScripts();
325 QWriteLocker _{&s.lock};
326 s.byDir.insert(dir, {name, fi.absolutePath()});
327 }
328
329 sweepStagedFolders(root, prefix, folder);
330 return file;
331}
332
333inline void loadJSObjectFromString(
334 const QString& rootPath, const QByteArray& str, QQmlComponent& comp, bool is_ui)
335{
336 const QString path = is_ui ? uiPathOf(rootPath) : rootPath;
337
338 if(scriptIsItsFile(path, str))
339 {
340 loadJSObjectFromUrl(QUrl::fromLocalFile(path), str, comp);
341 return;
342 }
343
344 // An edit is in no file, and only something with a file can be compiled by
345 // url -- which is what lets qml hand back an already compiled form, and what
346 // will let it compile off this thread. So it is given one.
347 if(auto* engine = comp.engine())
348 {
349 if(const QString staged = stageEditedScript(path, str); !staged.isEmpty())
350 {
351 installEditRedirect(*engine);
352 loadJSObjectFromUrl(QUrl::fromLocalFile(staged), str, comp);
353 return;
354 }
355 }
356
357 comp.setData(str, QUrl::fromLocalFile(path));
358}
359
362inline void loadJSObjectFromFile(const QString& path, QQmlComponent& comp)
363{
364 QFile f{path};
365 if(!f.open(QIODevice::ReadOnly))
366 return;
367 loadJSObjectFromUrl(QUrl::fromLocalFile(path), f.readAll(), comp);
368}
369
370inline JS::Script* createJSObject(QQmlComponent& c, QQmlContext* context)
371{
372 const auto& errs = c.errors();
373 if(!errs.empty())
374 {
375 ossia::logger().error(
376 "Uncaught exception at line {} : {}", errs[0].line(),
377 errs[0].toString().toStdString());
378 return nullptr;
379 }
380 else
381 {
382 auto object = c.create(context);
383 auto obj = qobject_cast<JS::Script*>(object);
384 if(obj)
385 return obj;
386 delete object;
387 return nullptr;
388 }
389}
390
391inline JS::Script* createJSObject(
392 const QString& rootPath, const QString& val, QQmlEngine* engine,
393 QQmlContext* context)
394{
395 if(val.trimmed().startsWith("import"))
396 {
397 QQmlComponent c{engine};
398 loadJSObjectFromString(rootPath, val.toUtf8(), c, false);
399 return createJSObject(c, context);
400 }
401 else if(QFile::exists(val))
402 {
403 QQmlComponent c{engine};
404 loadJSObjectFromFile(val, c);
405 return createJSObject(c, context);
406 }
407 return nullptr;
408}
409
410inline void setupExecFuncs(auto* self, QObject* context, ossia::qt::qml_engine_functions* m_execFuncs)
411{
412 QObject::connect(
413 m_execFuncs, &ossia::qt::qml_engine_functions::system, qApp,
414 [](const QString& code) {
415 std::thread{[code] { ::system(code.toStdString().c_str()); }}.detach();
416 }, Qt::QueuedConnection);
417
418 if(auto* js_panel = score::GUIAppContext().findPanel<JS::PanelDelegate>())
419 {
420 QObject::connect(
421 m_execFuncs, &ossia::qt::qml_engine_functions::exec, js_panel,
422 &JS::PanelDelegate::evaluate, Qt::QueuedConnection);
423
424 QObject::connect(
425 m_execFuncs, &ossia::qt::qml_engine_functions::compute, m_execFuncs,
426 [self, context, m_execFuncs, js_panel](const QString& code, const QString& cbname) {
427 // Exec thread
428
429 // Callback ran in UI thread
430 auto cb = [self
431 , context=QPointer{context}
432 , cur = QPointer{self->m_object}
433 , m_execFuncs
434 , cbname] (const QVariant& v) {
435 if(!self)
436 return;
437
438 // Go back to exec thread, we have to go through the normal engine exec ctx
439 ossia::qt::run_async(m_execFuncs, [self, context, cur, v, cbname] {
440 if(!context || !cur)
441 return;
442 if(self->m_object != cur)
443 return;
444
445 auto mo = self->m_object->metaObject();
446 for(int i = 0; i < mo->methodCount(); i++)
447 {
448 if(mo->method(i).name() == cbname)
449 {
450 mo->method(i).invoke(
451 self->m_object, Qt::DirectConnection, QGenericReturnArgument(),
452 QArgument<QVariant>{"v", v});
453 }
454 }
455 });
456 };
457
458 // Go to ui thread
459 ossia::qt::run_async(js_panel, [js_panel, code, cb]() {
460 js_panel->compute(code, cb); // This invokes cb
461 });
462 }, Qt::DirectConnection);
463 }
464}
465}
The CommandDispatcher class.
Definition CommandDispatcher.hpp:13
Definition ExecutionHelpers.hpp:175
Definition ConsolePanel.hpp:40
Definition QmlObjects.hpp:859
@ Record
The whole run becomes one command at stop.
Owns one redirect for the engine it is parented to.
Definition ExecutionHelpers.hpp:210
Definition ExecutionHelpers.hpp:140
QString originalDir
where everything else it asks for lives
Definition ExecutionHelpers.hpp:142
QString fileName
the staged script, the one file that is really there
Definition ExecutionHelpers.hpp:141
Definition ExecutionHelpers.hpp:138