Worker 内部机制:Bootstrap、Deno 命名空间与权限系统
前置知识
- ›第 1-3 篇文章
- ›了解 V8 isolate 和 context 的基本概念
Worker 内部机制:Bootstrap、Deno 命名空间与权限系统
前三篇文章分别介绍了 CLI 分发机制、扩展系统和模块加载。现在我们来到 Deno 运行时的核心——MainWorker。这个结构体持有 V8 isolate、驱动事件循环,并向用户代码暴露 Deno.* API。本文将深入追踪其创建过程中的泛型 WorkerOptions,观察 bootstrap 序列如何跨越 Rust 与 JavaScript 的边界,了解 90_deno_ns.js 如何将数十个扩展模块组装成 Deno 全局对象,并剖析那套由 8 种权限类型构成的"默认安全"体系。
MainWorker 的创建与 WorkerOptions
MainWorker 结构体出乎意料地简洁——它在 JsRuntime 的基础上封装了少量用于生命周期事件的 V8 函数句柄:
pub struct MainWorker {
pub js_runtime: JsRuntime,
should_break_on_first_statement: bool,
should_wait_for_inspector_session: bool,
exit_code: ExitCode,
bootstrap_fn_global: Option<v8::Global<v8::Function>>,
dispatch_load_event_fn_global: v8::Global<v8::Function>,
dispatch_beforeunload_event_fn_global: v8::Global<v8::Function>,
dispatch_unload_event_fn_global: v8::Global<v8::Function>,
dispatch_process_beforeexit_event_fn_global: v8::Global<v8::Function>,
dispatch_process_exit_event_fn_global: v8::Global<v8::Function>,
}
这些 v8::Global<v8::Function> 句柄指向初始化阶段提取并保存的 JavaScript 函数,供后续调用使用。bootstrap 函数在 bootstrap 阶段通过 Option::take 消费一次即销毁;事件分发函数则在 worker 的整个生命周期内持续存在。
创建过程由 WorkerServiceOptions 参数化控制,它带有三个泛型类型参数:
pub struct WorkerServiceOptions<
TInNpmPackageChecker: InNpmPackageChecker,
TNpmPackageFolderResolver: NpmPackageFolderResolver,
TExtNodeSys: ExtNodeSys,
> {
pub module_loader: Rc<dyn ModuleLoader>,
pub permissions: PermissionsContainer,
pub fs: Arc<dyn FileSystem>,
pub blob_store: Arc<BlobStore>,
pub node_services: Option<NodeExtInitServices<...>>,
pub v8_code_cache: Option<Arc<dyn CodeCache>>,
// ...
}
这些泛型支持不同的配置场景:CLI 使用 DenoInNpmPackageChecker 和 NpmResolver<RealSys>,独立二进制文件使用基于虚拟文件系统的自有实现,测试环境则可以对一切进行 mock。
classDiagram
class MainWorker {
+js_runtime: JsRuntime
+bootstrap(options)
+execute_main_module(url)
+run_event_loop()
+dispatch_load_event()
}
class WorkerServiceOptions~T1,T2,T3~ {
+module_loader: Rc~dyn ModuleLoader~
+permissions: PermissionsContainer
+fs: Arc~dyn FileSystem~
+node_services: Option~NodeExtInitServices~
}
class WorkerOptions {
+bootstrap: BootstrapOptions
+extensions: Vec~Extension~
+startup_snapshot: Option~&[u8]~
+unconfigured_runtime: Option~UnconfiguredRuntime~
}
MainWorker <.. WorkerServiceOptions : created from
MainWorker <.. WorkerOptions : created from
Bootstrap 序列:Rust → V8 → JavaScript
bootstrap() 方法是 Rust 第一次调用 JavaScript 的地方:
pub fn bootstrap(&mut self, options: BootstrapOptions) {
{
let op_state = self.js_runtime.op_state();
let mut state = op_state.borrow_mut();
state.put(options.clone());
if let Some((fd, serialization)) = options.node_ipc_init {
state.put(deno_node::ChildPipeFd(fd, serialization));
}
}
deno_core::scope!(scope, &mut self.js_runtime);
v8::tc_scope!(scope, scope);
let args = options.as_v8(scope);
let bootstrap_fn = self.bootstrap_fn_global.take().unwrap();
let bootstrap_fn = v8::Local::new(scope, bootstrap_fn);
let undefined = v8::undefined(scope);
bootstrap_fn.call(scope, undefined.into(), &[args]);
}
bootstrap 选项首先被写入 OpState(供 op 访问),然后序列化为 V8 值,直接传递给 JavaScript 端的 bootstrapMainRuntime() 函数。该函数在快照创建阶段从 99_main.js 中提取,并以 v8::Global<v8::Function> 的形式保存。
sequenceDiagram
participant Rust as MainWorker (Rust)
participant OpState as OpState
participant V8 as V8 Scope
participant JS as 99_main.js
Rust->>OpState: Put BootstrapOptions
Rust->>V8: Create scope
Rust->>V8: options.as_v8(scope)
Note over V8: Serialize Deno version,<br/>location, unstable features,<br/>inspect flag, etc.
Rust->>V8: bootstrap_fn.call(scope, args)
V8->>JS: bootstrapMainRuntime(runtimeOptions)
JS->>JS: Configure console for serve mode
JS->>JS: Register main module handler
JS->>JS: Set up globalThis properties
JS->>JS: Configure error formatting
JS->>JS: Set hasBootstrapped = true
JS-->>V8: return
V8-->>Rust: bootstrap complete
对 bootstrap 函数使用 take().unwrap() 是有意为之——这个函数只应被调用一次。如果尝试二次调用,None 值会触发 panic,JavaScript 端会抛出"Worker runtime already bootstrapped"错误。
组装 Deno 命名空间
runtime/js/90_deno_ns.js 通过从各扩展模块导入来构建 Deno 全局命名空间:
import * as fs from "ext:deno_fs/30_fs.js";
import * as net from "ext:deno_net/01_net.js";
import * as process from "ext:deno_process/40_process.js";
import * as permissions from "ext:runtime/10_permissions.js";
import * as kv from "ext:deno_kv/01_db.ts";
import * as telemetry from "ext:deno_telemetry/telemetry.ts";
// ... 30+ imports
const denoNs = {
writeFileSync: fs.writeFileSync,
writeFile: fs.writeFile,
readTextFile: fs.readTextFile,
readFile: fs.readFile,
watchFs: fsEvents.watchFs,
chmod: fs.chmod,
cwd: fs.cwd,
// ... 100+ API bindings
};
每个 ext: 导入对应一个随扩展打包的 JavaScript 文件(详见第 2 篇)。denoNs 对象是一个扁平的命名空间对象——没有原型链,没有类,只有函数引用和懒加载 getter。
graph TD
subgraph "Extension Sources"
FS["ext:deno_fs/30_fs.js"]
NET["ext:deno_net/01_net.js"]
PROC["ext:deno_process/40_process.js"]
KV["ext:deno_kv/01_db.ts"]
CRON["ext:deno_cron/01_cron.ts"]
FETCH["ext:deno_fetch/22_http_client.js"]
PERM["ext:runtime/10_permissions.js"]
end
subgraph "Deno Namespace"
DNS["Deno.readFile()<br/>Deno.writeFile()<br/>Deno.cwd()"]
DNN["Deno.connect()<br/>Deno.listen()"]
DNP["Deno.run()<br/>Deno.Command"]
DNK["Deno.openKv()"]
DNC["Deno.cron()"]
DNH["Deno.createHttpClient()"]
DNPERM["Deno.permissions"]
end
FS --> DNS
NET --> DNN
PROC --> DNP
KV --> DNK
CRON --> DNC
FETCH --> DNH
PERM --> DNPERM
不稳定 API 通过 feature ID 进行门控。bootstrap 阶段会读取 BootstrapOptions 中的 unstableFeatures 数组,决定哪些不稳定 API 可以对外暴露。未启用的 feature 对应的 API 在 Deno 对象上将保持未定义状态。
提示: JS 文件名前的数字前缀(
01_、02_、30_、90_、99_)用于控制加载顺序。90_deno_ns.js导入所有其他模块,99_main.js再导入90_deno_ns.js。这一约定早于扩展中 ES 模块支持的引入,沿用至今主要是为了保持清晰的结构。
权限系统
Deno"默认安全"的理念由一套 8 种权限类型构成的系统来落地,定义在 runtime/permissions/lib.rs 中。这些权限类型覆盖了 op 可能访问的所有系统资源:
| 权限 | 标志位 | 保护范围 |
|---|---|---|
| Read | --allow-read |
文件/目录读取 |
| Write | --allow-write |
文件/目录写入 |
| Net | --allow-net |
网络连接 |
| Env | --allow-env |
环境变量访问 |
| Run | --allow-run |
子进程执行 |
| FFI | --allow-ffi |
外部函数接口 |
| Sys | --allow-sys |
系统信息查询 |
| Import | --allow-import |
远程模块导入 |
PermissionFlags 结构体体现了三层设计:
pub struct PermissionFlags {
pub allow_all: bool,
pub allow_env: Option<Vec<String>>,
pub deny_env: Option<Vec<String>>,
pub ignore_env: Option<Vec<String>>,
pub allow_read: Option<Vec<String>>,
pub deny_read: Option<Vec<String>>,
pub ignore_read: Option<Vec<String>>,
// ... similar for net, run, ffi, sys, write
}
deny_* 标志的优先级高于 allow_*——你可以开放所有网络访问,同时明确屏蔽某个特定域名。ignore_* 标志则用于为特定资源关闭交互式提示,在 CI 环境中尤为实用。
flowchart TD
OP["Op requests permission<br/>e.g., read /etc/passwd"]
DENY{Denied by<br/>--deny-read?}
DENY -->|Yes| BLOCKED["❌ PermissionDenied"]
DENY -->|No| ALLOW{Allowed by<br/>--allow-read?}
ALLOW -->|Yes| GRANTED["✅ Granted"]
ALLOW -->|No| IGNORE{Ignored by<br/>--ignore-read?}
IGNORE -->|Yes| BLOCKED
IGNORE -->|No| BROKER{Permission<br/>broker active?}
BROKER -->|Yes| IPC["Ask broker via IPC"]
BROKER -->|No| TTY{Interactive<br/>terminal?}
TTY -->|Yes| PROMPT["🔐 Prompt user<br/>Allow? [y/n/A]"]
TTY -->|No| BLOCKED
PROMPT -->|Allow| GRANTED
PROMPT -->|Allow All| GRANT_ALL["✅ Grant for type"]
PROMPT -->|Deny| BLOCKED
IPC -->|Allow| GRANTED
IPC -->|Deny| BLOCKED
权限检查发生在 Rust op 边界——也就是第 2 篇中追踪过的那些 op 入口处。每个文件系统 op 在访问磁盘之前,都会通过 state.borrow::<PermissionsContainer>() 执行权限检查。这确保了无论代码通过何种路径调用,都无法绕过权限验证。
权限库中的 OtelAuditFn 类型提示揭示了另一个特性:OpenTelemetry 审计追踪。配置 OTEL 后,每次权限检查——无论通过还是拒绝——都会作为遥测事件上报,形成完整的程序访问记录。
权限 Broker:实验性的 IPC 权限管理
maybe_setup_permission_broker() 函数展示了一个实验性特性:通过 IPC 将权限决策委托给外部进程:
fn maybe_setup_permission_broker() {
let Ok(socket_path) = std::env::var("DENO_PERMISSION_BROKER_PATH") else {
return;
};
log::warn!("{} Permission broker is an experimental feature",
colors::yellow("Warning"));
let broker = PermissionBroker::new(socket_path);
deno_runtime::deno_permissions::broker::set_broker(broker);
}
设置 DENO_PERMISSION_BROKER_PATH 环境变量后,Deno 会连接到一个 Unix domain socket,并将权限请求发送给外部进程处理。这为 IDE 或编排器等父进程统一管理多个 Deno 子进程的权限提供了可能。
sequenceDiagram
participant Deno as Deno Process
participant Broker as Permission Broker
Note over Deno: DENO_PERMISSION_BROKER_PATH set
Deno->>Broker: Connect via Unix socket
Deno->>Deno: Op requests net permission
Deno->>Broker: Check: net, "example.com:443"
Broker->>Broker: Apply policy
Broker-->>Deno: Allow / Deny
Deno->>Deno: Grant or reject op
在权限决策流程中,broker 的介入优先于交互式提示——只要 broker 处于活跃状态,所有权限决策都由它全权接管。
CliMainWorker:覆盖率、性能分析与 HMR
runtime/ 中的 MainWorker 是一个通用 worker。CLI 将其封装为 CliMainWorker,在此基础上添加 CLI 专属功能:
pub struct CliMainWorkerOptions {
pub create_hmr_runner: Option<CreateHmrRunnerCb>,
pub maybe_coverage_dir: Option<PathBuf>,
pub maybe_cpu_prof_config: Option<CpuProfilerConfig>,
pub default_npm_caching_strategy: NpmCachingStrategy,
pub needs_test_modules: bool,
}
pub struct CliMainWorker {
worker: LibMainWorker,
shared: Arc<SharedState>,
}
run() 方法在模块执行外层包装了可选的覆盖率收集、CPU 性能分析和 HMR(热模块替换)逻辑。这些功能以 Rc<RefCell<Option<...>>> 的形式持有,确保正常退出路径和 Deno.exit() op 路径都能正确刷新数据——先到先得,避免重复刷新。
提示: 中间类型
LibMainWorker(来自deno_lib)让独立二进制文件可以复用相同的 worker 逻辑,而无需依赖完整的 CLI。这与第 1 篇中提到的模块提取模式如出一辙。
下一步
至此,我们已经完整了解了一个 Deno worker 的生命周期:基于泛型 service options 的创建、跨越 Rust-V8-JavaScript 边界的 bootstrap、从扩展模块组装命名空间,以及在 op 层执行的权限检查。在最后一篇文章中,我们将探索 Deno 的集成工具链——cli/tools/ 目录下支撑 fmt、lint、test、compile 等命令的实现——以及 ext/node/ 中深度的 Node.js 兼容层、LSP 架构和 npm 集成。