Подтвердить что ты не робот

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 секунд)?

Спасибо заранее!

4b9b3361

Ответ 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.
}