Node.js vm: Как отменить Script.runInNewContext()?
Я хочу использовать модуль vm
как безопасный способ запуска внешнего кода. Он работает очень хорошо, но остается один вопрос:
var UNKNOWN_CODE = "while(true){}";
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(UNKNOWN_CODE);
script.runInNewContext(ctx);
console.log("finished"); //never executed
Есть ли способ отменить выполнение (например, если он длится более 5 секунд)?
Спасибо заранее!
Ответы
Ответ 1
Да, теперь это возможно, потому что я добавил поддержку параметра timeout
в модуль Node vm
. Вы можете просто передать значение тайм-аута в миллисекунду в runInNewContext()
, и оно выдаст исключение, если код не завершит выполнение за указанный промежуток времени.
Обратите внимание: это не подразумевает какой-либо модели безопасности для запуска ненадежного кода. Это просто позволяет вам тайм-аут, который вы доверяете или иным образом защищаете.
var vm = require("vm");
try {
vm.runInNewContext("while(true) {}", {}, "loop", 1000);
} catch (e) {
// Exception thrown after 1000ms
}
console.log("finished"); // Will now be executed
Именно то, что вы ожидаете:
$ time ./node test.js
finished
real 0m1.069s
user 0m1.047s
sys 0m0.017s
Ответ 2
Вам нужно запустить его в отдельном процессе, например:
master.js:
var cluster = require('cluster');
cluster.setupMaster({
exec : "runner.js",
args : process.argv.slice(2),
silent : false
});
//This will be fired when the forked process becomes online
cluster.on( "online", function(worker) {
var timer = 0;
worker.on( "message", function(msg) {
clearTimeout(timer); //The worker responded in under 5 seconds, clear the timeout
console.log(msg);
worker.destroy(); //Don't leave him hanging
});
timer = setTimeout( function() {
worker.destroy(); //Give it 5 seconds to run, then abort it
console.log("worker timed out");
}, 5000);
worker.send( 'while(true){}' ); //Send the code to run for the worker
});
cluster.fork();
runner.js:
//The runner.js is ran in a separate process and just listens for the message which contains code to be executed
process.on('message', function( UNKNOWN_CODE ) {
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(UNKNOWN_CODE);
script.runInNewContext(ctx);
process.send( "finished" ); //Send the finished message to the parent process
});
Чтобы запустить этот пример, поместите эти файлы в одну и ту же папку и директорию и запустите
node master.js
После 5 секунд вы должны увидеть сообщение "рабочее время". Если вы измените его на 'while(false){}'
, рабочий выполнит код немедленно, и вместо него вы должны увидеть "finished"
.
Документы кластера
Ответ 3
Вы можете включить "script breaker" в UNKNOWN_CODE. Что-то вроде:
;setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);
Итак, все будет выглядеть так:
var UNKNOWN_CODE = "while(true){}";
var scriptBreaker = ';setTimeout(function() { throw new Error("Execution time limit reached!") }, 2000);';
var vm = require("vm");
var obj = {};
var ctx = vm.createContext(obj);
var script = vm.createScript(scriptBreaker + UNKNOWN_CODE);
try {
script.runInNewContext(ctx);
console.log("Finished");
}
catch (err) {
console.log("Timeout!");
// Handle Timeout Error...
}
Забастовкa >
UPDATE:
После дополнительных тестов я пришел к выводу, что надежным подходом было бы использование процесса, как указано Esailija. Тем не менее, я делаю это несколько иначе.
В основном приложении у меня есть такой код:
var cp = require('child_process');
function runUnsafeScript(script, callback) {
var worker = cp.fork('./script-runner', [script]);
worker.on('message', function(data) {
worker.kill();
callback(false, data);
});
worker.on('exit', function (code, signal) {
callback(new Error(code), false);
});
worker.on('error', function (err) {
callback(err, false);
});
setTimeout(function killOnTimeOut() {
worker.kill();
callback(new Error("Timeout"), false);
}, 5000);
}
В script -runner.js он выглядит следующим образом:
var vm = require("vm");
var script = vm.createScript( process.argv[2] );
var obj = { sendResult:function (result) { process.send(result); process.exit(0); } };
var context = vm.createContext(obj);
script.runInNewContext(context);
process.on('uncaughtException', function(err) {
process.exit(1);
});
Этот подход позволил достичь следующих целей:
- запустите script в ограниченном контексте
- избегать проблем с мертвыми циклами и исключениями
- запускать много (ограничено аппаратными) небезопасными скриптами одновременно, чтобы они не прерывали друг друга
- передать script результаты выполнения в основное приложение для дальнейшей обработки
Ответ 4
Возможно, вы захотите проверить Threads a Gogo. К сожалению, он еще не обновлен до 0.8.x.
Однако, как упоминалось в @Esailija, существует безопасный запуск внешнего кода, если он не выполняется в другом процессе.
var Threads = require('threads_a_gogo');
var t = Threads.create();
t.eval("while(true) { console.log('.'); }");
setTimeout(function() {
t.destroy();
console.log('finished');
}, 1000);
Ответ 5
В новых версиях Node.js(v0.12
и далее) вы сможете передать параметр тайм-аута на vm.runInNewContext
.
Это еще не стабильная версия node, но если вы хотите использовать последнюю нестабильную версию (v0.11.13
), вы можете передать параметр таймаута следующим образом:
vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});
Теперь, после 1000 миллисекунд, script выдает ошибку тайм-аута. Вы можете поймать его так:
try {
vm.runInNewContext('while(1) {}', {}, {timeout: '1000'});
}
catch(e) {
console.log(e); // Script execution timed out.
}