Движок правил wb-rules 2.0

From Wiren Board
Jump to navigation Jump to search

В обновлённом движке правил wb-rules присутствует ряд важных нововведений, касающихся логики написания сценариев.

Сценарии

Изоляция сценариев

Начиная с версии wb-rules 2.0, каждый файл сценария запускается в своём отдельном пространстве имён — контексте. Таким образом, каждый сценарий может определять свои функции и глобальные переменные без риска изменить поведение других сценариев.

Пример

В качестве примера приведём два сценария, одновременно запускаемых в движке правил. Каждый сценарий определяет глобальные переменные и функции.

В предыдущих версиях wb-rules обращение к глобальной переменной, изменяемой в нескольких файлах сценариев, может привести к неопределённому поведению. В версиях, начиная с 2.0, поведение строго определено и такое же, как будто сценарий единственный в системе.

В комментариях указан вывод команд log для ранних версий и для актуальной версии.

Сценарий 1 (rules1.js)

test1 = 42;

setTimeout(function myFuncOne() {
    log("myFuncOne called");
    log("test1: {}, test2: {}", test1, test2);
    // раньше: test1: [либо 42, либо 84], test2: Hello
    // теперь: test1: 42, test2: (undefined)
    // (будет выведена ошибка выполнения: ReferenceError: identifier 'test2' undefined)
}, 1000);

Сценарий 2 (rules2.js)

test1 = 84;
test2 = "Hello";

setTimeout(function myFuncTwo() {
    log("myFuncTwo called");
    log("test1: {}, test2: {}", test1, test2);
    // раньше: test1: [либо 42, либо 84], test2: Hello
    // теперь: test1: 84, test2: Hello
}, 1000);

Примечание

В предыдущих версиях wb-rules для изоляции правил рекомендовалось использовать замыкание, т.е. оборачивание кода сценария в конструкцию:

(function() {
    // код сценария идёт здесь
})();

Начиная с версии 2.0, в подобной конструкции нет необходимости. Тем не менее, старые сценарии, использующие эту конструкцию, продолжат работу без изменений в поведении.

Обходные пути

Если в вашей системе использовалось общее глобальное пространство для хранения общих данных и функций, есть несколько способов реализации такого поведения:

Использование модулей

Можно написать модуль для организации взаимодействия. У модулей есть статическое хранилище, общее для всех файлов, импортировавших модуль. (см. module.static)

Постоянное хранилище

Для обмена данными также можно использовать глобальные постоянные хранилища (PersistentStorage).

Внимание: при использовании глобальных постоянных хранилищ может произойти совпадение имён, в этом случае возможно нарушение поведения, которое трудно обнаружить.

var ps = new PersistentStorage("my-global-storage", {global: true});

/// ...

ps.myvar = "value"; // это значение доступно для всех пользователей хранилища с именем "my-global-storage"

Прототип глобального объекта

ВНИМАНИЕ: метод считается «грязным», т.к. все переменные и функции, опубликованные таким образом, становятся доступными всем сценариям в системе. Старайтесь избегать этого способа. За неопределённое поведение при использовании этого метода несёт ответственность сам программист.

Глобальные объекты всех сценариев имеют общий объект — прототип, в котором определены стандартные функции wb-rules (такие, как defineRule, setTimeout и т.д.). Через него можно передавать переменные или функции в общую область видимости.

global.__proto__.myVar = 42; // теперь myVar — общая переменная для всех сценариев

// из других сценариев к переменной можно обращаться так
log("shared myVar: {}", myVar);

// или вот так, что чуть более аккуратно, т.к. однозначно показывает, где определена переменная
log("shared myVar: {}", global.__proto__.myVar);

Правило поиска переменной в первом случае будет выглядеть так:

  1. Проверяем, есть ли myVar среди локальных переменных (определённой как var myVar = ...).
  2. Если нет, проверяем, есть ли myVar в глобальном объекте (определённой как myVar = ...).
  3. Если нет, проверяем, есть ли myVar в прототипе глобального объекта (определённой как global.__proto__.myVar).

Поиск останавливается, как только переменная найдена.

Таким образом, первый способ обращения будет работать только в том случае, если myVar не определена в верхних областях видимости.

Анонимные правила

Теперь правила можно объявлять анонимно (без задания специального имени). Это позволит уменьшить путаницу и неочевидное поведение системы при дублировании имён правил в одном скрипте.Уникальные имена для анонимных правил генерируются автоматически. Старый синтаксис (с явным заданием имени правила) продолжит работу без изменений.

ВНИМАНИЕ: начиная с версии 2.0, при объявлении правил с одинаковыми именами в одном файле теперь будет возвращаться ошибка.

Пример

defineRule({
    whenChanged: "mydev/test",
    then: function() {
        log("mydev/test changed");
    }
});

Управление правилами

В wb-rules 2.0 также появляется возможность управлять выполнением правил. Теперь функция defineRule() возвращает идентификатор созданного правила (аналогично setTimeout()/setInterval()), который можно использовать позже для:

  • выключения/включения отработки правила;
  • принудительного запуска тела правила.

По умолчанию, все правила включены.

Пример

var myRule = defineRule({
    whenChanged: "mydev/test",
    then: function() {
        log("mydev/test changed");
    }
});

// ...

disableRule(myRule); // отключить проверку и выполнение правила
enableRule(myRule); // разрешить выполнение правила

runRule(myRule); // принудительно запустить тело правила (функцию then)
// на текущий момент не поддерживается передача аргументов в then

Постоянное хранилище данных

В wb-rules 2.0 добавлена поддержка постоянных хранилищ. По сути, это объекты, значения в которых будут сохраняться даже при потере питания контроллера. Такие хранилища удобно использовать для хранения состояний или конфигурации.

var ps = new PersistentStorage("my-storage");

ps.key = "Hello World";
log(ps.key);

По умолчанию, хранилища создаются локальными для данного файла сценария, с привязкой к имени файла. Таким образом, при создании хранилищ с одинаковыми именами в разных файлах сценариев, создастся два разных хранилища (и сценарий не получит доступа к «чужим» данным).

Однако, есть возможность создавать глобальные хранилища. Для этого нужно добавить аргумент { global: true } в вызов конструктора:

var ps = new PersistentStorage("my-storage", { global: true });

Если такое хранилище уже было создано когда-либо ранее, сценарий получит к нему доступ.

Виртуальные устройства

В предыдущих версиях wb-rules значения контролов виртуальных устройств хранились только в MQTT retained, что не очень надёжно (в случае потери питания данные могли быть легко утеряны). Начиная с версии 2.0, эти значения сохраняются также в специальное хранилище в постоянной памяти и восстанавливаются при загрузке сценария.

Если необходимо каждый раз при перезагрузке скрипта восстанавливать строго определённое значение (т.е. не восстанавливать предыдущее сохранённое), можно добавить в описание контрола поле forceDefault:

defineVirtualDevice("vdev", {
    ...
    cells: {
        ...
        mycell: {
            type: "value",
            value: 10,
            forceDefault: true // при каждой загрузке сценария поле mycell будет получать значение 10
        }
    }
});

По умолчанию поле принимает значение false.

Модули

Начиная с версии 2.0, в движке правил wb-rules появилась поддержка подключаемых JS-модулей (похожая по поведению на аналогичную в Node.js, но с некоторыми особенностями).

Расположение

Поиск модулей происходит по следующим путям (в заданном порядке):

  • /etc/wb-rules-modules
  • /usr/share/wb-rules-modules

Таким образом, пользовательские модули удобно складывать в /etc/wb-rules-modules.

Добавить свои пути можно редактированием /etc/default/wb-rules добавлением путей к переменной WB_RULES_MODULES через разделитель ::

...
WB_RULES_MODULES="/etc/wb-rules-modules:/usr/share/wb-rules-modules"
...

Подключение модуля к сценарию

Подключение модуля происходит с помощью функции require(). Она возвращает объект, экспортированный модулем (exports).

...
var myModule = require("myModule");
...

При этом движок правил будет искать файл myModule.js по очереди в директориях поиска (см. Расположение).

Также допустим поиск файла модуля по поддиректориям в директориях поиска, тогда вызов будет выглядеть так:

...
var myModule = require("path/to/myModule");
...

После того, как файл будет найден, его содержимое будет выполнено, и из файла будет передан объект exports.

Примечание 1: если модуль был подключен в одном сценарии несколько раз (несколько вызовов require("myModule")), содержимое файла модуля будет выполнено только в первый раз, а при повторных вызовах будет возвращаться сохранённый объект exports.

Примечание 2: если модуль подключается в разных сценариях, для каждого сценария будет создан свой объект модуля и заново выполнен весь код модуля. Если модулю требуется использовать данные, общие для всех файлов сценариев, для хранения данных следует использовать объект module.static.

Создание модуля

Для создания модуля достаточно создать файл с именем, соответствующим имени модуля (с расширением .js) в директории /etc/wb-rules-modules.

В этом файле будут доступны все стандартные функции wb-rules, а также набор специальных объектов, с помощью которого можно реализовать необходимый функционал модуля.

Объект exports

С помощью объекта exports можно передавать пользовательскому сценарию параметры и методы.

Пример

Файл модуля /etc/wb-rules-modules/myModule.js

exports.hello = function(text) {
    log("Hello from module, {}", text);
};

exports.answer = 42;

Файл сценария scenario.js

var m = require("myModule");
m.hello("world"); // выведет в лог "Hello from module, world"
log("The answer is {}", m.answer); // выведет в лог "The answer is 42"

Будьте внимательны: объект exports можно только дополнять значениями, но не переопределять. Иначе значения экспортированы не будут!

exports = function(text) {
    log("Hello from module, {}", text);
};

// Ожидание:
var m = require("my-module");
m("world"); // не работает

// На практике m будет пустым объектом.
// Та же проблема произойдёт при использовании такой конструкции:
exports = {
    hello: function(text) {
        log("Hello from module, {}", world);
    },
    answer: 42
};

Объект module

Объект module содержит параметры, относящиеся непосредственно к файлу модуля.

module.filename

Содержит полный путь до файла модуля. Например, для модуля, сохранённого в /etc/wb-rules-modules/myModule.js:

log(module.filename); // выведет /etc/wb-rules-modules/myModule.js

module.static

Объект, хранящий данные, общие для всех экземпляров данного модуля. Его следует использовать для тех данных, которые должны быть доступны сразу во всех сценариях, использующих данный модуль.

Файл /etc/wb-rules-modules/myModule.js

exports.counter = function() {
    if (module.static.count === undefined) {
        module.static.count = 1;
    }
    log("Number of calls: {}", module.static.count);
    module.static.count++;
};

Файл сценария scenario1.js

var m = require("myModule");
m.counter();
m.counter();

Файл сценария scenario2.js

var m = require("myModule");
m.counter();
m.counter();
m.counter();

В результате работы двух скриптов в логе окажется 5 сообщений:

Number of calls: 1
Number of calls: 2
Number of calls: 3
Number of calls: 4
Number of calls: 5

__filename

Переменная __filename берётся из глобального объекта сценария, к которому подключается модуль, и содержит имя файла сценария.

В случае, если модуль подключается в другом модуле, переменная __filename, тем не менее, будет содержать именно имя файла сценария — вершины дерева зависимостей.

Файл /etc/wb-rules-modules/myModule.js

exports.hello = function() {
    log(__filename);
};

Файл сценария /etc/wb-rules/scenario1.js

var m = require("myModule");
m.hello(); // выведет scenario1.js

Замеры производительности

Cм. Замеры производительности