Summary
CommandWrap::get_wrap::<W>() — the documented mechanism for one wrapper to read another's state during pre_spawn ("that's how CreationFlags on Windows works along with JobObject") — does not work. There are two distinct defects, both present in 9.1.0 (and 8.2.x):
get_wrap returns None during pre_spawn/post_spawn/wrap_child. spawn_with does let mut wrappers = std::mem::take(&mut self.wrappers); and then runs spawn_inner(.., &mut wrappers, ..), which calls wrapper.pre_spawn(command, self). But self.wrappers was just emptied by the mem::take, so any core.get_wrap::<_>() / core.has_wrap::<_>() inside a hook sees an empty map.
get_wrap panics on a present wrapper. It does let w_any = w as &dyn Any; where w: &Box<dyn CommandWrapper>, so the &dyn Any's concrete type is Box<dyn CommandWrapper>, not the inner W — downcast_ref::<W>() returns None and the .expect("downcasting is guaranteed to succeed due to wrap()'s internals") panics.
Because of (1), JobObject::pre_spawn's if let Some(CreationFlags(user_flags)) = core.get_wrap::<CreationFlags>() never matches, so it calls command.creation_flags(CREATE_SUSPENDED) and silently drops the user's creation flags (e.g. CREATE_NEW_PROCESS_GROUP). The "CreationFlags must come first" recipe in the JobObject docs therefore does not actually preserve those flags.
Minimal repro (platform-agnostic, process-wrap 9.1.0)
use process_wrap::tokio::{CommandWrap, CommandWrapper};
use std::io::Result;
use tokio::process::Command;
#[derive(Debug)]
struct Marker(u32);
impl CommandWrapper for Marker {}
#[derive(Debug)]
struct Reader;
impl CommandWrapper for Reader {
fn pre_spawn(&mut self, _cmd: &mut Command, core: &CommandWrap) -> Result<()> {
println!("inside pre_spawn: has_wrap::<Marker>() = {}", core.has_wrap::<Marker>());
println!("inside pre_spawn: get_wrap::<Marker>() = {:?}", core.get_wrap::<Marker>().map(|m| m.0));
Ok(())
}
}
#[tokio::main]
async fn main() {
let mut cmd = CommandWrap::from(Command::new("true"));
cmd.wrap(Marker(42));
cmd.wrap(Reader);
let mut child = cmd.spawn().expect("spawn");
let _ = child.wait().await;
// get_wrap on a present wrapper (outside a hook, map restored) panics:
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cmd.get_wrap::<Marker>().map(|m| m.0)));
println!("get_wrap outside hook: {}", if r.is_err() { "PANICKED" } else { "ok" });
}
Output:
inside pre_spawn: has_wrap::<Marker>() = false
inside pre_spawn: get_wrap::<Marker>() = None
get_wrap outside hook: PANICKED // "downcasting is guaranteed to succeed due to wrap()'s internals"
Both wrappers are registered, yet the hook sees Marker as absent (defect 1); and reading it outside a hook panics (defect 2).
Real-world impact
On Windows, combining CreationFlags(CREATE_NEW_PROCESS_GROUP) with JobObject (the documented composition) drops the CREATE_NEW_PROCESS_GROUP flag, so the child stays in the parent's console process group and is killed by CTRL_BREAK_EVENT/CTRL_C_EVENT sent to that group. I hit this isolating worker-job children from a console signal and had to stop using the JobObject wrap (assigning the job object post-spawn instead) to keep the flag.
Suggested fixes
- Run the hooks against the live map — e.g. pass a
&self/core that still holds wrappers, or look the wrapper up in the working wrappers map rather than self.wrappers, during spawn_inner.
- In
get_wrap, downcast the inner value: cast &**w (the &dyn CommandWrapper) — via an as_any() on the trait — instead of w (the &Box<…>).
Versions
- process-wrap 9.1.0 (also reproduces on 8.2.x)
- rustc stable, Linux (defects are in
generic_wrap.rs, platform-agnostic)
Summary
CommandWrap::get_wrap::<W>()— the documented mechanism for one wrapper to read another's state duringpre_spawn("that's howCreationFlagson Windows works along withJobObject") — does not work. There are two distinct defects, both present in 9.1.0 (and 8.2.x):get_wrapreturnsNoneduringpre_spawn/post_spawn/wrap_child.spawn_withdoeslet mut wrappers = std::mem::take(&mut self.wrappers);and then runsspawn_inner(.., &mut wrappers, ..), which callswrapper.pre_spawn(command, self). Butself.wrapperswas just emptied by themem::take, so anycore.get_wrap::<_>()/core.has_wrap::<_>()inside a hook sees an empty map.get_wrappanics on a present wrapper. It doeslet w_any = w as &dyn Any;wherew: &Box<dyn CommandWrapper>, so the&dyn Any's concrete type isBox<dyn CommandWrapper>, not the innerW—downcast_ref::<W>()returnsNoneand the.expect("downcasting is guaranteed to succeed due to wrap()'s internals")panics.Because of (1),
JobObject::pre_spawn'sif let Some(CreationFlags(user_flags)) = core.get_wrap::<CreationFlags>()never matches, so it callscommand.creation_flags(CREATE_SUSPENDED)and silently drops the user's creation flags (e.g.CREATE_NEW_PROCESS_GROUP). The "CreationFlagsmust come first" recipe in theJobObjectdocs therefore does not actually preserve those flags.Minimal repro (platform-agnostic, process-wrap 9.1.0)
Output:
Both wrappers are registered, yet the hook sees
Markeras absent (defect 1); and reading it outside a hook panics (defect 2).Real-world impact
On Windows, combining
CreationFlags(CREATE_NEW_PROCESS_GROUP)withJobObject(the documented composition) drops theCREATE_NEW_PROCESS_GROUPflag, so the child stays in the parent's console process group and is killed byCTRL_BREAK_EVENT/CTRL_C_EVENTsent to that group. I hit this isolating worker-job children from a console signal and had to stop using theJobObjectwrap (assigning the job object post-spawn instead) to keep the flag.Suggested fixes
&self/core that still holdswrappers, or look the wrapper up in the workingwrappersmap rather thanself.wrappers, duringspawn_inner.get_wrap, downcast the inner value: cast&**w(the&dyn CommandWrapper) — via anas_any()on the trait — instead ofw(the&Box<…>).Versions
generic_wrap.rs, platform-agnostic)