Skip to content

feat(spider-execution-manager): Add single-process supervisor pool for the task executor.#326

Open
LinZhihao-723 wants to merge 10 commits into
y-scope:mainfrom
LinZhihao-723:process-pool-basic
Open

feat(spider-execution-manager): Add single-process supervisor pool for the task executor.#326
LinZhihao-723 wants to merge 10 commits into
y-scope:mainfrom
LinZhihao-723:process-pool-basic

Conversation

@LinZhihao-723
Copy link
Copy Markdown
Member

@LinZhihao-723 LinZhihao-723 commented May 20, 2026

Description

This PR depends on #325.

New crate

This PR introduces a new crate spider-execution-manager. Its first module, process_pool, is a single-process supervisor that owns one spider-task-executor subprocess and serializes access to it. The public surface is one factory (ProcessPool::new) and one async API (ProcessPool::execute(request, hard_timeout) -> Result<Outcome, InternalError>). Multi-slot pools are deliberately deferred — the EM main loop only needs one process today, and the smaller surface keeps the consumer's integration minimal.

Lifecycle and concurrency

The pool spawns the initial executor at construction. Concurrent execute callers queue on a single tokio::Mutex around the handle slot; the guard is held for the entire call, so the mutex is the entire concurrency gate (no separate semaphore). The parent is the sole authority for the hard timeout — if either the parent's timer fires or the child exits / closes stdout, the dead handle is dropped (Command::kill_on_drop(true) SIGKILLs the OS process), a fresh executor is spawned in the same critical section, and the next caller sees a healthy process. The four channel-level failure modes inside the protocol exchange (framed write failure, framed read failure, stdout EOF, response decode failure) all collapse into Outcome::ExecutorCrash, which feeds the same respawn branch as Outcome::Timeout.

Logging and observability

Each spawn allocates a monotonic executor_id and routes the child's stderr to <log_dir>/<em_id>-<executor_id>.log in create-or-append mode (rather than inheriting the parent's stderr). Per-spawn filenames mean each respawn naturally rotates onto a fresh file; a size/time rotator at the executor side is the documented next step. All pool-emitted tracing events carry executor_id as a typed u64 field so JSON subscribers render it as a number; ID newtypes that lack Display use debug formatting.

Error model

InternalError is the pool's own failure surface, distinct from a task-level Outcome. Four variants cover the cases where the pool can't serve the current call: NotRunning (a prior respawn failed and the pool is unrecoverable), ExecutorCreationFailure (any I/O step during spawn), EncodeTaskContext, and EncodeTaskInputs. All non-NotRunning variants use thiserror's #[from] so the body propagates with ? throughout. There are no panics in either the happy path or the error path of execute.

Cancellation safety (known limitation)

execute holds the mutex guard across .await points. If the caller's future is dropped mid-flight (e.g. a higher-layer timeout, tokio::select! losing the branch, ctrl-C), the guard releases but the executor keeps running and will eventually push a response into the stdout pipe — which the next execute call would read as the first frame of its own request. The protocol no longer carries instance_id, so there's no automatic mismatch detection. The EM main loop will own these futures to completion, so this isn't reachable in practice today; documented as a follow-up to harden via a Drop guard that flags the handle dirty on cancellation.

Test coverage

End-to-end tests added at tests/huntsman/task-executor/tests/test_process_pool.rs, against the real task-executor binary using the existing harness:

  • fibonacci_succeeds — happy path.
  • always_fail_reports_task_error — in-task failure surfaces a decodable ExecutorError.
  • always_panic_returns_crash_then_respawns — panicking task returns Outcome::ExecutorCrash; a follow-up execute on the same pool succeeds, proving the respawn.
  • hard_timeout_kills_then_respawns — long-running task plus a short hard timeout returns Outcome::Timeout; a follow-up execute succeeds, proving the SIGKILL + respawn.

The respawn assertion is functional — the follow-up call returning Outcome::Success is the visible evidence that a fresh process is alive and serving. No internal pool state is inspected.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Ensure all workflows pass.
  • Add test coverage as discussed above.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added process pool execution manager for managing subprocess-based task executors with timeout and crash recovery
    • Added standalone task executor binary supporting length-delimited message-based communication
    • Implemented wire protocol for task execution requests and responses with serialization support
  • Tests

    • Added comprehensive integration test suite covering task execution, error handling, panic recovery, and timeout resilience

Review Change Stack

@LinZhihao-723 LinZhihao-723 requested review from a team and sitaowang1998 as code owners May 20, 2026 00:46
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Walkthrough

This pull request implements a process pool manager for executing TDL tasks in isolated, long-lived subprocesses. A new spider-execution-manager component manages a pool of spider-task-executor binary subprocesses using bincode-framed request/response IPC. The executor binary handles task loading and execution; the pool serializes concurrent requests, implements timeout supervision, and transparently respawns crashed executors. Error types are serialized for cross-process transmission. New integration test infrastructure and comprehensive test coverage validate correctness and resilience.

Changes

Execution System Implementation

Layer / File(s) Summary
Wire protocol and executor error serialization
components/spider-task-executor/src/protocol.rs, components/spider-task-executor/src/error.rs, components/spider-task-executor/src/manager.rs, components/spider-task-executor/src/lib.rs
Defines Request, Response, and ExecutorOutcome enums for bincode-framed subprocess IPC. Makes ExecutorError serializable by converting typed error fields to String payloads with From implementations. Updates TdlPackageManager::load() to return a package reference instead of a name string.
Task executor subprocess binary
components/spider-task-executor/Cargo.toml, components/spider-task-executor/src/bin/spider_task_executor.rs
Implements the spider-task-executor standalone binary that reads SPIDER_TDL_PACKAGE_DIR for TDL packages, initializes JSON tracing to stderr, enters a loop deserializing framed Requests from stdin, executes tasks with execute_task(), and sends back framed Response::Result payloads with ExecutorOutcome and elapsed time. Expands dependencies to include tokio, bincode, tracing, and serialization crates.
Process pool manager and subprocess lifecycle
components/spider-execution-manager/Cargo.toml, components/spider-execution-manager/src/lib.rs, components/spider-execution-manager/src/process_pool.rs
Introduces ProcessPool that pre-spawns an executor and wraps it in a mutex. execute() method serializes concurrent requests, sends framed bincode to the executor, races response receipt against a deadline using tokio::select!, and respawns the executor (by dropping and re-creating the handle) when Timeout or ExecutorCrash outcomes are returned. Exposes ProcessPoolConfig, ExecuteRequest, Outcome, and InternalError types.

Integration Test Infrastructure and Test Cases

Layer / File(s) Summary
Test harness and executor handle
tests/huntsman/task-executor/Cargo.toml, tests/huntsman/task-executor/src/lib.rs
Provides ExecutorHandle struct to spawn the executor binary as a subprocess in tests, wrapping framed bincode I/O over stdin/stdout. Exports configuration helpers (task_executor_bin(), tdl_package_dir()), request builders (execute_request(), build_ctx()), and codec helpers (encode_single_input<T>(), encode_no_inputs(), decode_single_output<T>()) to simplify test code.
Integration test task implementations
tests/huntsman/integration-test-tasks/Cargo.toml, tests/huntsman/integration-test-tasks/src/lib.rs
Defines four TDL test tasks: fibonacci (recursive computation), always_fail (returns ExecutionError), always_panic (panics to trigger crash detection), and instrument (sleeps and echoes input for overhead measurement). Includes INSTRUMENT_SLEEP_US constant for benchmarking consistency.
Executor and pool correctness tests
tests/huntsman/task-executor/Cargo.toml, tests/huntsman/task-executor/tests/test_executor.rs, tests/huntsman/task-executor/tests/test_process_pool.rs, tests/huntsman/task-executor/tests/overhead_instrument.rs
Tests executor binary directly: fibonacci correctness, task error reporting, and subprocess crash handling. Tests ProcessPool: success cases, in-task failures, executor respawn after crash and timeout, and overhead instrumentation that measures end-to-end latency, executor FFI time, and IPC overhead, writing percentile metrics to markdown.

Build Configuration and Manifest Updates

Layer / File(s) Summary
Workspace and build task configuration
Cargo.toml, taskfiles/test.yaml
Adds components/spider-execution-manager, tests/huntsman/integration-test-tasks, and tests/huntsman/task-executor to workspace members. Updates spider-huntsman-unit-tests-executor task to define G_TDL_PACKAGES_DIR and G_RUST_RELEASE_DIR, stage compiled .so artifacts into a directory tree, set SPIDER_TDL_PACKAGE_DIR, SPIDER_TASK_EXECUTOR_BIN, and SPIDER_TDL_PACKAGE_COMPLEX environment variables, and build all required binaries.
Existing test compatibility
tests/huntsman/tdl-integration/tests/complex.rs
Updates load_and_query_name test to work with the new TdlPackageManager::load() return type (now &TdlPackage instead of String).

🎯 4 (Complex) | ⏱️ ~75 minutes


Possibly related PRs

  • y-scope/spider#317: Modifies the same ExecutorError type by adding serde derives and changing error variant payloads to strings; this PR continues that work by making errors serializable across process boundaries.

Suggested reviewers

  • sitaowang1998
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing a single-process supervisor pool for the task executor as the primary addition to the spider-execution-manager crate.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/huntsman/tdl-integration/tests/complex.rs (1)

105-106: ⚡ Quick win

Consider using the load() return value directly in other tests.

Throughout the file, tests follow the pattern of discarding the load() result and immediately calling get() to retrieve the same package. Now that load() returns a package reference, these tests could use it directly for consistency and to eliminate redundant lookups.

♻️ Example refactor for one test
 fn version_is_compatible() -> anyhow::Result<()> {
     let path = lib_path();
     let mut manager = TdlPackageManager::new();
-    manager.load(&path)?;
-    let pkg = manager.get(PACKAGE_NAME).expect("package should be loaded");
+    let pkg = manager.load(&path)?;
     assert_eq!(pkg.version(), Version::SPIDER_TDL);

This pattern can be applied to tests at lines 102–110, 114–126, 130–167, 171–196, 200–224, 228–258, 262–300, 304–328, 332–346, and 350–364.

Also applies to: 117-118, 133-134, 174-175, 203-204, 231-232, 265-266, 307-308, 335-336, 353-354

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/tdl-integration/tests/complex.rs` around lines 105 - 106,
Tests currently discard the return value of manager.load(&path)? and call
manager.get(PACKAGE_NAME) immediately after; change each test to use the
PackageRef returned by manager.load(...) directly (e.g., let pkg =
manager.load(&path)? ) instead of calling manager.get(...), replacing subsequent
uses of the retrieved package with that local variable; update all occurrences
mentioned (the blocks around manager.load/get and the listed line ranges) so
they use the load() return value for consistency and to avoid redundant lookups.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/spider-task-executor/src/bin/spider_task_executor.rs`:
- Around line 8-10: The package resolution docs are inconsistent with the
loading behavior: update the comment that describes the path for an `Execute`
request so it matches the actual filename used by the loader; replace the
`${package}/${package}.so` description with the implemented
`${SPIDER_TDL_PACKAGE_DIR}/${package}/lib{package}.so` (i.e., mention the lib
prefix) so the docstring and the runtime lookup (which loads `lib{package}.so`)
are aligned.

In `@tests/huntsman/task-executor/tests/overhead_instrument.rs`:
- Around line 177-180: The test fails when SPIDER_TEST_INSTRUMENT_OUTPUT_DIR
isn't pre-created because the code calls File::create on path immediately;
before creating the file, call std::fs::create_dir_all(&output_dir) (or
equivalent) to ensure the directory exists and handle errors similarly (e.g.,
unwrap_or_else with a clear panic message). Update the block around output_dir,
OUTPUT_FILE, path, and the File::create call so the directory is created first,
then proceed to File::create(&path) and file.write_all(...).
- Around line 83-85: The percentile index math for p50/p95/p99 is using 1-based
nearest-rank logic on a 0-based vector, biasing results; update the index
computation to use 0-based nearest-rank by computing the 1-based rank =
(percentile * count + 50) / 100, converting to a 0-based index by subtracting 1,
and then clamping with .min(last) and ensuring no underflow (e.g., use
saturating_sub(1) or handle rank==0). Apply this change to the p50/p95/p99
calculations that reference samples, count, and last so indices are computed as
((count * 50 + 50) / 100).saturating_sub(1), ((count * 95 + 50) /
100).saturating_sub(1), and ((count * 99 + 50) / 100).saturating_sub(1) (with
proper casting to usize) before indexing samples.

---

Nitpick comments:
In `@tests/huntsman/tdl-integration/tests/complex.rs`:
- Around line 105-106: Tests currently discard the return value of
manager.load(&path)? and call manager.get(PACKAGE_NAME) immediately after;
change each test to use the PackageRef returned by manager.load(...) directly
(e.g., let pkg = manager.load(&path)? ) instead of calling manager.get(...),
replacing subsequent uses of the retrieved package with that local variable;
update all occurrences mentioned (the blocks around manager.load/get and the
listed line ranges) so they use the load() return value for consistency and to
avoid redundant lookups.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 40925990-1b4d-4733-8af9-8ee05f16d492

📥 Commits

Reviewing files that changed from the base of the PR and between aadb9eb and 25ff527.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • Cargo.toml
  • components/spider-execution-manager/Cargo.toml
  • components/spider-execution-manager/src/lib.rs
  • components/spider-execution-manager/src/process_pool.rs
  • components/spider-task-executor/Cargo.toml
  • components/spider-task-executor/src/bin/spider_task_executor.rs
  • components/spider-task-executor/src/error.rs
  • components/spider-task-executor/src/lib.rs
  • components/spider-task-executor/src/manager.rs
  • components/spider-task-executor/src/protocol.rs
  • taskfiles/test.yaml
  • tests/huntsman/integration-test-tasks/Cargo.toml
  • tests/huntsman/integration-test-tasks/src/lib.rs
  • tests/huntsman/task-executor/Cargo.toml
  • tests/huntsman/task-executor/src/lib.rs
  • tests/huntsman/task-executor/tests/overhead_instrument.rs
  • tests/huntsman/task-executor/tests/test_executor.rs
  • tests/huntsman/task-executor/tests/test_process_pool.rs
  • tests/huntsman/tdl-integration/tests/complex.rs

Comment on lines +8 to +10
//! Package resolution: each `Execute` request names a TDL package; the executor looks for
//! `${SPIDER_TDL_PACKAGE_DIR}/${package}/${package}.so` and caches the loaded library by name.
//!
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix package resolution docs to match the implemented filename.

Line 9 says ${package}/${package}.so, but Line 76 resolves lib{package}.so. Keep docs and behaviour aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/spider-task-executor/src/bin/spider_task_executor.rs` around lines
8 - 10, The package resolution docs are inconsistent with the loading behavior:
update the comment that describes the path for an `Execute` request so it
matches the actual filename used by the loader; replace the
`${package}/${package}.so` description with the implemented
`${SPIDER_TDL_PACKAGE_DIR}/${package}/lib{package}.so` (i.e., mention the lib
prefix) so the docstring and the runtime lookup (which loads `lib{package}.so`)
are aligned.

Comment on lines +76 to +77
let path = pkg_dir.join(package).join(format!("lib{package}.so"));
manager.load(&path)?
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate package names before joining filesystem paths.

package is used directly in path construction. Absolute paths or traversal components can escape SPIDER_TDL_PACKAGE_DIR and load unintended shared libraries.

Suggested fix
 use std::{
-    path::{Path, PathBuf},
+    path::{Component, Path, PathBuf},
     time::Instant,
 };
@@
 fn run_task(
@@
 ) -> Result<Vec<u8>, ExecutorError> {
+    let package_path = Path::new(package);
+    if package_path.is_absolute()
+        || package_path
+            .components()
+            .any(|c| !matches!(c, Component::Normal(_)))
+    {
+        return Err(ExecutorError::InvalidLibrary(format!(
+            "invalid package name: {package}"
+        )));
+    }
+
     let pkg = if let Some(pkg) = manager.get(package) {
         pkg
     } else {
         let path = pkg_dir.join(package).join(format!("lib{package}.so"));
         manager.load(&path)?

Comment on lines +83 to +85
let p50 = samples[(count / 2).min(last)].as_secs_f64() * 1_000_000.0;
let p95 = samples[(count * 95 / 100).min(last)].as_secs_f64() * 1_000_000.0;
let p99 = samples[(count * 99 / 100).min(last)].as_secs_f64() * 1_000_000.0;
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix percentile index math for 0-based sampling.

Current p50/p95/p99 indexes are biased because they don’t use a 0-based nearest-rank conversion. This skews reported overhead stats.

Proposed patch
-        let p50 = samples[(count / 2).min(last)].as_secs_f64() * 1_000_000.0;
-        let p95 = samples[(count * 95 / 100).min(last)].as_secs_f64() * 1_000_000.0;
-        let p99 = samples[(count * 99 / 100).min(last)].as_secs_f64() * 1_000_000.0;
+        let pct_idx = |pct: usize| (((count * pct) + 99) / 100).saturating_sub(1).min(last);
+        let p50 = samples[pct_idx(50)].as_secs_f64() * 1_000_000.0;
+        let p95 = samples[pct_idx(95)].as_secs_f64() * 1_000_000.0;
+        let p99 = samples[pct_idx(99)].as_secs_f64() * 1_000_000.0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let p50 = samples[(count / 2).min(last)].as_secs_f64() * 1_000_000.0;
let p95 = samples[(count * 95 / 100).min(last)].as_secs_f64() * 1_000_000.0;
let p99 = samples[(count * 99 / 100).min(last)].as_secs_f64() * 1_000_000.0;
let pct_idx = |pct: usize| (((count * pct) + 99) / 100).saturating_sub(1).min(last);
let p50 = samples[pct_idx(50)].as_secs_f64() * 1_000_000.0;
let p95 = samples[pct_idx(95)].as_secs_f64() * 1_000_000.0;
let p99 = samples[pct_idx(99)].as_secs_f64() * 1_000_000.0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/task-executor/tests/overhead_instrument.rs` around lines 83 -
85, The percentile index math for p50/p95/p99 is using 1-based nearest-rank
logic on a 0-based vector, biasing results; update the index computation to use
0-based nearest-rank by computing the 1-based rank = (percentile * count + 50) /
100, converting to a 0-based index by subtracting 1, and then clamping with
.min(last) and ensuring no underflow (e.g., use saturating_sub(1) or handle
rank==0). Apply this change to the p50/p95/p99 calculations that reference
samples, count, and last so indices are computed as ((count * 50 + 50) /
100).saturating_sub(1), ((count * 95 + 50) / 100).saturating_sub(1), and ((count
* 99 + 50) / 100).saturating_sub(1) (with proper casting to usize) before
indexing samples.

Comment on lines +177 to +180
let path = output_dir.join(OUTPUT_FILE);
let mut file =
File::create(&path).unwrap_or_else(|err| panic!("create {} failed: {err}", path.display()));
file.write_all(preamble.as_bytes()).expect("write preamble");
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Create the output directory before writing the markdown file.

If SPIDER_TEST_INSTRUMENT_OUTPUT_DIR is set but not pre-created, file creation fails and the benchmark test aborts unnecessarily.

Proposed patch
     let path = output_dir.join(OUTPUT_FILE);
+    std::fs::create_dir_all(&output_dir)
+        .unwrap_or_else(|err| panic!("create {} failed: {err}", output_dir.display()));
     let mut file =
         File::create(&path).unwrap_or_else(|err| panic!("create {} failed: {err}", path.display()));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let path = output_dir.join(OUTPUT_FILE);
let mut file =
File::create(&path).unwrap_or_else(|err| panic!("create {} failed: {err}", path.display()));
file.write_all(preamble.as_bytes()).expect("write preamble");
let path = output_dir.join(OUTPUT_FILE);
std::fs::create_dir_all(&output_dir)
.unwrap_or_else(|err| panic!("create {} failed: {err}", output_dir.display()));
let mut file =
File::create(&path).unwrap_or_else(|err| panic!("create {} failed: {err}", path.display()));
file.write_all(preamble.as_bytes()).expect("write preamble");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/task-executor/tests/overhead_instrument.rs` around lines 177 -
180, The test fails when SPIDER_TEST_INSTRUMENT_OUTPUT_DIR isn't pre-created
because the code calls File::create on path immediately; before creating the
file, call std::fs::create_dir_all(&output_dir) (or equivalent) to ensure the
directory exists and handle errors similarly (e.g., unwrap_or_else with a clear
panic message). Update the block around output_dir, OUTPUT_FILE, path, and the
File::create call so the directory is created first, then proceed to
File::create(&path) and file.write_all(...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant