Выполнение пользовательского кода в nodejs

Мне тут по долгу службы пришлось столкнуться с node.js. Это такой серверный JavaScript, если что. И в его основе лежит V8 — JS-движок от Google, который стоит в Google Chrome.

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

У нас есть сервис с некоторым набором данных, неважно каких. Этот сервис позволяет выполнять пользовательские операции над этими данными (например, валидацию). Т.е. любой пользователь может зайти, написать что-то вроде setValid(data.a == 1) — и код будет выполнен. Понятное дело, что сразу возникает вопрос безопасности. Ведь никто не мешает пользователю подключить библиотеку работы с файловой системой и сделать что-нибудь нехорошее:

var fs = require('fs');
fs.readdir('~', function(err, files) {
  files.forEach(function (file) {
    fs.unlink('~/' + file);
  });
});

Или может вообще не возвращать результат, а загрузить процессор на 100%. Мы ведь ничего не можем гарантировать, когда дело доходит по пользовательского кода. Да ведь там могут быть и просто синтаксические ошибки.

Итак, мое решение.

Во-первых, весь пользовательский код выполняется в отдельном процессе. Этот процесс принудительно завершается по истечении некоторого времени (секунд 10 или около того).

// parent.js
var child = child_process.fork('./child.js');
child.on('message', function(message) {
    // message будет содержать ответ от потомка
});
setTimeout(function () {
    child.kill();
    console.log('Task was killed by timeout');
}, 10000);
child.send(data); // Оправляем параметры для выполнения
// child.js
process.on('message', function (message) {
    // message будет содержать параметры выполнения
    // TODO ????
    process.send(result); // Отправляем результат
});

Во-вторых, я оборачиваю вызов eval в try..catch, хотя да, это очевидно.

В-третьих, eval вызывается в специальной функции с кучей неиспользуемых параметров. Имена этих параметров соответствуют всем доступным глобальным именам и они будут равны undefined в момент работы eval.

var libs = { // Разрешенные библиотеки
    "libxmljs": require("libxmljs") 
};
var l = "";
for (var i in libs) {
    if (libs.hasOwnProperty(i)) {
        if (l) {
            l += ',';
        }
        l += i + '=this.libs.' + i;
    }
}
l = "var " + l + ';';

// Класс ответственный за выполнение кода
var RunnerClass = function (code) {
    this.code = code;
};
RunnerClass.prototype.__proto__ = EventEmitter.prototype;

RunnerClass.prototype.run = function (data) {
    var code = this.code;

    // Объект, который представляет собой контекст this для выполняемого кода. всё, что описано тут, доступно во вложенном коде.
    var context = new EventEmitter();
    context["data"] = data;
    context["libs"] = libs;
    context["finish"] = function () {
        this.emit("finish");
    };
    context.on("finish", function () {
        this.emit("finish", context.result);
        console.log('Result', context.result);        
    }.bind(this));

    // Вот эти параметры и прячут глобальные переменные
    var sandbox = function(Buffer, CodeRunner, EventsEmitter, context, global, exports, i, libs, process, module, require,  __dirname, __filename) {
        try {
            eval(l + '!function(code,l){' + code + '}.call(this)'); // Дополнительно прячем в именах параметров код и подключение библиотек.
        } catch (e) {
            console.log("Code execution error: " + e.message);
        }
    };
    // Вызываем sandbox без параметров, так что все имена становятся равными undefined
    sandbox.call(context); 
};

Понятно или еще комментариев дописать?

Нужно только осторожно следить за тем, какие функции и классы открывать для пользователей. Возможно, я что-то не предусмотрел, поправьте меня в таком случае.

getElementsByClassName в IE

А вы знаете, что в Internet Explorer вплоть до 8-го у document нет метода getElementsByClassName? А следовательно, все ваши верстки, основанные только на классах, будут сильно проигрывать в производительности. Все фреймворки эмулируют метод вызовом getElementsByTagName('*') с дальнейшей ручной фильтрацией.

var els = document.getElementsByTagName("*"), filtered = [], len = els.length, i;
for (i = 0; i < len; ++i) {
    el = els[i];
    el.className.search(/^(.* |)some-class( .*|)$/) !== -1 && filtered.push(el);
}

Ну это код на случай, если вам придется использовать vanilla.js. Обратите внимание, что в цикле нельзя изменять DOM-дерево. Нужно сначала скопировать все подходящие элементы в отдельный массив и только потом их обрабатывать. Все потому, что getElementsByTagName (как и все остальные похожие функции) возвращает NodeList, который динамически изменяется вместе с деревом.

May the Force be with you.

jQuery.Deferred

А помните все те велосипеды, которые приходилось писать для обработки множества асинхронных вызовов? Скажем, у нас есть 10 функций или ajax-запросов. Нужно вызвать их все, подождать пока они отработают, а потом выполнить какой-то общий код. Знакомо, не правда ли? И вот, я случайно обнаружил, что в jQuery подобные вещи уже давно реализованы и их можно использовать. Называется всё это jQuery.Deferred.

Итак, попробуем написать тестовое приложение. Оно запускает 10 таймеров с различным временем. И когда все таймеры сработают, приложение должно вывести какое-нибудь сообщение.

var doAction = function (i) {
    var d = new $.Deferred();
    setTimeout(function () {
        console.log(i + ' done.');
        d.resolve();
    }, Math.random() * 9000 + 1000);
    return d.promise();
};
$(function () {
    var actions = [];
    for (var i = 0; i < 10; ++i) {
        actions.push(doAction(i));
    }
    $.when.apply(this, actions).then(function () {
        console.log('Everything is ok.');
    });
});

У объекта Deferred довольно много всяческих методов, но нам интересно несколько. Метод promise возвращает специальный объект, с помощью которого можно отслеживать состояние выполняемой задачи, методы resolve и reject завершают задачу успешно и не успешно соответственно.

Получив набор объектов promise, мы можем их комбинировать. В нашем случае с помощью $.when создаем новый deferred-объект, который является объединением всех остальных. Ну и с помощью then вешаем общую функцию, которая сработает самой последней.

Ну и напоследок, $.ajax тоже является deferred объектом, поэтому можно написать:

$.when($.ajax(a), $.ajax(b)).then(function () {});

И получить обработчик, который выполнится после завершения всех запросов. Хотя что я вам тут рассказываю, вы ведь всё это знаете и без меня.

Branch prediction

Есть такая вещь в современных и не очень процессорах, называется предсказатель переходов (branch predictor). Он позволяет значительно увеличить скорость работы процессора. Недавно мне Антон прислал ссылку на обсуждение предсказателя переходов на Stack Overflow. И там есть очень хорошее объяснение, обязательно посмотрите.

Так вот, я решил проверить, насколько всё это применимо к JavaScript. Ведь, хотя это и интерпретируемый язык, во все последние браузеры встроен JIT-компилятор. И логично предположить, что производительность будет страдать от плохо предсказываемых переходов. Как оказалось, идея такого исследования не мне первому пришла в голову и на jsPerf нашелся соответствующий тест. Я добавил еще несколько интересных вариантов и готов поделиться результатами.

Сначала о самом тесте. У нас есть 2 одинаковых случайных массива с числами от 0 до 255. Потом один из них мы сортируем. Ну и смотрим, с какой скоростью посчитается сумма всех элементов массива больших или равных 128.

Есть 2 основных способа это сделать: с помощью if и с помощью тернарного оператора.

var sum = 0;
for (var j = 0; j < arraySize; ++j) {
  if (unsorted[j] > 128) {
    sum += unsorted[j];
  }
}
var sum = 0;
for (var j = 0; j < arraySize; ++j) {
  sum += unsorted[j] >=128 ? unsorted[j] : 0;
}

Ну и третий способ — небольшая оптимизация второго:

var sum = 0, item;
for (var j = 0; j < arraySize; ++j) {
  item = unsorted[j];
  sum += item >=128 ? item : 0;
}

Результаты

Что хочется отметить. У Google Chrome действительно хороший JIT-компилятор, и разница в скорости выполнения хорошо заметна. В случае Firefox не всё так гладко. В оригинальном варианте с if разница в скорости в 2 раза, как и в случае Chrome. А вот тернарный оператор почему-то работает значительно медленнее, и разница между отсортированным и неотсортированным массивом не так заметна. Opera вообще всё равно, хотя проход по отсортированному массиву все-таки немного быстрее. И еще для Opera имеет значение, что мы сделали 1 обращение к элементу массива вместо двух. Подозреваю, что такая оптимизация в ее компиляторе не предусмотрена.

Про Internet Explorer ничего не скажу, у меня его нет. Если не трудно, перейдите по ссылке в IE и нажмите Run Tests.

Наследование в JavaScript: скандалы, интриги, расследования

Наследование в JavaScript — одна из самых сложных и запутанных тем. Бо́льшая часть программистов даже не пытается лезть в неё, часть поменьше кое-что слышала и даже пытается что-то делать. И лишь небольшая часть разбирается в этой теме.

Итак, наследование.

var ParentClass = function () {}; // ParentClass constructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
ChildClass.prototype = ParentClass.prototype;
ChildClass.prototype.method2 = function () {};

Почему-то некоторые уверены, что просто скопировав прототип, мы получим правильное наследование. Так вот, пример выше неверен. Если мы скопируем прототип таким образом, то ChildClass будет не просто наследником, а родительским классом, но с другим конструктором. А значит method2, который мы добавили в 5-й сточке, будет так же в прототипе у ParentClass. Это получается потому, что присваивание объектов происходит по ссылке, а значит ChildClass.prototype после 4-й строчки будет ссылаться на тот же объект, что и ParentClass.prototype.

Вариант, увиденный совсем недавно.

var ParentClass = function () {}; // ParentClass constructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
ChildClass.prototype = ParentClass.prototype;
var child = new ChildClass();
child.method2 = function () {};

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

— А давайте поместим расширение класса в конструктор, — скажете вы. — Например, вот так:

var ChildClass = function () { // ChildClass constructor
    this.method2 = function () {};
}; 

Хм. Да, так тоже можно. Но у нас получится по одному экземпляру метода method2 на каждый экземпляр класса, а не один на всех, как в случае прототипа. Поэтому — нет.

Вариант 3.

var ParentClass = function () {}; // ParentClass constructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor
for (var prop in ParentClass.prototype) {
    if (ParentClass.prototype.hasOwnProperty(prop)) {
        ChildClass.prototype[prop] = ParentClass.prototype[prop];
    }
}
ChildClass.prototype.method2 = function () {};

Ну, этот вариант по крайней мере работает. И что самое интересное, работает правильно. Но есть несколько вещей, которые не очень хороши. Если у нас в прототипе родительского класса 100500 полей и методов, то в цикле нам придётся пробежаться по ним всем и скопировать в прототип дочернего класса. А если при этом у нас много дочерних классов, то получится вообще кошмар. В памяти расплодится множество одинаковых объектов, содержащих перечисление всех методов и полей.

И ещё, если мы добавим в самом конце один метод в родительский класс:

ParentClass.prototype.method3 = function () {};

то он попадёт только в него, а не в оба, т.к. связь между классами была потеряна на этапе копирования.

Правильный вариант.

var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor

var tempConstructor = function() {};
tempConstructor.prototype = ParentClass.prototype;
ChildClass.prototype = new tempConstructor();
ChildClass.prototype.constructor = ChildClass;

ChildClass.prototype.method2 = function () {};

Нам нужно скопировать прототип, а самый быстрый способ это сделать — создать экземпляр класса. И даже лучше! В этом случае родительский прототип уйдёт на уровень глубже, а прототип дочернего класса окажется пустым. Плюс, если мы добавим в родительский прототип ещё метод после того, как инициализировали дочерний, то он тоже будет в дочернем классе.

Что же происходит в примере выше? Мы создаём временный класс, такой же, как родительский, но у него пустой конструктор. А потом инициализируем прототип дочернего класса этим временным классом. И последний этап: нужно вернуть на место свойство constructor из дочернего класса.

Всё просто? Наверное так.

И ещё правильный вариант, но не всегда.

var ParentClass = function () {}; // ParentClass contructor
ParentClass.prototype.method1 = function () {};
var ChildClass = function () {}; // ChildClass constructor

ChildClass.prototype.__proto__ = ParentClass.prototype;

ChildClass.prototype.method2 = function () {};

Этот вариант делает всё то же, что и предыдущий, только без создания временного класса. Мы просто берём и записываем прототип родителя в иерархию. Возникает вопрос, а зачем же тогда выбирать более сложный вариант, если есть попроще? Надеюсь, никто не сомневается, что причина выбора — Internet Explorer. Наш любимый IE не поддерживает свойство __proto__, а значит код не будет работать. Но если ваш код никогда не будет запускаться в IE (может, вы под node.js или Phonegap пишете), тогда этот вариант ваш.

Итог.

JavaScript очень гибкий язык, и в нём есть множество способов достичь одинакового результата. Как напишете, так и будет работать. Я просто предложил самый логичный и быстрый способ. Если у вас есть ещё варианты или просто вопросы — милости прошу в комментарии.

UPD: Более подробная версия поста с картинками.

← СтаршеМоложе →