More work on scripting
This commit is contained in:
parent
3a9a0a0abc
commit
3c274b04ab
@ -11,6 +11,7 @@ ScriptsHandler::ScriptsHandler(ScriptEngine *scriptEngine, QObject *parent):
|
||||
m_engine(scriptEngine)
|
||||
{
|
||||
registerEnum<ScriptEngine::ScriptError>();
|
||||
registerEnum<ScriptEngine::ScriptMessageType>();
|
||||
|
||||
registerObject<Script, Scripts>();
|
||||
|
||||
@ -22,6 +23,13 @@ ScriptsHandler::ScriptsHandler(ScriptEngine *scriptEngine, QObject *parent):
|
||||
returns.insert("scripts", objectRef<Scripts>());
|
||||
registerMethod("GetScripts", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Get a scripts content.";
|
||||
params.insert("id", enumValueName(Uuid));
|
||||
returns.insert("scriptError", enumRef<ScriptEngine::ScriptError>());
|
||||
returns.insert("o:content", enumValueName(String));
|
||||
registerMethod("GetScriptContent", description, params, returns);
|
||||
|
||||
params.clear(); returns.clear();
|
||||
description = "Add a script";
|
||||
params.insert("name", enumValueName(String));
|
||||
@ -46,6 +54,50 @@ ScriptsHandler::ScriptsHandler(ScriptEngine *scriptEngine, QObject *parent):
|
||||
returns.insert("scriptError", enumRef<ScriptEngine::ScriptError>());
|
||||
registerMethod("RemoveScript", description, params, returns);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted when a script has been added to the system.";
|
||||
params.insert("script", objectRef<Script>());
|
||||
registerNotification("ScriptAdded", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted when a script has been removed from the system.";
|
||||
params.insert("id", enumValueName(Uuid));
|
||||
registerNotification("ScriptRemoved", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted when a script has been changed in the system.";
|
||||
params.insert("script", objectRef<Script>());
|
||||
registerNotification("ScriptChanged", description, params);
|
||||
|
||||
params.clear();
|
||||
description = "Emitted when a script produces a console message.";
|
||||
params.insert("scriptId", enumValueName(Uuid));
|
||||
params.insert("type", enumRef<ScriptEngine::ScriptMessageType>());
|
||||
params.insert("message", enumValueName(String));
|
||||
registerNotification("ScriptLogMessage", description, params);
|
||||
|
||||
connect(m_engine, &ScriptEngine::scriptAdded, this, [this](const Script &script){
|
||||
QVariantMap params;
|
||||
params.insert("script", pack(script));
|
||||
emit ScriptAdded(params);
|
||||
});
|
||||
connect(m_engine, &ScriptEngine::scriptRemoved, this, [this](const QUuid &scriptId){
|
||||
QVariantMap params;
|
||||
params.insert("id", scriptId);
|
||||
emit ScriptAdded(params);
|
||||
});
|
||||
connect(m_engine, &ScriptEngine::scriptChanged, this, [this](const Script &script){
|
||||
QVariantMap params;
|
||||
params.insert("script", pack(script));
|
||||
emit ScriptChanged(params);
|
||||
});
|
||||
connect(m_engine, &ScriptEngine::scriptConsoleMessage, this, [this](const QUuid &scriptId, ScriptEngine::ScriptMessageType type, const QString &message){
|
||||
QVariantMap params;
|
||||
params.insert("scriptId", scriptId);
|
||||
params.insert("type", enumValueName(type));
|
||||
params.insert("message", message);
|
||||
emit ScriptLogMessage(params);
|
||||
});
|
||||
}
|
||||
|
||||
QString ScriptsHandler::name() const
|
||||
@ -62,6 +114,18 @@ JsonReply *ScriptsHandler::GetScripts(const QVariantMap ¶ms)
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply *ScriptsHandler::GetScriptContent(const QVariantMap ¶ms)
|
||||
{
|
||||
QUuid scriptId = params.value("id").toUuid();
|
||||
ScriptEngine::GetScriptReply reply = m_engine->scriptContent(scriptId);
|
||||
QVariantMap returns;
|
||||
returns.insert("scriptError", enumValueName(reply.scriptError));
|
||||
if (reply.scriptError == ScriptEngine::ScriptErrorNoError) {
|
||||
returns.insert("content", reply.content);
|
||||
}
|
||||
return createReply(returns);
|
||||
}
|
||||
|
||||
JsonReply* ScriptsHandler::AddScript(const QVariantMap ¶ms)
|
||||
{
|
||||
qWarning() << "Script:" << params.value("content").toString();
|
||||
|
||||
@ -18,10 +18,17 @@ public:
|
||||
|
||||
public slots:
|
||||
JsonReply* GetScripts(const QVariantMap ¶ms);
|
||||
JsonReply* GetScriptContent(const QVariantMap ¶ms);
|
||||
JsonReply* AddScript(const QVariantMap ¶ms);
|
||||
JsonReply* EditScript(const QVariantMap ¶ms);
|
||||
JsonReply* RemoveScript(const QVariantMap ¶ms);
|
||||
|
||||
signals:
|
||||
void ScriptAdded(const QVariantMap ¶ms);
|
||||
void ScriptRemoved(const QVariantMap ¶ms);
|
||||
void ScriptChanged(const QVariantMap ¶ms);
|
||||
void ScriptLogMessage(const QVariantMap ¶ms);
|
||||
|
||||
private:
|
||||
ScriptEngine *m_engine = nullptr;
|
||||
};
|
||||
|
||||
@ -15,20 +15,43 @@
|
||||
|
||||
#include "loggingcategories.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
namespace nymeaserver {
|
||||
|
||||
QtMessageHandler ScriptEngine::s_upstreamMessageHandler;
|
||||
QList<ScriptEngine*> ScriptEngine::s_engines;
|
||||
|
||||
ScriptEngine::ScriptEngine(DeviceManager *deviceManager, QObject *parent) : QObject(parent),
|
||||
m_deviceManager(deviceManager)
|
||||
{
|
||||
qmlRegisterType<ScriptEvent>("nymea", 1, 0, "Event");
|
||||
qmlRegisterType<ScriptAction>("nymea", 1, 0, "Action");
|
||||
qmlRegisterType<ScriptState>("nymea", 1, 0, "State");
|
||||
if (s_engines.isEmpty()) {
|
||||
s_upstreamMessageHandler = qInstallMessageHandler(&logMessageHandler);
|
||||
}
|
||||
s_engines.append(this);
|
||||
|
||||
qmlRegisterType<ScriptEvent>("nymea", 1, 0, "DeviceEvent");
|
||||
qmlRegisterType<ScriptAction>("nymea", 1, 0, "DeviceAction");
|
||||
qmlRegisterType<ScriptState>("nymea", 1, 0, "DeviceState");
|
||||
|
||||
m_engine = new QQmlApplicationEngine(this);
|
||||
m_engine->setProperty("deviceManager", reinterpret_cast<quint64>(m_deviceManager));
|
||||
|
||||
QDir dir;
|
||||
if (!dir.exists(NymeaSettings::storagePath() + "/scripts/")) {
|
||||
dir.mkpath(NymeaSettings::storagePath() + "/scripts/");
|
||||
}
|
||||
|
||||
loadScripts();
|
||||
|
||||
}
|
||||
|
||||
ScriptEngine::~ScriptEngine()
|
||||
{
|
||||
s_engines.removeAll(this);
|
||||
if (s_engines.isEmpty()) {
|
||||
qInstallMessageHandler(s_upstreamMessageHandler);
|
||||
}
|
||||
}
|
||||
|
||||
Scripts ScriptEngine::scripts()
|
||||
@ -40,6 +63,25 @@ Scripts ScriptEngine::scripts()
|
||||
return ret;
|
||||
}
|
||||
|
||||
ScriptEngine::GetScriptReply ScriptEngine::scriptContent(const QUuid &id)
|
||||
{
|
||||
GetScriptReply reply;
|
||||
if (!m_scripts.contains(id)) {
|
||||
reply.scriptError = ScriptErrorScriptNotFound;
|
||||
return reply;
|
||||
}
|
||||
QFile scriptFile(baseName(id) + ".qml");
|
||||
if (!scriptFile.open(QFile::ReadOnly)) {
|
||||
reply.scriptError = ScriptErrorHardwareFailure;
|
||||
return reply;
|
||||
}
|
||||
reply.content = scriptFile.readAll();
|
||||
reply.scriptError = ScriptErrorNoError;
|
||||
|
||||
scriptFile.close();
|
||||
return reply;
|
||||
}
|
||||
|
||||
ScriptEngine::AddScriptReply ScriptEngine::addScript(const QString &name, const QByteArray &content)
|
||||
{
|
||||
QUuid id = QUuid::createUuid();
|
||||
@ -82,6 +124,8 @@ ScriptEngine::AddScriptReply ScriptEngine::addScript(const QString &name, const
|
||||
reply.scriptError = ScriptErrorInvalidScript;
|
||||
reply.errors = script->errors;
|
||||
delete script;
|
||||
QFile::remove(jsonFileName);
|
||||
QFile::remove(fileName);
|
||||
return reply;
|
||||
}
|
||||
|
||||
@ -89,6 +133,9 @@ ScriptEngine::AddScriptReply ScriptEngine::addScript(const QString &name, const
|
||||
|
||||
reply.scriptError = ScriptErrorNoError;
|
||||
reply.script = *m_scripts.value(id);
|
||||
|
||||
emit scriptAdded(reply.script);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
@ -107,20 +154,24 @@ ScriptEngine::EditScriptReply ScriptEngine::editScript(const QUuid &id, const QB
|
||||
Script *script = m_scripts.value(id);
|
||||
unloadScript(script);
|
||||
|
||||
|
||||
// Deleted compiled qml file to make sure we're reloading the new one
|
||||
QString compiledScriptFileName = baseName(id) + ".qmlc";
|
||||
QFile::remove(compiledScriptFileName);
|
||||
|
||||
if (!scriptFile.open(QFile::ReadWrite | QFile::Truncate)) {
|
||||
if (!scriptFile.open(QFile::ReadWrite)) {
|
||||
qCWarning(dcScriptEngine()) << "Error opening script" << id;
|
||||
reply.scriptError = ScriptErrorHardwareFailure;
|
||||
return reply;
|
||||
}
|
||||
|
||||
QByteArray oldContent = scriptFile.readAll();
|
||||
scriptFile.seek(0);
|
||||
scriptFile.close();
|
||||
|
||||
scriptFile.open(QFile::WriteOnly | QFile::Truncate);
|
||||
qint64 bytesWritten = scriptFile.write(content);
|
||||
scriptFile.flush();
|
||||
scriptFile.close();
|
||||
if (bytesWritten != content.length()) {
|
||||
qCWarning(dcScriptEngine()) << "Error writing script content";
|
||||
reply.scriptError = ScriptErrorHardwareFailure;
|
||||
@ -133,14 +184,19 @@ ScriptEngine::EditScriptReply ScriptEngine::editScript(const QUuid &id, const QB
|
||||
reply.errors = script->errors;
|
||||
|
||||
// Restore old content
|
||||
scriptFile.seek(0);
|
||||
scriptFile.open(QFile::WriteOnly | QFile::Truncate);
|
||||
scriptFile.write(oldContent);
|
||||
scriptFile.flush();
|
||||
scriptFile.close();
|
||||
loadScript(script);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
reply.scriptError = ScriptErrorNoError;
|
||||
|
||||
emit scriptChanged(*script);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
@ -161,20 +217,59 @@ ScriptEngine::ScriptError ScriptEngine::removeScript(const QUuid &id)
|
||||
QFile::remove(jsonFileName);
|
||||
QFile::remove(compiledScriptFileName);
|
||||
|
||||
emit scriptRemoved(script->id());
|
||||
|
||||
delete script;
|
||||
return ScriptErrorNoError;
|
||||
}
|
||||
|
||||
void ScriptEngine::loadScripts()
|
||||
{
|
||||
QDir dir(NymeaSettings::storagePath() + "/scripts/");
|
||||
foreach (const QString &entry, dir.entryList({"*json"})) {
|
||||
qCDebug(dcScriptEngine()) << "Have script:" << entry;
|
||||
QFileInfo jsonFileInfo(NymeaSettings::storagePath() + "/scripts/" + entry);
|
||||
QString jsonFileName = jsonFileInfo.absoluteFilePath();
|
||||
QString scriptFileName = jsonFileInfo.absolutePath() + "/" + jsonFileInfo.baseName() + ".qml";
|
||||
if (!QFile::exists(scriptFileName)) {
|
||||
qCWarning(dcScriptEngine()) << "Missing script" << scriptFileName;
|
||||
continue;
|
||||
}
|
||||
|
||||
// QString fileName = "/home/micha/Develop/nymea/tests/script.qml";
|
||||
QFile jsonFile(jsonFileName);
|
||||
if (!jsonFile.open(QFile::ReadOnly)) {
|
||||
qCWarning(dcScriptEngine()) << "Failed to open script metadata" << jsonFileName;
|
||||
continue;
|
||||
}
|
||||
|
||||
// loadScript(fileName);
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll(), &error);
|
||||
jsonFile.close();
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qCWarning(dcScriptEngine()) << "Error parsing script metadata" << jsonFileName;
|
||||
continue;
|
||||
}
|
||||
|
||||
Script *script = new Script();
|
||||
script->setId(jsonFileInfo.baseName());
|
||||
script->setName(jsonDoc.toVariant().toMap().value("name").toString());
|
||||
|
||||
bool loaded = loadScript(script);
|
||||
if (!loaded) {
|
||||
qCWarning(dcScriptEngine()) << "Script failed to load:";
|
||||
delete script;
|
||||
continue;
|
||||
}
|
||||
|
||||
m_scripts.insert(script->id(), script);
|
||||
qCDebug(dcScriptEngine()) << "Script loaded" << scriptFileName;
|
||||
}
|
||||
}
|
||||
|
||||
bool ScriptEngine::loadScript(Script *script)
|
||||
{
|
||||
qCDebug(dcScriptEngine()) << "Loading script" << script->name();
|
||||
|
||||
QString fileName = baseName(script->id()) + ".qml";
|
||||
QString jsonFileName = baseName(script->id()) + ".json";
|
||||
|
||||
@ -196,6 +291,8 @@ bool ScriptEngine::loadScript(Script *script)
|
||||
|
||||
qCWarning(dcScriptEngine()) << "Loading script";
|
||||
|
||||
script->errors.clear();
|
||||
|
||||
script->component = new QQmlComponent(m_engine, QUrl::fromLocalFile(fileName), this);
|
||||
script->context = new QQmlContext(m_engine, this);
|
||||
script->object = script->component->create(script->context);
|
||||
@ -208,6 +305,8 @@ bool ScriptEngine::loadScript(Script *script)
|
||||
}
|
||||
delete script->context;
|
||||
delete script->component;
|
||||
|
||||
m_engine->clearComponentCache();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -215,19 +314,46 @@ bool ScriptEngine::loadScript(Script *script)
|
||||
|
||||
void ScriptEngine::unloadScript(Script *script)
|
||||
{
|
||||
if (!script->object || !script->component || !script->context) {
|
||||
qCWarning(dcScriptEngine()) << "Script seems not to be loaded. Cannot unload.";
|
||||
return;
|
||||
}
|
||||
delete script->object;
|
||||
script->object = nullptr;
|
||||
delete script->component;
|
||||
script->component = nullptr;
|
||||
delete script->context;
|
||||
script->context = nullptr;
|
||||
|
||||
m_engine->clearComponentCache();
|
||||
qCDebug(dcScriptEngine()) << "Unloading script" << script->name();
|
||||
}
|
||||
|
||||
QString ScriptEngine::baseName(const QUuid &id)
|
||||
{
|
||||
QString path = NymeaSettings::storagePath() + '/';
|
||||
QString path = NymeaSettings::storagePath() + "/scripts/";
|
||||
QString basename = id.toString().remove(QRegExp("[{}]"));
|
||||
return path + basename;
|
||||
}
|
||||
|
||||
void ScriptEngine::onScriptMessage(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
QFileInfo fi(context.file);
|
||||
QUuid scriptId = fi.baseName();
|
||||
if (!m_scripts.contains(scriptId)) {
|
||||
return;
|
||||
}
|
||||
emit scriptConsoleMessage(scriptId, type == QtDebugMsg ? ScriptMessageTypeLog : ScriptMessageTypeWarning, message);
|
||||
}
|
||||
|
||||
void ScriptEngine::logMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
if (strcmp(context.category, "qml") == 0) {
|
||||
foreach (ScriptEngine *engine, s_engines) {
|
||||
engine->onScriptMessage(type, context, message);
|
||||
}
|
||||
}
|
||||
s_upstreamMessageHandler(type, context, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -22,6 +22,12 @@ public:
|
||||
};
|
||||
Q_ENUM(ScriptError)
|
||||
|
||||
enum ScriptMessageType {
|
||||
ScriptMessageTypeLog,
|
||||
ScriptMessageTypeWarning
|
||||
};
|
||||
Q_ENUM(ScriptMessageType)
|
||||
|
||||
struct AddScriptReply {
|
||||
ScriptError scriptError;
|
||||
QStringList errors;
|
||||
@ -31,27 +37,44 @@ public:
|
||||
ScriptError scriptError;
|
||||
QStringList errors;
|
||||
};
|
||||
struct GetScriptReply {
|
||||
ScriptError scriptError;
|
||||
QByteArray content;
|
||||
};
|
||||
|
||||
explicit ScriptEngine(DeviceManager *deviceManager, QObject *parent = nullptr);
|
||||
~ScriptEngine();
|
||||
|
||||
Scripts scripts();
|
||||
GetScriptReply scriptContent(const QUuid &id);
|
||||
AddScriptReply addScript(const QString &name, const QByteArray &content);
|
||||
EditScriptReply editScript(const QUuid &id, const QByteArray &content);
|
||||
ScriptError removeScript(const QUuid &id);
|
||||
|
||||
signals:
|
||||
void scriptAdded(const Script &script);
|
||||
void scriptRemoved(const QUuid &id);
|
||||
void scriptChanged(const Script &script);
|
||||
|
||||
void scriptConsoleMessage(const QUuid &scriptId, ScriptMessageType type, const QString &message);
|
||||
|
||||
private:
|
||||
void loadScripts();
|
||||
bool loadScript(Script *script);
|
||||
void unloadScript(Script *script);
|
||||
|
||||
private:
|
||||
QString baseName(const QUuid &id);
|
||||
|
||||
void onScriptMessage(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
private:
|
||||
DeviceManager *m_deviceManager = nullptr;
|
||||
QQmlEngine *m_engine = nullptr;
|
||||
|
||||
QHash<QUuid, Script*> m_scripts;
|
||||
|
||||
static QtMessageHandler s_upstreamMessageHandler;
|
||||
static QList<ScriptEngine*> s_engines;
|
||||
static void logMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user