mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
191 lines
7.2 KiB
Rust
191 lines
7.2 KiB
Rust
//! A stopped run reports its exit only once the whole process group is gone, so the next
|
|
//! start cannot meet a port the previous process still holds.
|
|
#![cfg(unix)]
|
|
|
|
use ide_core::{IdeEvent, RunId, SharedSink};
|
|
use ide_run::{ProcessManager, RunConfig, StartOptions};
|
|
use parking_lot::Mutex;
|
|
use std::collections::BTreeMap;
|
|
use std::sync::Arc;
|
|
use std::sync::mpsc::{self, Receiver};
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn channel_sink() -> (SharedSink, Receiver<IdeEvent>) {
|
|
let (tx, events) = mpsc::channel();
|
|
let tx = Mutex::new(tx);
|
|
let sink: SharedSink = Arc::new(move |e: IdeEvent| {
|
|
let _ = tx.lock().send(e);
|
|
});
|
|
(sink, events)
|
|
}
|
|
|
|
fn wait_exit(events: &Receiver<IdeEvent>, id: RunId, timeout: Duration) -> Option<i32> {
|
|
let deadline = Instant::now() + timeout;
|
|
loop {
|
|
let left = deadline.saturating_duration_since(Instant::now());
|
|
match events.recv_timeout(left).expect("RunExited event") {
|
|
IdeEvent::RunExited { run, code } if run == id => return code,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn wait_for(what: &str, mut ready: impl FnMut() -> bool) {
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while !ready() {
|
|
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
|
|
fn assert_still_running(events: &Receiver<IdeEvent>, timeout: Duration) {
|
|
let event = events.recv_timeout(timeout);
|
|
assert!(
|
|
matches!(event, Err(mpsc::RecvTimeoutError::Timeout)),
|
|
"run exited before shutdown was released: {event:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_stopped_run_keeps_its_port_until_the_group_is_gone() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (sink, events) = channel_sink();
|
|
let pm = ProcessManager::new();
|
|
// The `mvn`-starts-a-JVM shape: the direct child dies on SIGINT while the process it
|
|
// started ignores it and keeps its socket open until the test releases it.
|
|
let script = tmp.path().join("hold.py");
|
|
std::fs::write(
|
|
&script,
|
|
"import pathlib, signal, socket, sys, time\n\
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)\n\
|
|
s = socket.socket(); s.bind(('127.0.0.1', 0)); s.listen()\n\
|
|
open(sys.argv[1], 'w').write(str(s.getsockname()[1]))\n\
|
|
while not pathlib.Path(sys.argv[2]).exists(): time.sleep(0.01)\n",
|
|
)
|
|
.unwrap();
|
|
let port_file = tmp.path().join("port");
|
|
let release_file = tmp.path().join("release");
|
|
let config = RunConfig {
|
|
id: "boot".into(),
|
|
name: "boot".into(),
|
|
kind: "shell".into(),
|
|
command: "/bin/sh".into(),
|
|
args: vec![
|
|
"-c".into(),
|
|
"python3 \"$1\" \"$2\" \"$3\" & sleep 60".into(),
|
|
"sh".into(),
|
|
script.display().to_string(),
|
|
port_file.display().to_string(),
|
|
release_file.display().to_string(),
|
|
],
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: false,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
};
|
|
let info = pm.start(tmp.path(), &config, &StartOptions::default(), sink, Box::new(|_| {})).expect("start");
|
|
wait_for("the listening port", || {
|
|
std::fs::read_to_string(&port_file).ok().is_some_and(|text| text.trim().parse::<u16>().is_ok())
|
|
});
|
|
let port: u16 = std::fs::read_to_string(&port_file).expect("port").trim().parse().unwrap();
|
|
|
|
pm.stop(info.id).unwrap();
|
|
let pid = info.pid.expect("child pid") as libc::pid_t;
|
|
wait_for("the direct child to exit", || unsafe { libc::kill(pid, 0) != 0 });
|
|
let exit = events.recv_timeout(Duration::from_secs(1));
|
|
let port_held = std::net::TcpStream::connect(("127.0.0.1", port)).is_ok();
|
|
std::fs::write(&release_file, "").unwrap();
|
|
assert!(
|
|
matches!(exit, Err(mpsc::RecvTimeoutError::Timeout)),
|
|
"run exited while its descendant held the port: {exit:?}"
|
|
);
|
|
assert!(port_held, "the descendant still holds its port");
|
|
let code = wait_exit(&events, info.id, Duration::from_secs(20));
|
|
assert!(code.is_some(), "the run reports its exit");
|
|
assert!(
|
|
std::net::TcpStream::connect(("127.0.0.1", port)).is_err(),
|
|
"the port is still bound when the run reports finished"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_first_stop_does_not_kill_a_program_that_is_still_shutting_down() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (sink, events) = channel_sink();
|
|
let pm = ProcessManager::new();
|
|
// Ignores SIGINT until stdin releases the shutdown, like a slow Spring Boot shutdown.
|
|
// A first Stop must wait even past the grace period instead of killing it (exit code 137).
|
|
let script = tmp.path().join("slow.py");
|
|
let ready_file = tmp.path().join("ready");
|
|
std::fs::write(
|
|
&script,
|
|
"import pathlib, signal, sys\n\
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)\n\
|
|
pathlib.Path(sys.argv[1]).touch()\n\
|
|
sys.stdin.readline()\n",
|
|
)
|
|
.unwrap();
|
|
let config = RunConfig {
|
|
id: "slow".into(),
|
|
name: "slow".into(),
|
|
kind: "shell".into(),
|
|
command: "python3".into(),
|
|
args: vec![script.display().to_string(), ready_file.display().to_string()],
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: false,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
};
|
|
let info = pm.start(tmp.path(), &config, &StartOptions::default(), sink, Box::new(|_| {})).expect("start");
|
|
wait_for("the SIGINT handler", || ready_file.exists());
|
|
pm.stop_with_grace(info.id, Duration::from_millis(100)).unwrap();
|
|
assert_still_running(&events, Duration::from_secs(1));
|
|
pm.write_stdin(info.id, "finish shutdown\n").unwrap();
|
|
let code = wait_exit(&events, info.id, Duration::from_secs(10));
|
|
assert_eq!(code, Some(0), "the program exited by itself, not by SIGKILL");
|
|
}
|
|
|
|
#[test]
|
|
fn a_second_stop_kills_a_program_that_ignores_the_first() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let (sink, events) = channel_sink();
|
|
let pm = ProcessManager::new();
|
|
let script = tmp.path().join("stuck.py");
|
|
let ready_file = tmp.path().join("ready");
|
|
std::fs::write(
|
|
&script,
|
|
"import pathlib, signal, sys\n\
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)\n\
|
|
pathlib.Path(sys.argv[1]).touch()\n\
|
|
sys.stdin.readline()\n",
|
|
)
|
|
.unwrap();
|
|
let config = RunConfig {
|
|
id: "stuck".into(),
|
|
name: "stuck".into(),
|
|
kind: "shell".into(),
|
|
command: "python3".into(),
|
|
args: vec![script.display().to_string(), ready_file.display().to_string()],
|
|
cwd: None,
|
|
env: BTreeMap::new(),
|
|
detected: false,
|
|
is_test: false,
|
|
debug_adapter: None,
|
|
allow_parallel: false,
|
|
extras: Default::default(),
|
|
};
|
|
let info = pm.start(tmp.path(), &config, &StartOptions::default(), sink, Box::new(|_| {})).expect("start");
|
|
wait_for("the SIGINT handler", || ready_file.exists());
|
|
pm.stop(info.id).unwrap();
|
|
assert_still_running(&events, Duration::from_millis(200));
|
|
pm.stop(info.id).unwrap();
|
|
let code = wait_exit(&events, info.id, Duration::from_secs(10));
|
|
assert_eq!(code, Some(137), "the second Stop SIGKILLs");
|
|
}
|