Add scripts api namespace

pull/231/head
Michael Zanetti 2019-11-19 20:03:48 +01:00
parent 6ab6f8a80b
commit 3a9a0a0abc
12 changed files with 488 additions and 7 deletions

View File

@ -52,6 +52,7 @@
#include "devicehandler.h"
#include "actionhandler.h"
#include "ruleshandler.h"
#include "scriptshandler.h"
#include "eventhandler.h"
#include "logginghandler.h"
#include "statehandler.h"

View File

@ -72,10 +72,10 @@ public:
void registerTransportInterface(TransportInterface *interface, bool authenticationRequired);
void unregisterTransportInterface(TransportInterface *interface);
bool registerHandler(JsonHandler *handler) override;
bool registerExperienceHandler(JsonHandler *handler, int majorVersion, int minorVersion) override;
private:
bool registerHandler(JsonHandler *handler);
QHash<QString, JsonHandler *> handlers() const;
void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap &params = QVariantMap(), const QString &deprecationWarning = QString());

View File

@ -0,0 +1,106 @@
#include "scriptshandler.h"
#include "loggingcategories.h"
#include "scriptengine/scriptengine.h"
namespace nymeaserver {
ScriptsHandler::ScriptsHandler(ScriptEngine *scriptEngine, QObject *parent):
JsonHandler(parent),
m_engine(scriptEngine)
{
registerEnum<ScriptEngine::ScriptError>();
registerObject<Script, Scripts>();
QVariantMap params, returns;
QString description;
params.clear(); returns.clear();
description = "Get all script, that is, their names and properties, but no content.";
returns.insert("scripts", objectRef<Scripts>());
registerMethod("GetScripts", description, params, returns);
params.clear(); returns.clear();
description = "Add a script";
params.insert("name", enumValueName(String));
params.insert("content", enumValueName(String));
returns.insert("scriptError", enumRef<ScriptEngine::ScriptError>());
returns.insert("o:script", objectRef<Script>());
returns.insert("o:errors", enumValueName(StringList));
registerMethod("AddScript", description, params, returns);
params.clear(); returns.clear();
description = "Edit a script";
params.insert("id", enumValueName(Uuid));
params.insert("o:name", enumValueName(String));
params.insert("content", enumValueName(String));
returns.insert("scriptError", enumRef<ScriptEngine::ScriptError>());
returns.insert("o:errors", enumValueName(StringList));
registerMethod("EditScript", description, params, returns);
params.clear(); returns.clear();
description = "remove a script";
params.insert("id", enumValueName(Uuid));
returns.insert("scriptError", enumRef<ScriptEngine::ScriptError>());
registerMethod("RemoveScript", description, params, returns);
}
QString ScriptsHandler::name() const
{
return "Scripts";
}
JsonReply *ScriptsHandler::GetScripts(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantMap returns;
returns.insert("scripts", pack(m_engine->scripts()));
return createReply(returns);
}
JsonReply* ScriptsHandler::AddScript(const QVariantMap &params)
{
qWarning() << "Script:" << params.value("content").toString();
QVariantMap returns;
ScriptEngine::AddScriptReply scriptReply = m_engine->addScript(params.value("name").toString(), params.value("content").toByteArray());
returns.insert("scriptError", enumValueName(scriptReply.scriptError));
if (scriptReply.scriptError != ScriptEngine::ScriptErrorNoError) {
returns.insert("errors", scriptReply.errors);
} else {
returns.insert("script", pack(scriptReply.script));
}
return createReply(returns);
}
JsonReply *ScriptsHandler::EditScript(const QVariantMap &params)
{
QUuid scriptId = params.value("id").toUuid();
QByteArray content = params.value("content").toByteArray();
QVariantMap returns;
ScriptEngine::EditScriptReply reply = m_engine->editScript(scriptId, content);
returns.insert("scriptError", enumValueName(reply.scriptError));
if (reply.scriptError != ScriptEngine::ScriptErrorNoError) {
returns.insert("errors", reply.errors);
}
return createReply(returns);
}
JsonReply *ScriptsHandler::RemoveScript(const QVariantMap &params)
{
QUuid scriptId = params.value("id").toUuid();
ScriptEngine::ScriptError status = m_engine->removeScript(scriptId);
QVariantMap returns;
returns.insert("scriptError", enumValueName(status));
return createReply(returns);
}
}

View File

@ -0,0 +1,31 @@
#ifndef SCRIPTSHANDLER_H
#define SCRIPTSHANDLER_H
#include "jsonrpc/jsonhandler.h"
#include "scriptengine/scriptengine.h"
namespace nymeaserver {
class ScriptsHandler : public JsonHandler
{
Q_OBJECT
public:
explicit ScriptsHandler(ScriptEngine *scriptEngine, QObject *parent = nullptr);
QString name() const override;
public slots:
JsonReply* GetScripts(const QVariantMap &params);
JsonReply* AddScript(const QVariantMap &params);
JsonReply* EditScript(const QVariantMap &params);
JsonReply* RemoveScript(const QVariantMap &params);
private:
ScriptEngine *m_engine = nullptr;
};
}
#endif // SCRIPTSHANDLER_H

View File

@ -20,11 +20,13 @@ HEADERS += nymeacore.h \
devices/translator.h \
experiences/experiencemanager.h \
jsonrpc/jsonrpcserverimplementation.h \
jsonrpc/scriptshandler.h \
ruleengine/ruleengine.h \
ruleengine/rule.h \
ruleengine/stateevaluator.h \
ruleengine/ruleaction.h \
ruleengine/ruleactionparam.h \
scriptengine/script.h \
scriptengine/scriptaction.h \
scriptengine/scriptengine.h \
scriptengine/scriptevent.h \
@ -98,11 +100,13 @@ SOURCES += nymeacore.cpp \
devices/translator.cpp \
experiences/experiencemanager.cpp \
jsonrpc/jsonrpcserverimplementation.cpp \
jsonrpc/scriptshandler.cpp \
ruleengine/ruleengine.cpp \
ruleengine/rule.cpp \
ruleengine/stateevaluator.cpp \
ruleengine/ruleaction.cpp \
ruleengine/ruleactionparam.cpp \
scriptengine/script.cpp \
scriptengine/scriptaction.cpp \
scriptengine/scriptengine.cpp \
scriptengine/scriptevent.cpp \

View File

@ -98,7 +98,9 @@
#include "tagging/tagsstorage.h"
#include "platform/platform.h"
#include "experiences/experiencemanager.h"
#include "scriptengine/scriptengine.h"
#include "jsonrpc/scriptshandler.h"
#include "devices/devicemanagerimplementation.h"
#include "devices/device.h"
@ -161,7 +163,9 @@ void NymeaCore::init() {
qCDebug(dcApplication) << "Creating Rule Engine";
m_ruleEngine = new RuleEngine(this);
new ScriptEngine(m_deviceManager, this);
qCDebug(dcApplication()) << "Creating Script Engine";
m_scriptEngine = new ScriptEngine(m_deviceManager, this);
m_serverManager->jsonServer()->registerHandler(new ScriptsHandler(m_scriptEngine, m_scriptEngine));
qCDebug(dcApplication()) << "Creating Tags Storage";
m_tagsStorage = new TagsStorage(m_deviceManager, m_ruleEngine, this);

View File

@ -55,6 +55,7 @@ class UserManager;
class Platform;
class System;
class ExperienceManager;
class ScriptEngine;
class NymeaCore : public QObject
{
@ -125,6 +126,7 @@ private:
ServerManager *m_serverManager;
DeviceManagerImplementation *m_deviceManager;
RuleEngine *m_ruleEngine;
ScriptEngine *m_scriptEngine;
LogEngine *m_logger;
TimeManager *m_timeManager;
CloudManager *m_cloudManager;

View File

@ -0,0 +1,51 @@
#include "script.h"
namespace nymeaserver {
Script::Script()
{
}
QUuid Script::id() const
{
return m_id;
}
void Script::setId(const QUuid &id)
{
m_id = id;
}
QString Script::name() const
{
return m_name;
}
void Script::setName(const QString &name)
{
m_name = name;
}
Scripts::Scripts()
{
}
Scripts::Scripts(const QList<Script> &other):
QList<Script>(other)
{
}
QVariant Scripts::get(int index)
{
return QVariant::fromValue(at(index));
}
void Scripts::put(const QVariant &value)
{
append(value.value<Script>());
}
}

View File

@ -0,0 +1,53 @@
#ifndef SCRIPT_H
#define SCRIPT_H
#include <QMetaObject>
#include <QUuid>
#include <QQmlContext>
#include <QQmlComponent>
#include <QObject>
namespace nymeaserver {
class Script
{
Q_GADGET
Q_PROPERTY(QUuid id READ id)
Q_PROPERTY(QString name READ name WRITE setName)
public:
Script();
QUuid id() const;
void setId(const QUuid &id);
QString name() const;
void setName(const QString &name);
QStringList errors;
private:
QUuid m_id;
QString m_name;
friend class ScriptEngine;
QQmlContext *context = nullptr;
QQmlComponent *component = nullptr;
QObject *object = nullptr;
};
class Scripts: public QList<Script>
{
Q_GADGET
Q_PROPERTY(int count READ count)
public:
Scripts();
Scripts(const QList<Script> &other);
Q_INVOKABLE QVariant get(int index);
Q_INVOKABLE void put(const QVariant &value);
};
}
Q_DECLARE_METATYPE(nymeaserver::Script)
Q_DECLARE_METATYPE(nymeaserver::Scripts)
#endif // SCRIPT_H

View File

@ -5,7 +5,13 @@
#include "scriptevent.h"
#include "scriptstate.h"
#include "nymeasettings.h"
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQmlComponent>
#include <QJsonParseError>
#include <QJsonDocument>
#include "loggingcategories.h"
@ -18,18 +24,210 @@ ScriptEngine::ScriptEngine(DeviceManager *deviceManager, QObject *parent) : QObj
qmlRegisterType<ScriptAction>("nymea", 1, 0, "Action");
qmlRegisterType<ScriptState>("nymea", 1, 0, "State");
m_engine = new QQmlApplicationEngine(this);
m_engine->setProperty("deviceManager", reinterpret_cast<quint64>(m_deviceManager));
loadScripts();
}
Scripts ScriptEngine::scripts()
{
Scripts ret;
foreach (Script *script, m_scripts) {
ret.append(*script);
}
return ret;
}
ScriptEngine::AddScriptReply ScriptEngine::addScript(const QString &name, const QByteArray &content)
{
QUuid id = QUuid::createUuid();
QString fileName = baseName(id) + ".qml";
QString jsonFileName = baseName(id) + ".json";
AddScriptReply reply;
QFile jsonFile(jsonFileName);
if (!jsonFile.open(QFile::ReadWrite)) {
qCWarning(dcScriptEngine()) << "Error opening script metadata" << jsonFileName;
reply.scriptError = ScriptErrorHardwareFailure;
return reply;
}
QVariantMap metadata;
metadata.insert("name", name);
jsonFile.write(QJsonDocument::fromVariant(metadata).toJson());
jsonFile.close();
QFile scriptFile(fileName);
if (!scriptFile.open(QFile::WriteOnly)) {
qCWarning(dcScriptEngine()) << "Error opening script file:" << fileName;
reply.scriptError = ScriptErrorHardwareFailure;
return reply;
}
qint64 len = scriptFile.write(content);
if (len != content.length()) {
qCWarning(dcScriptEngine()) << "Error writing script content";
reply.scriptError = ScriptErrorHardwareFailure;
return reply;
}
scriptFile.close();
Script *script = new Script();
script->setId(id);
script->setName(name);
bool loaded = loadScript(script);
if (!loaded) {
reply.scriptError = ScriptErrorInvalidScript;
reply.errors = script->errors;
delete script;
return reply;
}
m_scripts.insert(script->id(), script);
reply.scriptError = ScriptErrorNoError;
reply.script = *m_scripts.value(id);
return reply;
}
ScriptEngine::EditScriptReply ScriptEngine::editScript(const QUuid &id, const QByteArray &content)
{
QString scriptFileName = baseName(id) + ".qml";
QFile scriptFile(scriptFileName);
EditScriptReply reply;
if (!m_scripts.contains(id)) {
qCWarning(dcScriptEngine()) << "No script with id" << id;
reply.scriptError = ScriptErrorScriptNotFound;
return reply;
}
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)) {
qCWarning(dcScriptEngine()) << "Error opening script" << id;
reply.scriptError = ScriptErrorHardwareFailure;
return reply;
}
QByteArray oldContent = scriptFile.readAll();
scriptFile.seek(0);
qint64 bytesWritten = scriptFile.write(content);
if (bytesWritten != content.length()) {
qCWarning(dcScriptEngine()) << "Error writing script content";
reply.scriptError = ScriptErrorHardwareFailure;
return reply;
}
bool loaded = loadScript(script);
if (!loaded) {
reply.scriptError = ScriptErrorInvalidScript;
reply.errors = script->errors;
// Restore old content
scriptFile.seek(0);
scriptFile.write(oldContent);
loadScript(script);
return reply;
}
reply.scriptError = ScriptErrorNoError;
return reply;
}
ScriptEngine::ScriptError ScriptEngine::removeScript(const QUuid &id)
{
Script *script = m_scripts.take(id);
if (!script) {
return ScriptErrorScriptNotFound;
}
unloadScript(script);
QString jsonFileName = baseName(id) + ".json";
QString scriptFileName = baseName(id) + ".qml";
QString compiledScriptFileName = baseName(id) + ".qmlc";
QFile::remove(scriptFileName);
QFile::remove(jsonFileName);
QFile::remove(compiledScriptFileName);
delete script;
return ScriptErrorNoError;
}
void ScriptEngine::loadScripts()
{
QString fileName = "/home/micha/Develop/nymea/tests/script.qml";
QQmlApplicationEngine *engine = new QQmlApplicationEngine(this);
engine->setProperty("deviceManager", reinterpret_cast<quint64>(m_deviceManager));
// QString fileName = "/home/micha/Develop/nymea/tests/script.qml";
// loadScript(fileName);
}
bool ScriptEngine::loadScript(Script *script)
{
QString fileName = baseName(script->id()) + ".qml";
QString jsonFileName = baseName(script->id()) + ".json";
QFile jsonFile(jsonFileName);
if (!jsonFile.open(QFile::ReadOnly)) {
qCWarning(dcScriptEngine()) << "Failed to open script metadata";
return false;
}
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll(), &error);
jsonFile.close();
if (error.error != QJsonParseError::NoError) {
qCWarning(dcScriptEngine()) << "Failed to parse script metadata";
return false;
}
QString name = jsonDoc.toVariant().toMap().value("name").toString();
qCWarning(dcScriptEngine()) << "Loading script";
engine->load(fileName);
script->component = new QQmlComponent(m_engine, QUrl::fromLocalFile(fileName), this);
script->context = new QQmlContext(m_engine, this);
script->object = script->component->create(script->context);
if (!script->object) {
qCWarning(dcScriptEngine()) << "Script failed to load:";
foreach (const QQmlError &error, script->component->errors()) {
qCWarning(dcScriptEngine()) << error.toString();
script->errors.append(QString("%1:%2: %3").arg(error.line()).arg(error.column()).arg(error.description()));
}
delete script->context;
delete script->component;
return false;
}
return true;
}
void ScriptEngine::unloadScript(Script *script)
{
delete script->object;
script->object = nullptr;
delete script->component;
script->component = nullptr;
delete script->context;
script->context = nullptr;
}
QString ScriptEngine::baseName(const QUuid &id)
{
QString path = NymeaSettings::storagePath() + '/';
QString basename = id.toString().remove(QRegExp("[{}]"));
return path + basename;
}
}

View File

@ -6,6 +6,7 @@
#include <QQmlEngine>
#include "devices/devicemanager.h"
#include "script.h"
namespace nymeaserver {
@ -13,15 +14,44 @@ class ScriptEngine : public QObject
{
Q_OBJECT
public:
enum ScriptError {
ScriptErrorNoError,
ScriptErrorScriptNotFound,
ScriptErrorInvalidScript,
ScriptErrorHardwareFailure
};
Q_ENUM(ScriptError)
struct AddScriptReply {
ScriptError scriptError;
QStringList errors;
Script script;
};
struct EditScriptReply {
ScriptError scriptError;
QStringList errors;
};
explicit ScriptEngine(DeviceManager *deviceManager, QObject *parent = nullptr);
signals:
Scripts scripts();
AddScriptReply addScript(const QString &name, const QByteArray &content);
EditScriptReply editScript(const QUuid &id, const QByteArray &content);
ScriptError removeScript(const QUuid &id);
private:
void loadScripts();
bool loadScript(Script *script);
void unloadScript(Script *script);
private:
QString baseName(const QUuid &id);
private:
DeviceManager *m_deviceManager = nullptr;
QQmlEngine *m_engine = nullptr;
QHash<QUuid, Script*> m_scripts;
};
}

View File

@ -29,6 +29,7 @@ public:
explicit JsonRPCServer() = default;
virtual ~JsonRPCServer() = default;
virtual bool registerHandler(JsonHandler *handler) = 0;
virtual bool registerExperienceHandler(JsonHandler *handler, int majorVersion, int minorVersion) = 0;
};