Read OSS

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 使用 DenoInNpmPackageCheckerNpmResolver<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/ 目录下支撑 fmtlinttestcompile 等命令的实现——以及 ext/node/ 中深度的 Node.js 兼容层、LSP 架构和 npm 集成。