Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .github/workflows/code_style.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,42 @@ jobs:
- name: Check lints
run: cargo clippy --all --benches --tests --examples ${{ matrix.flags }} -- -D warnings

clippy-windows:
name: Clippy Windows (${{ matrix.name }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- name: all-features
flags: "--all-features"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
components: clippy
- uses: Swatinem/rust-cache@v2
with:
key: clippy-windows-${{ matrix.name }}
- name: Check lints
run: cargo clippy --all --benches --tests --examples ${{ matrix.flags }} -- -D warnings

# Roll-up job for branch protection
code-style:
name: Code Style (fmt + clippy)
runs-on: ubuntu-latest
if: always()
needs: [format, clippy]
needs: [format, clippy, clippy-windows]
steps:
- run: |
if [[ "${{ needs.format.result }}" != "success" || "${{ needs.clippy.result }}" != "success" ]]; then
if [[ "${{ needs.format.result }}" != "success" || "${{ needs.clippy.result }}" != "success" || "${{ needs.clippy-windows.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
30 changes: 28 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ jobs:
- name: Run Telegram Channel Tests
run: cargo test --manifest-path channels-src/telegram/Cargo.toml -- --nocapture

windows-build:
name: Windows Build (${{ matrix.name }})
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
include:
- name: all-features
flags: "--all-features"
- name: default
flags: ""
- name: libsql-only
flags: "--no-default-features --features libsql"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
key: windows-${{ matrix.name }}
- name: Check compilation
run: cargo check --all --benches --tests --examples ${{ matrix.flags }}

wasm-wit-compat:
name: WASM WIT Compatibility
runs-on: ubuntu-latest
Expand Down Expand Up @@ -100,10 +126,10 @@ jobs:
name: Run Tests
runs-on: ubuntu-latest
if: always()
needs: [tests, telegram-tests, wasm-wit-compat, docker-build, version-check]
needs: [tests, telegram-tests, wasm-wit-compat, docker-build, windows-build, version-check]
steps:
- run: |
if [[ "${{ needs.tests.result }}" != "success" || "${{ needs.telegram-tests.result }}" != "success" || "${{ needs.wasm-wit-compat.result }}" != "success" || "${{ needs.docker-build.result }}" != "success" ]]; then
if [[ "${{ needs.tests.result }}" != "success" || "${{ needs.telegram-tests.result }}" != "success" || "${{ needs.wasm-wit-compat.result }}" != "success" || "${{ needs.docker-build.result }}" != "success" || "${{ needs.windows-build.result }}" != "success" ]]; then
echo "One or more jobs failed"
exit 1
fi
Expand Down
11 changes: 10 additions & 1 deletion src/channels/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,16 @@ impl WasmChannelRuntime {
// Enable persistent compilation cache. Wasmtime serializes compiled native
// code to disk (~/.cache/wasmtime by default), so subsequent startups
// deserialize instead of recompiling — typically 10-50x faster.
if let Err(e) = wasmtime_config.cache_config_load_default() {
//
// On Windows, each Engine gets its own cache subdirectory to avoid
// OS error 33 (ERROR_LOCK_VIOLATION) when multiple engines share the
// default cache and Windows holds exclusive locks on memory-mapped
// files. See #448.
if let Err(e) = crate::tools::wasm::enable_compilation_cache(
&mut wasmtime_config,
"channels",
config.cache_dir.as_deref(),
) {
tracing::warn!("Failed to enable wasmtime compilation cache: {}", e);
}

Expand Down
4 changes: 3 additions & 1 deletion src/sandbox/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
//! ```

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
use std::time::Duration;

use bollard::Docker;
Expand Down
3 changes: 3 additions & 0 deletions src/secrets/keychain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
use crate::secrets::SecretError;

/// Service name for keychain entries.
#[cfg(any(target_os = "macos", target_os = "linux"))]
const SERVICE_NAME: &str = "ironclaw";

/// Account name for the master key.
#[cfg(any(target_os = "macos", target_os = "linux"))]
const MASTER_KEY_ACCOUNT: &str = "master_key";

/// Generate a random 32-byte master key.
Expand Down Expand Up @@ -261,6 +263,7 @@ mod platform {
pub use platform::{delete_master_key, get_master_key, has_master_key, store_master_key};

/// Parse a hex string to bytes.
#[cfg(any(target_os = "macos", target_os = "linux", test))]
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, SecretError> {
if !hex.len().is_multiple_of(2) {
return Err(SecretError::KeychainError(
Expand Down
1 change: 1 addition & 0 deletions src/setup/channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ async fn setup_tunnel_cloudflare() -> Result<TunnelSettings, ChannelSetupError>
/// Detect running cloudflared processes or managed services that could conflict
/// with IronClaw's tunnel management.
fn detect_existing_cloudflared() -> Option<String> {
#[allow(unused_mut)]
let mut conflicts: Vec<String> = Vec::new();

// Check for running cloudflared processes (all platforms)
Expand Down
2 changes: 1 addition & 1 deletion src/tools/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub use limits::{
DEFAULT_FUEL_LIMIT, DEFAULT_MEMORY_LIMIT, DEFAULT_TIMEOUT, FuelConfig, ResourceLimits,
WasmResourceLimiter,
};
pub use runtime::{PreparedModule, WasmRuntimeConfig, WasmToolRuntime};
pub use runtime::{PreparedModule, WasmRuntimeConfig, WasmToolRuntime, enable_compilation_cache};
pub use wrapper::{OAuthRefreshConfig, WasmToolWrapper};

// Capabilities (V2)
Expand Down
120 changes: 118 additions & 2 deletions src/tools/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! This matches NEAR blockchain patterns for deterministic, isolated execution.

use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;

Expand All @@ -18,6 +18,58 @@ use crate::tools::wasm::limits::{FuelConfig, ResourceLimits};
/// which causes any store with an expired epoch deadline to trap.
pub const EPOCH_TICK_INTERVAL: Duration = Duration::from_millis(500);

/// Enable wasmtime's persistent compilation cache for a [`Config`].
///
/// On Unix, this delegates to `cache_config_load_default()` which uses a
/// shared cache directory. On Windows, each engine gets its own subdirectory
/// (keyed by `label`) to avoid OS error 33 (`ERROR_LOCK_VIOLATION`) when
/// multiple engines memory-map files in the same cache directory. See #448.
///
/// If `explicit_dir` is `Some`, it is used as the cache directory on all
/// platforms, bypassing the default.
pub fn enable_compilation_cache(
wasmtime_config: &mut Config,
label: &str,
explicit_dir: Option<&Path>,
) -> anyhow::Result<()> {
// If the caller provided an explicit directory, or we're on Windows and
// need per-engine isolation, write a TOML config with a custom directory.
let custom_dir = match explicit_dir {
Some(dir) => Some(dir.to_path_buf()),
#[cfg(windows)]
None => {
let base = dirs::cache_dir()
.unwrap_or_else(std::env::temp_dir)
.join("ironclaw");
Some(base.join(format!("wasmtime-{}", label)))
}
#[cfg(not(windows))]
None => {
let _ = label;
None
}
};

match custom_dir {
Some(dir) => {
std::fs::create_dir_all(&dir)?;
let toml_path = dir.join("wasmtime-cache.toml");
let escaped = dir
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"");
let toml_content = format!("[cache]\nenabled = true\ndirectory = \"{}\"\n", escaped);
Comment on lines +57 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Manually escaping characters for TOML generation is brittle and can be error-prone if paths contain unusual characters. It's more robust to use the toml crate to handle serialization and escaping of values. This ensures that the generated TOML is always valid.

You can achieve this by using toml::Value::String, which will correctly quote and escape the path string. This may require adding the toml crate as a dependency if it's not already available.

Suggested change
let escaped = dir
.to_string_lossy()
.replace('\\', "\\\\")
.replace('"', "\\\"");
let toml_content = format!("[cache]\nenabled = true\ndirectory = \"{}\"\n", escaped);
let toml_content = format!(
"[cache]\nenabled = true\ndirectory = {}\n",
toml::Value::String(dir.to_string_lossy().into_owned())
);

std::fs::write(&toml_path, toml_content)?;
wasmtime_config.cache_config_load(&toml_path)?;
Ok(())
}
None => {
wasmtime_config.cache_config_load_default()?;
Ok(())
}
}
}

/// Configuration for the WASM runtime.
#[derive(Debug, Clone)]
pub struct WasmRuntimeConfig {
Expand Down Expand Up @@ -136,7 +188,14 @@ impl WasmToolRuntime {
// Enable persistent compilation cache. Wasmtime serializes compiled native
// code to disk (~/.cache/wasmtime by default), so subsequent startups
// deserialize instead of recompiling — typically 10-50x faster.
if let Err(e) = wasmtime_config.cache_config_load_default() {
//
// On Windows, each Engine gets its own cache subdirectory to avoid
// OS error 33 (ERROR_LOCK_VIOLATION) when multiple engines share the
// default cache and Windows holds exclusive locks on memory-mapped
// files. See #448.
if let Err(e) =
enable_compilation_cache(&mut wasmtime_config, "tools", config.cache_dir.as_deref())
{
tracing::warn!("Failed to enable wasmtime compilation cache: {}", e);
}

Expand Down Expand Up @@ -348,6 +407,63 @@ mod tests {
assert_eq!(limits.fuel, 500_000);
}

/// Per-engine cache directories must work correctly to avoid file lock
/// conflicts on Windows where multiple engines sharing a single cache
/// directory triggers OS error 33 (ERROR_LOCK_VIOLATION). Regression test
/// for #448: `enable_compilation_cache` must create a subdirectory and
/// produce a valid TOML config that wasmtime can load.
#[test]
fn test_enable_compilation_cache_with_explicit_dir() {
use crate::tools::wasm::runtime::enable_compilation_cache;

let tmp = tempfile::tempdir().expect("failed to create temp dir");
let cache_dir = tmp.path().join("custom-cache");

let mut config = wasmtime::Config::new();
enable_compilation_cache(&mut config, "test-engine", Some(cache_dir.as_path()))
.expect("enable_compilation_cache should succeed with explicit dir");

// The cache directory should have been created.
assert!(cache_dir.exists(), "cache directory should be created");

// A TOML config file should have been written inside.
let toml_path = cache_dir.join("wasmtime-cache.toml");
assert!(toml_path.exists(), "TOML config should be written");

let content = std::fs::read_to_string(&toml_path).unwrap();
assert!(
content.contains("[cache]"),
"TOML must contain [cache] section"
);
assert!(content.contains("enabled = true"), "cache must be enabled");
}

/// Two engines with different labels must get independent cache directories
/// so that their file locks do not conflict. Regression test for #448.
#[test]
fn test_enable_compilation_cache_label_isolation() {
use crate::tools::wasm::runtime::enable_compilation_cache;

let tmp = tempfile::tempdir().expect("failed to create temp dir");
let base = tmp.path().join("isolation");

let dir_a = base.join("engine-a");
let dir_b = base.join("engine-b");

let mut config_a = wasmtime::Config::new();
enable_compilation_cache(&mut config_a, "a", Some(dir_a.as_path()))
.expect("cache A should succeed");

let mut config_b = wasmtime::Config::new();
enable_compilation_cache(&mut config_b, "b", Some(dir_b.as_path()))
.expect("cache B should succeed");

// Both directories must exist and be distinct.
assert!(dir_a.exists());
assert!(dir_b.exists());
assert_ne!(dir_a, dir_b);
}

/// The WASM runtime (Wasmtime engine) must initialise successfully even
/// when no tools directory exists on disk. The engine only configures the
/// compiler and epoch ticker — loading modules from a directory is a
Expand Down
Loading