swactor/crates/process/tests/process_resources.rs

289 lines
8.9 KiB
Rust
Raw Normal View History

#![cfg(target_os = "linux")]
use std::collections::HashMap;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::time::{Duration, Instant};
use swactor::actor::{ActorInterface, Ctx};
use swactor::runtime::{Inbox, Runtime, RuntimeConfig, RuntimeParts, SingleThreadRuntime};
use swactor_process::{
ExitStatus, ProcessOutput, ProcessOutputConfig, ProcessResourceError, ProcessSpawnResources,
ProcessSpec, spawn_local_process, spawn_local_process_with_resources,
};
const CHILD_BOOTSTRAP_FD: RawFd = 198;
struct SpawnOnce {
spec: Option<ProcessSpec>,
resources: Option<ProcessSpawnResources>,
output: Option<ProcessOutputConfig>,
sender: swactor::runtime::ExternalSender,
}
impl ActorInterface for SpawnOnce {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx<'_>) {
spawn_local_process_with_resources(
ctx,
&self.sender,
self.spec.take().expect("process spec"),
self.resources.take().expect("process resources"),
self.output.take().expect("process output"),
)
.expect("spawn process actor");
ctx.stop_self();
}
fn handle(&mut self, _ctx: &Ctx<'_>, _message: ()) {}
}
struct SpawnNativeOnce {
spec: Option<ProcessSpec>,
output: Option<ProcessOutputConfig>,
sender: swactor::runtime::ExternalSender,
}
impl ActorInterface for SpawnNativeOnce {
type Incoming = ();
type Response = ();
fn on_start(&mut self, ctx: &Ctx<'_>) {
spawn_local_process(
ctx,
&self.sender,
self.spec.take().expect("process spec"),
self.output.take().expect("process output"),
)
.expect("spawn native process actor");
ctx.stop_self();
}
fn handle(&mut self, _ctx: &Ctx<'_>, _message: ()) {}
}
fn seqpacket_pair() -> (OwnedFd, OwnedFd) {
let mut descriptors = [-1; 2];
// SAFETY: storage contains two descriptor slots and successful socketpair
// initializes both with uniquely owned descriptors.
let result = unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_SEQPACKET | libc::SOCK_CLOEXEC,
0,
descriptors.as_mut_ptr(),
)
};
assert_eq!(result, 0, "socketpair: {}", io::Error::last_os_error());
// SAFETY: successful socketpair returned two fresh descriptors.
unsafe {
(
OwnedFd::from_raw_fd(descriptors[0]),
OwnedFd::from_raw_fd(descriptors[1]),
)
}
}
fn recv_packet(fd: RawFd) -> Vec<u8> {
let mut bytes = [0_u8; 64];
// SAFETY: bytes is writable and fd is a connected seqpacket endpoint.
let length = unsafe {
libc::recv(
fd,
bytes.as_mut_ptr().cast(),
bytes.len(),
libc::MSG_CMSG_CLOEXEC,
)
};
assert!(length >= 0, "recv: {}", io::Error::last_os_error());
bytes[..length as usize].to_vec()
}
fn runtime_host() -> (Runtime, SingleThreadRuntime) {
let parts = RuntimeParts::new(RuntimeConfig::default());
let runtime = parts.runtime().clone();
let host = SingleThreadRuntime::new(parts);
(runtime, host)
}
fn python_spec(script: &str, args: Vec<String>, label: &str) -> ProcessSpec {
let mut command_args = vec!["-c".to_owned(), script.to_owned()];
command_args.extend(args);
ProcessSpec {
command: "python3".to_owned(),
args: command_args,
env: HashMap::new(),
working_dir: None,
label: Some(label.to_owned()),
}
}
fn spawn_once(
runtime: &Runtime,
spec: ProcessSpec,
resources: ProcessSpawnResources,
output: &Inbox<ProcessOutput>,
) {
runtime
.spawn(SpawnOnce {
spec: Some(spec),
resources: Some(resources),
output: Some(ProcessOutputConfig::disabled(*output.addr())),
sender: runtime.create_sender(),
})
.expect("spawn resource test actor");
}
fn drive_until_exits(
host: &mut SingleThreadRuntime,
output: &Inbox<ProcessOutput>,
expected: usize,
) -> Vec<ProcessOutput> {
let deadline = Instant::now() + Duration::from_secs(5);
let mut observed = Vec::new();
while Instant::now() < deadline {
host.tick();
while let Some(event) = output.try_recv() {
observed.push(event);
}
if observed
.iter()
.filter(|event| matches!(event, ProcessOutput::Exited { .. }))
.count()
== expected
{
return observed;
}
std::thread::yield_now();
}
panic!("processes did not exit: {observed:?}");
}
#[test]
fn descriptor_mapping_survives_exec_and_source_stays_cloexec() {
let (host_endpoint, child_endpoint) = seqpacket_pair();
let source_fd = child_endpoint.as_raw_fd();
let mut resources = ProcessSpawnResources::new();
resources
.add_descriptor(child_endpoint, CHILD_BOOTSTRAP_FD)
.expect("resource mapping");
// SAFETY: F_GETFD only reads flags for a live descriptor.
let flags = unsafe { libc::fcntl(source_fd, libc::F_GETFD) };
assert_ne!(flags & libc::FD_CLOEXEC, 0);
let (runtime, mut runtime_host) = runtime_host();
let output = runtime.new_inbox::<ProcessOutput>().expect("output inbox");
spawn_once(
&runtime,
python_spec(
"import os,sys\nsource=int(sys.argv[1])\ntry:\n os.fstat(source)\nexcept OSError:\n os.write(198,b'mapped')\nelse:\n raise SystemExit(3)",
vec![source_fd.to_string()],
"resource-owned",
),
resources,
&output,
);
let observed = drive_until_exits(&mut runtime_host, &output, 1);
assert!(
observed
.iter()
.any(|event| matches!(event, ProcessOutput::Started { .. }))
);
assert_eq!(recv_packet(host_endpoint.as_raw_fd()), b"mapped");
}
#[test]
fn concurrent_children_receive_only_their_own_descriptor() {
let (host_a, child_a) = seqpacket_pair();
let (host_b, child_b) = seqpacket_pair();
let source_a = child_a.as_raw_fd();
let source_b = child_b.as_raw_fd();
let mut resources_a = ProcessSpawnResources::new();
resources_a
.add_descriptor(child_a, CHILD_BOOTSTRAP_FD)
.expect("resource A");
let mut resources_b = ProcessSpawnResources::new();
resources_b
.add_descriptor(child_b, CHILD_BOOTSTRAP_FD)
.expect("resource B");
let script = "import os,sys\nfor value in sys.argv[1:3]:\n try:\n os.fstat(int(value))\n except OSError:\n pass\n else:\n raise SystemExit(3)\nos.write(198,sys.argv[3].encode())";
let (runtime, mut runtime_host) = runtime_host();
let output = runtime.new_inbox::<ProcessOutput>().expect("output inbox");
spawn_once(
&runtime,
python_spec(
script,
vec![source_a.to_string(), source_b.to_string(), "A".to_owned()],
"resource-a",
),
resources_a,
&output,
);
spawn_once(
&runtime,
python_spec(
script,
vec![source_a.to_string(), source_b.to_string(), "B".to_owned()],
"resource-b",
),
resources_b,
&output,
);
let _ = drive_until_exits(&mut runtime_host, &output, 2);
assert_eq!(recv_packet(host_a.as_raw_fd()), b"A");
assert_eq!(recv_packet(host_b.as_raw_fd()), b"B");
}
#[test]
fn native_spawn_receives_no_contextual_descriptor() {
let (runtime, mut runtime_host) = runtime_host();
let output = runtime.new_inbox::<ProcessOutput>().expect("output inbox");
runtime
.spawn(SpawnNativeOnce {
spec: Some(python_spec(
"import os\ntry:\n os.fstat(198)\nexcept OSError:\n raise SystemExit(0)\nraise SystemExit(3)",
Vec::new(),
"native-no-context",
)),
output: Some(ProcessOutputConfig::disabled(*output.addr())),
sender: runtime.create_sender(),
})
.expect("spawn native test actor");
let observed = drive_until_exits(&mut runtime_host, &output, 1);
assert!(observed.iter().any(|event| {
matches!(
event,
ProcessOutput::Exited {
status: ExitStatus::Code(0)
}
)
}));
}
#[test]
fn duplicate_and_standard_child_targets_are_rejected() {
let (_, first) = seqpacket_pair();
let (_, second) = seqpacket_pair();
let (_, standard) = seqpacket_pair();
let mut resources = ProcessSpawnResources::new();
resources
.add_descriptor(first, CHILD_BOOTSTRAP_FD)
.expect("first target");
assert!(matches!(
resources.add_descriptor(second, CHILD_BOOTSTRAP_FD),
Err(ProcessResourceError::DuplicateChildDescriptor(
CHILD_BOOTSTRAP_FD
))
));
assert!(matches!(
resources.add_descriptor(standard, 2),
Err(ProcessResourceError::InvalidChildDescriptor(2))
));
}