use std::net::IpAddr;
use std::sync::Arc;
use crate::{ContainerAsync, Image, core::error::Result};
fn is_reentering_shared_runtime(runtime: &tokio::runtime::Runtime) -> bool {
matches!(
tokio::runtime::Handle::try_current(),
Ok(current) if current.id() == runtime.handle().id()
)
}
pub(crate) fn block_on_runtime<F>(runtime: &tokio::runtime::Runtime, f: F) -> Result<F::Output>
where
F: std::future::Future + Send,
F::Output: Send,
{
if is_reentering_shared_runtime(runtime) {
return Err(crate::Error::other(
"cannot call sync API from within the shared runtime context (LogConsumer callback or async context): this would deadlock",
));
}
match tokio::runtime::Handle::try_current() {
Ok(_) => {
Ok(std::thread::scope(|s| {
s.spawn(|| runtime.block_on(f))
.join()
.expect("thread executing the future panicked; re-raising here")
}))
}
Err(_) => Ok(runtime.block_on(f)),
}
}
pub(crate) fn drop_shared_runtime(runtime: Arc<tokio::runtime::Runtime>) {
match tokio::runtime::Handle::try_current() {
Ok(_) => {
std::thread::spawn(move || drop(runtime))
.join()
.expect("thread dropping the shared runtime panicked");
}
Err(_) => drop(runtime),
}
}
struct ReentryErrorReader;
impl std::io::Read for ReentryErrorReader {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(reentry_io_error())
}
}
impl std::io::BufRead for ReentryErrorReader {
fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
Err(reentry_io_error())
}
fn consume(&mut self, _amt: usize) {}
}
fn reentry_io_error() -> std::io::Error {
std::io::Error::other(
"cannot read sync log reader from within the shared runtime context \
(LogConsumer callback or async context): this would deadlock",
)
}
pub struct Container<I: Image> {
runtime: Option<Arc<tokio::runtime::Runtime>>,
inner: Option<ContainerAsync<I>>,
}
impl<I: Image> Container<I> {
pub(crate) fn new(runtime: Arc<tokio::runtime::Runtime>, inner: ContainerAsync<I>) -> Self {
Self {
runtime: Some(runtime),
inner: Some(inner),
}
}
fn runtime(&self) -> &tokio::runtime::Runtime {
self.runtime
.as_ref()
.expect("runtime is only taken in Drop")
}
fn inner(&self) -> &ContainerAsync<I> {
self.inner.as_ref().expect("container already removed")
}
pub fn id(&self) -> &str {
self.inner().id()
}
pub fn image(&self) -> &I {
self.inner().image()
}
pub fn stop(&self) -> Result<()> {
self.stop_with_timeout(None)
}
pub fn stop_with_timeout(&self, timeout_seconds: Option<i32>) -> Result<()> {
block_on_runtime(
self.runtime(),
self.inner().stop_with_timeout(timeout_seconds),
)?
}
pub fn is_running(&self) -> Result<bool> {
block_on_runtime(self.runtime(), self.inner().is_running())?
}
pub fn exit_code(&self) -> Result<Option<i64>> {
block_on_runtime(self.runtime(), self.inner().exit_code())?
}
pub fn rm(mut self) -> Result<()> {
if let Some(inner) = self.inner.take() {
block_on_runtime(self.runtime(), inner.rm())??;
}
Ok(())
}
pub fn rm_blocking(mut self) -> Result<()> {
if let Some(inner) = self.inner.take() {
inner.rm_blocking()?;
}
Ok(())
}
pub fn ports(&self) -> Result<crate::core::ports::Ports> {
block_on_runtime(self.runtime(), self.inner().ports())?
}
pub fn get_host_port_ipv4(
&self,
internal_port: impl Into<crate::core::ports::ContainerPort>,
) -> Result<u16> {
let port = internal_port.into();
block_on_runtime(self.runtime(), self.inner().get_host_port_ipv4(port))?
}
pub fn get_host_port_ipv6(
&self,
internal_port: impl Into<crate::core::ports::ContainerPort>,
) -> Result<u16> {
let port = internal_port.into();
block_on_runtime(self.runtime(), self.inner().get_host_port_ipv6(port))?
}
pub fn stdout_to_vec(&self) -> Result<Vec<u8>> {
block_on_runtime(self.runtime(), self.inner().stdout_to_vec())?
}
pub fn stderr_to_vec(&self) -> Result<Vec<u8>> {
block_on_runtime(self.runtime(), self.inner().stderr_to_vec())?
}
pub fn container_state(&self) -> Result<crate::core::image::ContainerState> {
block_on_runtime(self.runtime(), self.inner().container_state())?
}
pub fn stdout(&self, follow: bool) -> Box<dyn std::io::BufRead + Send> {
if is_reentering_shared_runtime(self.runtime()) {
Box::new(ReentryErrorReader)
} else {
self.inner().stdout_sync(follow)
}
}
pub fn stderr(&self, follow: bool) -> Box<dyn std::io::BufRead + Send> {
if is_reentering_shared_runtime(self.runtime()) {
Box::new(ReentryErrorReader)
} else {
self.inner().stderr_sync(follow)
}
}
pub fn start(&self) -> Result<()> {
block_on_runtime(self.runtime(), self.inner().start())?
}
pub fn exec(&self, cmd: crate::core::ExecCommand) -> Result<SyncExecResult> {
match block_on_runtime(self.runtime(), self.inner().exec(cmd)) {
Ok(Ok(inner)) => Ok(SyncExecResult { inner }),
Ok(Err(e)) | Err(e) => Err(e),
}
}
pub fn get_bridge_ip_address(&self) -> Result<IpAddr> {
block_on_runtime(self.runtime(), self.inner().get_bridge_ip_address())?
}
pub fn get_host(&self) -> Result<crate::core::Host> {
block_on_runtime(self.runtime(), self.inner().get_host())?
}
pub fn copy_file_from<T: crate::core::copy::CopyFileFromContainer>(
&self,
source: impl Into<String> + Send,
target: T,
) -> Result<T::Output> {
block_on_runtime(self.runtime(), self.inner().copy_file_from(source, target))?
}
}
impl<I: Image> Drop for Container<I> {
fn drop(&mut self) {
drop(self.inner.take());
if let Some(runtime) = self.runtime.take() {
drop_shared_runtime(runtime);
}
}
}
pub struct SyncExecResult {
inner: crate::core::containers::async_container::exec::ExecResult,
}
impl SyncExecResult {
pub fn exit_code(&self) -> Result<Option<i64>> {
Ok(self.inner.exit_code)
}
pub fn stdout<'b>(&'b mut self) -> Box<dyn std::io::BufRead + Send + 'b> {
Box::new(&mut self.inner.stdout)
}
pub fn stderr<'b>(&'b mut self) -> Box<dyn std::io::BufRead + Send + 'b> {
Box::new(&mut self.inner.stderr)
}
pub fn stdout_to_vec(&mut self) -> Result<Vec<u8>> {
let mut stdout = Vec::new();
std::io::Read::read_to_end(&mut self.stdout(), &mut stdout)?;
Ok(stdout)
}
pub fn stderr_to_vec(&mut self) -> Result<Vec<u8>> {
let mut stderr = Vec::new();
std::io::Read::read_to_end(&mut self.stderr(), &mut stderr)?;
Ok(stderr)
}
}
impl std::fmt::Debug for SyncExecResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SyncExecResult")
.field("exit_code", &self.inner.exit_code)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_runtime() -> Arc<tokio::runtime::Runtime> {
Arc::new(
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("テスト用ランタイムの構築に失敗した"),
)
}
#[test]
fn drop_shared_runtime_outside_async_context() {
let runtime = build_runtime();
drop_shared_runtime(runtime);
}
#[test]
fn drop_shared_runtime_inside_current_thread_block_on() {
let outer = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("外側ランタイムの構築に失敗した");
let inner = build_runtime();
outer.block_on(async move {
drop_shared_runtime(inner);
});
}
#[test]
fn drop_shared_runtime_inside_multi_thread_block_on() {
let outer = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("外側ランタイムの構築に失敗した");
let inner = build_runtime();
outer.block_on(async move {
drop_shared_runtime(inner);
});
}
#[test]
fn drop_shared_runtime_non_final_drop_keeps_runtime_usable() {
let runtime = build_runtime();
let kept = runtime.clone();
let outer = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("外側ランタイムの構築に失敗した");
outer.block_on(async move {
drop_shared_runtime(runtime);
});
let value = kept.block_on(async { 42 });
assert_eq!(value, 42, "非最終 drop 後もランタイムが使えること");
}
#[test]
fn block_on_runtime_without_existing_runtime_runs_on_caller_thread() {
let runtime = build_runtime();
let caller = std::thread::current().id();
let executed = block_on_runtime(&runtime, async move { std::thread::current().id() })
.expect("block_on_runtime が成功すること");
assert_eq!(
executed, caller,
"既存ランタイムが無い場合は呼び出しスレッド上で future が実行されること"
);
}
#[test]
fn block_on_runtime_inside_current_thread_runtime_runs_on_another_thread() {
let outer = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("外側ランタイムの構築に失敗した");
let runtime = build_runtime();
let caller = std::thread::current().id();
let executed = outer.block_on(async {
block_on_runtime(&runtime, async move { std::thread::current().id() })
.expect("block_on_runtime が成功すること")
});
assert_ne!(
executed, caller,
"current_thread ランタイム内から呼ばれた場合は別スレッドで future が実行されること"
);
}
#[test]
fn block_on_runtime_inside_multi_thread_runtime_runs_on_another_thread() {
let outer = tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("外側ランタイムの構築に失敗した");
let runtime = build_runtime();
let caller = std::thread::current().id();
let executed = outer.block_on(async {
block_on_runtime(&runtime, async move { std::thread::current().id() })
.expect("block_on_runtime が成功すること")
});
assert_ne!(
executed, caller,
"multi_thread ランタイム内から呼ばれた場合は別スレッドで future が実行されること"
);
}
#[test]
fn block_on_runtime_detects_reentry_into_same_runtime() {
let runtime = build_runtime();
let result = runtime.block_on(async { block_on_runtime(&runtime, async { 42 }) });
assert!(
result.is_err(),
"同一 Runtime への再入は fail-fast で Err になること"
);
}
#[test]
fn reentry_error_reader_errors_on_all_read_paths() {
use std::io::{BufRead, Read};
let mut reader = ReentryErrorReader;
let mut buf = [0u8; 16];
let err = reader.read(&mut buf).expect_err("read がエラーになること");
assert!(
err.to_string().contains("cannot read sync log reader"),
"再入検出エラーであること: {err}"
);
let mut reader = ReentryErrorReader;
let err = reader.fill_buf().expect_err("fill_buf がエラーになること");
assert!(
err.to_string().contains("cannot read sync log reader"),
"再入検出エラーであること: {err}"
);
let mut reader = ReentryErrorReader;
let mut line = String::new();
let err = reader
.read_line(&mut line)
.expect_err("read_line がエラーになること");
assert!(
err.to_string().contains("cannot read sync log reader"),
"再入検出エラーであること: {err}"
);
}
#[test]
fn is_reentering_shared_runtime_detects_worker_context() {
let runtime = build_runtime();
assert!(
!is_reentering_shared_runtime(&runtime),
"ランタイム外では false であること"
);
let result = runtime.block_on(async { is_reentering_shared_runtime(&runtime) });
assert!(result, "同一ランタイムの block_on 中は true であること");
}
}