Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/pocket-ic/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ rust_library(
"@crate_index//:ic-certification",
"@crate_index//:ic-management-canister-types",
"@crate_index//:ic-transport-types",
"@crate_index//:libc",
"@crate_index//:reqwest",
"@crate_index//:schemars",
"@crate_index//:semver",
Expand Down Expand Up @@ -153,6 +154,7 @@ rust_test(
"@crate_index//:candid",
"@crate_index//:hex",
"@crate_index//:ic-management-canister-types",
"@crate_index//:libc",
"@crate_index//:maplit",
"@crate_index//:prost",
"@crate_index//:reqwest",
Expand Down
22 changes: 22 additions & 0 deletions packages/pocket-ic/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added
- The type `ServerHandle` (returned by `start_server`) with the methods `pid` (the OS
process id of the spawned server), `kill_and_wait` (kill the server now) and `detach`
(let the server outlive the spawning process, as in earlier `pocket-ic` versions).

### Changed
- Breaking: `start_server` now returns a `ServerHandle` (instead of a
`std::process::Child`). Every PocketIC server spawned by this crate — also implicitly
through `PocketIc`/`PocketIcBuilder` — is registered in a process-global registry and,
on unix, is automatically killed (its whole process group, so canister-sandbox children
are terminated too) and reaped when the spawning process exits normally — instead of
being detached and left to shut down on its TTL. This ensures the server no longer
keeps the spawning process's inherited stdout/stderr pipes open past process exit,
which caused issues on some CI runners. The cleanup does not run if the process
terminates abnormally (e.g. it is killed by a signal or aborts); the server then still
shuts down on its TTL, as before. On Windows nothing changes: the server is never
killed automatically and shuts down on its TTL. Migration: callers that previously kept
the `Child` to terminate the server themselves should call
`ServerHandle::kill_and_wait`; callers that used `Child::id` should use
`ServerHandle::pid`; callers that need the server to outlive the spawning process
should call `ServerHandle::detach`.



## 15.0.0 - 2026-06-26
Expand Down
3 changes: 3 additions & 0 deletions packages/pocket-ic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ tracing = { workspace = true }
tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }

[target.'cfg(unix)'.dependencies]
libc = { workspace = true }

[target.'cfg(windows)'.dependencies]
wslpath = "0.0.2"

Expand Down
25 changes: 20 additions & 5 deletions packages/pocket-ic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ use std::{
fs::OpenOptions,
net::{IpAddr, SocketAddr},
path::PathBuf,
process::{Child, Command},
process::Command,
sync::{Arc, mpsc::channel},
thread,
thread::JoinHandle,
Expand All @@ -101,6 +101,9 @@ use wslpath::windows_to_wsl;

pub mod common;
pub mod nonblocking;
mod server_process;

pub use server_process::ServerHandle;

const POCKET_IC_SERVER_NAME: &str = "pocket-ic-server";

Expand Down Expand Up @@ -2280,7 +2283,17 @@ pub struct StartServerParams {
}

/// Attempt to start a new PocketIC server.
pub async fn start_server(params: StartServerParams) -> (Child, Url) {
///
/// The spawned server process is owned by a process-global registry; the returned
/// [`ServerHandle`] only refers to it (dropping the handle does not stop the server). On
/// unix, once the spawning process exits normally, the server is killed (its whole
/// process group, so the canister-sandbox children are terminated too) and reaped on a
/// bounded, best-effort basis; if the process terminates abnormally (e.g. it is killed by
/// a signal or aborts), the server is not cleaned up and shuts down on its TTL instead.
/// On other platforms the server is never killed automatically and always shuts down on
/// its TTL. Callers can terminate the server earlier with [`ServerHandle::kill_and_wait`]
/// or let it outlive this process with [`ServerHandle::detach`].
pub async fn start_server(params: StartServerParams) -> (ServerHandle, Url) {
let default_bin_dir =
std::env::temp_dir().join(format!("{POCKET_IC_SERVER_NAME}-{LATEST_SERVER_VERSION}"));
let default_bin_path = default_bin_dir.join("pocket-ic");
Expand Down Expand Up @@ -2389,11 +2402,13 @@ pub async fn start_server(params: StartServerParams) -> (Child, Url) {
cmd.process_group(0);
}

// TODO: SDK-1936
#[allow(clippy::zombie_processes)]
let child = cmd
.spawn()
.unwrap_or_else(|_| panic!("Failed to start PocketIC binary ({})", bin_path.display()));
// Transfer ownership of the server process to the process-global registry, which
// kills and reaps it (and its sandbox children) once the process exits normally.
// See `server_process`.
let server_handle = server_process::register(child, port_file_path.clone());

loop {
if let Ok(port_string) = std::fs::read_to_string(port_file_path.clone())
Expand All @@ -2404,7 +2419,7 @@ pub async fn start_server(params: StartServerParams) -> (Child, Url) {
.parse()
.expect("Failed to parse port to number");
break (
child,
server_handle,
Url::parse(&format!("http://{LOCALHOST}:{port}/")).unwrap(),
);
}
Expand Down
Loading
Loading