なんでそんなことしたいの?
某サイトをスクレイピングするとき必要だった。
evalがアレなのはさんざん言われてるから隔離したい。
eval() を使わないでください! - JavaScript | MDN
Web Workerを使えばメイン側とワーカー側でグローバルが違うから隔離できるはず。
Denoのデフォルトではワーカー側がメイン側のパーミッションを引き継ぐ。
メイン側で--allow-net
してるとワーカー側でネット通信とかできちゃう。
ので、ワーカー側のパーミッションを無効化して使います。
Denoのスクリプト内で使うんじゃなくて実行結果を見たいだけなら
普通にdeno run sample.ts
かecho "1 + 1" | deno --
でいいと思います。
Denoに権限を与えてないから何もできないはずなので。
とりあえずWorkerを使ってみる
メイン側。
new Worker
のオプションには必ず{type: "module"}
をつける。
const workerPath = new URL("./worker.ts", import.meta.url).href;
const worker = new Worker(workerPath, { type: "module" });
worker.postMessage("あなたが青く見えるなら、");
console.log("main: posted.");
worker.onmessage = ({ data }) => {
console.log("main: received: ", data!);
};
ワーカー側。
イベントハンドラの型を認識させるため/// <reference lib="deno.worker" />
を一行目に書く。
/// <reference lib="deno.worker" />
self.onmessage = ({ data }) => {
console.log(" worker: received: ", data);
self.postMessage("りんごもうさぎの体も青くていいんだよ");
console.log(" worker: posted.");
self.close();
};
実行結果。
worker.ts
がローカルにあるので--allow-read
を指定してる。
外部にあるなら--allow-net
が必要。
% deno run -q --allow-read main.ts
main: posted.
worker: received: あなたが青く見えるなら、
worker: posted.
main: received: りんごもうさぎの体も青くていいんだよ
new Worker
したらメイン側からworker.terminate()
するか、ワーカー側で自らself.close()
しないと動き続ける。
受け渡しするデータは送信側で文字列(JSON)にされて受信側でオブジェクトにされるので同一じゃない。
どこでもドアみたい。怖い。
【閲覧注意】 どこでもドアの秘密
Worker側でDenoの機能を使ってみる
オプションに{deno: {namespace: true}}
をつけるとワーカー側でDeno
が使えるようになる。
実行時に--unstable
が必要になる。
ワーカー側からDeno.readTextFile
で同じフォルダにあるファイルを読んでみる。
const workerPath = new URL("./worker.ts", import.meta.url).href;
const worker = new Worker(workerPath, {
type: "module",
deno: {
namespace: true
},
});
// テキストファイルのパスを絶対パスにする。
// カレントフォルダにあるファイルを読みたいなら相対パスをそのまま渡す。
const textPath = new URL("./msg.txt", import.meta.url).href.replace("file://", "");
worker.postMessage(textPath);
worker.onmessage = ({ data }) => {
console.log(data)
};
/// <reference lib="deno.worker" />
self.onmessage = async ({ data }) => {
const text = await Deno.readTextFile(data);
self.postMessage(text);
self.close();
};
% deno run --allow-read -q --unstable main.ts
「失敗」という概念は一度捨ててみて
そしたら今まで得た技術知識...そしてこの絵が
あなたの味方をしてくれますよ
パーミッションの指定
パーミッションはデフォルトではmain側のものが引き継がれる。("inherit"
)
deno run -A
で全ての権限を与えてnew Worker
の第二引数でdeno
を書かなかった場合、
Deno
が使えないからDeno.readTextFile
は使えないけど、fetch
は使えるので通信はできちゃう。
全部拒否する場合はこう。
deno
プロパティを指定するために--unstable
が必要。
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
type: "module",
deno: {
namespace: false,
permissions: "none",
},
});
それかdeno: false
でnamespaceとpermissionsを全て無効にできる。
一部許可する場合はこんな感じ。
ここで許可してもmain.ts
に与えられてる権限以上のことは出来ない。
const worker = new Worker(new URL("./worker.js", import.meta.url).href, {
type: "module",
deno: {
namespace: true,
permissions: {
net: [
"https://deno.land/",
],
read: [
new URL("./file_1.txt", import.meta.url),
new URL("./file_2.txt", import.meta.url),
],
write: false,
},
},
});
Denoで安全にevalしたいコード
while(true) ;
とかも怖いから、処理が長いときは†自戒†する。
const WORKER_PATH = "./safe_eval_worker.ts";
const TIMEOUT_SEC = 3;
/** workerにpostMessageして返事を受け取るPromiseを返す */
function waitForWorkerResponse(
worker: Worker,
message: string,
timeoutSeconds = TIMEOUT_SEC,
) {
return new Promise((resolve, reject) => {
worker.postMessage(message);
// sec秒経過したら失敗
const timerId = setTimeout(() => {
worker.terminate();
reject(new Error(`worker operation timeout (${timeoutSeconds} seconds)`));
}, timeoutSeconds * 1000);
// 返事がきたら成功
worker.onmessage = ({ data }) => {
clearTimeout(timerId); //タイマー解除
worker.terminate();
resolve(data);
};
});
}
/** Workerを使って安全にevalできるはずの関数 */
export async function safeEval(source: string, timeoutSeconds = TIMEOUT_SEC) {
const workerURL = new URL(WORKER_PATH, import.meta.url).href;
const worker = new Worker(workerURL, { type: "module", deno: false });
const result = await waitForWorkerResponse(worker, source, timeoutSeconds);
return result;
}
/// <reference lib="deno.worker" />
/** evalの代替 */
function evaluator(source: string) {
return Function(`'use strict'; return ${source}`)();
}
self.onmessage = ({ data: source }) => {
const evaluated = evaluator(source);
self.postMessage(evaluated);
self.close();
};
import {safeEval} from "./safe_eval.ts"
const source = `1 + 1`;
const result = await safeEval(source);
console.log(result); // 2
こんな感じにした。
何回も実行する必要があるなら毎回new Worker
せず使いまわしたほうがいいと思います。
なんか間違ってたり改善点あれば教えて欲しいです。
あと「こんなん全然ダメだよ」って思われた方もぜひ。
絶対ですよ。