mirror of
https://github.com/nushell/nushell
synced 2024-11-14 08:57:08 +00:00
Fix (and test) for a deadlock that can happen while waiting for protocol info (#12633)
# Description The local socket PR introduced a `Waitable` type, which could either hold a value or be waited on until a value is available. Unlike a channel, it would always return that value once set. However, one issue with this design was that there was no way to detect whether a value would ever be written. This splits the writer into a different type `WaitableMut`, so that when it is dropped, waiting threads can fail (because they'll never get a value). # Tests + Formatting A test has been added to `stress_internals` to make sure this fails in the right way. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib`
This commit is contained in:
parent
0f645b3bb6
commit
c52884b3c8
8 changed files with 184 additions and 57 deletions
|
@ -10,7 +10,7 @@ use crate::{
|
||||||
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
||||||
PluginOutput, ProtocolInfo,
|
PluginOutput, ProtocolInfo,
|
||||||
},
|
},
|
||||||
util::Waitable,
|
util::{Waitable, WaitableMut},
|
||||||
};
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData,
|
engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData,
|
||||||
|
@ -85,6 +85,8 @@ impl std::fmt::Debug for EngineInterfaceState {
|
||||||
pub struct EngineInterfaceManager {
|
pub struct EngineInterfaceManager {
|
||||||
/// Shared state
|
/// Shared state
|
||||||
state: Arc<EngineInterfaceState>,
|
state: Arc<EngineInterfaceState>,
|
||||||
|
/// The writer for protocol info
|
||||||
|
protocol_info_mut: WaitableMut<Arc<ProtocolInfo>>,
|
||||||
/// Channel to send received PluginCalls to. This is removed after `Goodbye` is received.
|
/// Channel to send received PluginCalls to. This is removed after `Goodbye` is received.
|
||||||
plugin_call_sender: Option<mpsc::Sender<ReceivedPluginCall>>,
|
plugin_call_sender: Option<mpsc::Sender<ReceivedPluginCall>>,
|
||||||
/// Receiver for PluginCalls. This is usually taken after initialization
|
/// Receiver for PluginCalls. This is usually taken after initialization
|
||||||
|
@ -103,15 +105,17 @@ impl EngineInterfaceManager {
|
||||||
pub(crate) fn new(writer: impl PluginWrite<PluginOutput> + 'static) -> EngineInterfaceManager {
|
pub(crate) fn new(writer: impl PluginWrite<PluginOutput> + 'static) -> EngineInterfaceManager {
|
||||||
let (plug_tx, plug_rx) = mpsc::channel();
|
let (plug_tx, plug_rx) = mpsc::channel();
|
||||||
let (subscription_tx, subscription_rx) = mpsc::channel();
|
let (subscription_tx, subscription_rx) = mpsc::channel();
|
||||||
|
let protocol_info_mut = WaitableMut::new();
|
||||||
|
|
||||||
EngineInterfaceManager {
|
EngineInterfaceManager {
|
||||||
state: Arc::new(EngineInterfaceState {
|
state: Arc::new(EngineInterfaceState {
|
||||||
protocol_info: Waitable::new(),
|
protocol_info: protocol_info_mut.reader(),
|
||||||
engine_call_id_sequence: Sequence::default(),
|
engine_call_id_sequence: Sequence::default(),
|
||||||
stream_id_sequence: Sequence::default(),
|
stream_id_sequence: Sequence::default(),
|
||||||
engine_call_subscription_sender: subscription_tx,
|
engine_call_subscription_sender: subscription_tx,
|
||||||
writer: Box::new(writer),
|
writer: Box::new(writer),
|
||||||
}),
|
}),
|
||||||
|
protocol_info_mut,
|
||||||
plugin_call_sender: Some(plug_tx),
|
plugin_call_sender: Some(plug_tx),
|
||||||
plugin_call_receiver: Some(plug_rx),
|
plugin_call_receiver: Some(plug_rx),
|
||||||
engine_call_subscriptions: BTreeMap::new(),
|
engine_call_subscriptions: BTreeMap::new(),
|
||||||
|
@ -233,7 +237,7 @@ impl InterfaceManager for EngineInterfaceManager {
|
||||||
match input {
|
match input {
|
||||||
PluginInput::Hello(info) => {
|
PluginInput::Hello(info) => {
|
||||||
let info = Arc::new(info);
|
let info = Arc::new(info);
|
||||||
self.state.protocol_info.set(info.clone())?;
|
self.protocol_info_mut.set(info.clone())?;
|
||||||
|
|
||||||
let local_info = ProtocolInfo::default();
|
let local_info = ProtocolInfo::default();
|
||||||
if local_info.is_compatible_with(&info)? {
|
if local_info.is_compatible_with(&info)? {
|
||||||
|
|
|
@ -300,8 +300,7 @@ fn manager_consume_errors_on_sending_other_messages_before_hello() -> Result<(),
|
||||||
|
|
||||||
fn set_default_protocol_info(manager: &mut EngineInterfaceManager) -> Result<(), ShellError> {
|
fn set_default_protocol_info(manager: &mut EngineInterfaceManager) -> Result<(), ShellError> {
|
||||||
manager
|
manager
|
||||||
.state
|
.protocol_info_mut
|
||||||
.protocol_info
|
|
||||||
.set(Arc::new(ProtocolInfo::default()))
|
.set(Arc::new(ProtocolInfo::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
PluginOutput, ProtocolInfo, StreamId, StreamMessage,
|
PluginOutput, ProtocolInfo, StreamId, StreamMessage,
|
||||||
},
|
},
|
||||||
sequence::Sequence,
|
sequence::Sequence,
|
||||||
util::{with_custom_values_in, Waitable},
|
util::{with_custom_values_in, Waitable, WaitableMut},
|
||||||
};
|
};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::Operator, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, ListStream,
|
ast::Operator, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, ListStream,
|
||||||
|
@ -138,6 +138,8 @@ impl Drop for PluginCallState {
|
||||||
pub struct PluginInterfaceManager {
|
pub struct PluginInterfaceManager {
|
||||||
/// Shared state
|
/// Shared state
|
||||||
state: Arc<PluginInterfaceState>,
|
state: Arc<PluginInterfaceState>,
|
||||||
|
/// The writer for protocol info
|
||||||
|
protocol_info_mut: WaitableMut<Arc<ProtocolInfo>>,
|
||||||
/// Manages stream messages and state
|
/// Manages stream messages and state
|
||||||
stream_manager: StreamManager,
|
stream_manager: StreamManager,
|
||||||
/// State related to plugin calls
|
/// State related to plugin calls
|
||||||
|
@ -159,18 +161,20 @@ impl PluginInterfaceManager {
|
||||||
writer: impl PluginWrite<PluginInput> + 'static,
|
writer: impl PluginWrite<PluginInput> + 'static,
|
||||||
) -> PluginInterfaceManager {
|
) -> PluginInterfaceManager {
|
||||||
let (subscription_tx, subscription_rx) = mpsc::channel();
|
let (subscription_tx, subscription_rx) = mpsc::channel();
|
||||||
|
let protocol_info_mut = WaitableMut::new();
|
||||||
|
|
||||||
PluginInterfaceManager {
|
PluginInterfaceManager {
|
||||||
state: Arc::new(PluginInterfaceState {
|
state: Arc::new(PluginInterfaceState {
|
||||||
source,
|
source,
|
||||||
process: pid.map(PluginProcess::new),
|
process: pid.map(PluginProcess::new),
|
||||||
protocol_info: Waitable::new(),
|
protocol_info: protocol_info_mut.reader(),
|
||||||
plugin_call_id_sequence: Sequence::default(),
|
plugin_call_id_sequence: Sequence::default(),
|
||||||
stream_id_sequence: Sequence::default(),
|
stream_id_sequence: Sequence::default(),
|
||||||
plugin_call_subscription_sender: subscription_tx,
|
plugin_call_subscription_sender: subscription_tx,
|
||||||
error: OnceLock::new(),
|
error: OnceLock::new(),
|
||||||
writer: Box::new(writer),
|
writer: Box::new(writer),
|
||||||
}),
|
}),
|
||||||
|
protocol_info_mut,
|
||||||
stream_manager: StreamManager::new(),
|
stream_manager: StreamManager::new(),
|
||||||
plugin_call_states: BTreeMap::new(),
|
plugin_call_states: BTreeMap::new(),
|
||||||
plugin_call_subscription_receiver: subscription_rx,
|
plugin_call_subscription_receiver: subscription_rx,
|
||||||
|
@ -464,7 +468,7 @@ impl InterfaceManager for PluginInterfaceManager {
|
||||||
match input {
|
match input {
|
||||||
PluginOutput::Hello(info) => {
|
PluginOutput::Hello(info) => {
|
||||||
let info = Arc::new(info);
|
let info = Arc::new(info);
|
||||||
self.state.protocol_info.set(info.clone())?;
|
self.protocol_info_mut.set(info.clone())?;
|
||||||
|
|
||||||
let local_info = ProtocolInfo::default();
|
let local_info = ProtocolInfo::default();
|
||||||
if local_info.is_compatible_with(&info)? {
|
if local_info.is_compatible_with(&info)? {
|
||||||
|
@ -631,7 +635,14 @@ impl PluginInterface {
|
||||||
|
|
||||||
/// Get the protocol info for the plugin. Will block to receive `Hello` if not received yet.
|
/// Get the protocol info for the plugin. Will block to receive `Hello` if not received yet.
|
||||||
pub fn protocol_info(&self) -> Result<Arc<ProtocolInfo>, ShellError> {
|
pub fn protocol_info(&self) -> Result<Arc<ProtocolInfo>, ShellError> {
|
||||||
self.state.protocol_info.get()
|
self.state.protocol_info.get().and_then(|info| {
|
||||||
|
info.ok_or_else(|| ShellError::PluginFailedToLoad {
|
||||||
|
msg: format!(
|
||||||
|
"Failed to get protocol info (`Hello` message) from the `{}` plugin",
|
||||||
|
self.state.source.identity.name()
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the protocol info. This should be done after initialization
|
/// Write the protocol info. This should be done after initialization
|
||||||
|
|
|
@ -321,8 +321,7 @@ fn manager_consume_errors_on_sending_other_messages_before_hello() -> Result<(),
|
||||||
|
|
||||||
fn set_default_protocol_info(manager: &mut PluginInterfaceManager) -> Result<(), ShellError> {
|
fn set_default_protocol_info(manager: &mut PluginInterfaceManager) -> Result<(), ShellError> {
|
||||||
manager
|
manager
|
||||||
.state
|
.protocol_info_mut
|
||||||
.protocol_info
|
|
||||||
.set(Arc::new(ProtocolInfo::default()))
|
.set(Arc::new(ProtocolInfo::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,5 +3,5 @@ mod waitable;
|
||||||
mod with_custom_values_in;
|
mod with_custom_values_in;
|
||||||
|
|
||||||
pub(crate) use mutable_cow::*;
|
pub(crate) use mutable_cow::*;
|
||||||
pub use waitable::Waitable;
|
pub use waitable::*;
|
||||||
pub use with_custom_values_in::*;
|
pub use with_custom_values_in::*;
|
||||||
|
|
|
@ -1,18 +1,36 @@
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
Condvar, Mutex, MutexGuard, PoisonError,
|
Arc, Condvar, Mutex, MutexGuard, PoisonError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nu_protocol::ShellError;
|
use nu_protocol::ShellError;
|
||||||
|
|
||||||
/// A container that may be empty, and allows threads to block until it has a value.
|
/// A shared container that may be empty, and allows threads to block until it has a value.
|
||||||
#[derive(Debug)]
|
///
|
||||||
|
/// This side is read-only - use [`WaitableMut`] on threads that might write a value.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct Waitable<T: Clone + Send> {
|
pub struct Waitable<T: Clone + Send> {
|
||||||
|
shared: Arc<WaitableShared<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WaitableMut<T: Clone + Send> {
|
||||||
|
shared: Arc<WaitableShared<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct WaitableShared<T: Clone + Send> {
|
||||||
is_set: AtomicBool,
|
is_set: AtomicBool,
|
||||||
mutex: Mutex<Option<T>>,
|
mutex: Mutex<SyncState<T>>,
|
||||||
condvar: Condvar,
|
condvar: Condvar,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct SyncState<T: Clone + Send> {
|
||||||
|
writers: usize,
|
||||||
|
value: Option<T>,
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn fail_if_poisoned<'a, T>(
|
fn fail_if_poisoned<'a, T>(
|
||||||
result: Result<MutexGuard<'a, T>, PoisonError<MutexGuard<'a, T>>>,
|
result: Result<MutexGuard<'a, T>, PoisonError<MutexGuard<'a, T>>>,
|
||||||
|
@ -26,75 +44,138 @@ fn fail_if_poisoned<'a, T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone + Send> Waitable<T> {
|
impl<T: Clone + Send> WaitableMut<T> {
|
||||||
/// Create a new empty `Waitable`.
|
/// Create a new empty `WaitableMut`. Call [`.reader()`] to get [`Waitable`].
|
||||||
pub fn new() -> Waitable<T> {
|
pub fn new() -> WaitableMut<T> {
|
||||||
Waitable {
|
WaitableMut {
|
||||||
is_set: AtomicBool::new(false),
|
shared: Arc::new(WaitableShared {
|
||||||
mutex: Mutex::new(None),
|
is_set: AtomicBool::new(false),
|
||||||
condvar: Condvar::new(),
|
mutex: Mutex::new(SyncState {
|
||||||
|
writers: 1,
|
||||||
|
value: None,
|
||||||
|
}),
|
||||||
|
condvar: Condvar::new(),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for a value to be available and then clone it.
|
pub fn reader(&self) -> Waitable<T> {
|
||||||
|
Waitable {
|
||||||
|
shared: self.shared.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the value and let waiting threads know.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn get(&self) -> Result<T, ShellError> {
|
pub fn set(&self, value: T) -> Result<(), ShellError> {
|
||||||
let guard = fail_if_poisoned(self.mutex.lock())?;
|
let mut sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
|
||||||
if let Some(value) = (*guard).clone() {
|
self.shared.is_set.store(true, Ordering::SeqCst);
|
||||||
Ok(value)
|
sync_state.value = Some(value);
|
||||||
|
self.shared.condvar.notify_all();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send> Default for WaitableMut<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send> Clone for WaitableMut<T> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
let shared = self.shared.clone();
|
||||||
|
shared
|
||||||
|
.mutex
|
||||||
|
.lock()
|
||||||
|
.expect("failed to lock mutex to increment writers")
|
||||||
|
.writers += 1;
|
||||||
|
WaitableMut { shared }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send> Drop for WaitableMut<T> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Decrement writers...
|
||||||
|
if let Ok(mut sync_state) = self.shared.mutex.lock() {
|
||||||
|
sync_state.writers = sync_state
|
||||||
|
.writers
|
||||||
|
.checked_sub(1)
|
||||||
|
.expect("would decrement writers below zero");
|
||||||
|
}
|
||||||
|
// and notify waiting threads so they have a chance to see it.
|
||||||
|
self.shared.condvar.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + Send> Waitable<T> {
|
||||||
|
/// Wait for a value to be available and then clone it.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if there are no writers left that could possibly place a value.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn get(&self) -> Result<Option<T>, ShellError> {
|
||||||
|
let sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
|
||||||
|
if let Some(value) = sync_state.value.clone() {
|
||||||
|
Ok(Some(value))
|
||||||
|
} else if sync_state.writers == 0 {
|
||||||
|
// There can't possibly be a value written, so no point in waiting.
|
||||||
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
let guard = fail_if_poisoned(self.condvar.wait_while(guard, |g| g.is_none()))?;
|
let sync_state = fail_if_poisoned(
|
||||||
Ok((*guard)
|
self.shared
|
||||||
.clone()
|
.condvar
|
||||||
.expect("checked already for Some but it was None"))
|
.wait_while(sync_state, |g| g.writers > 0 && g.value.is_none()),
|
||||||
|
)?;
|
||||||
|
Ok(sync_state.value.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the value if one is available, but don't wait if not.
|
/// Clone the value if one is available, but don't wait if not.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn try_get(&self) -> Result<Option<T>, ShellError> {
|
pub fn try_get(&self) -> Result<Option<T>, ShellError> {
|
||||||
let guard = fail_if_poisoned(self.mutex.lock())?;
|
let sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
|
||||||
Ok((*guard).clone())
|
Ok(sync_state.value.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if value is available.
|
/// Returns true if value is available.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn is_set(&self) -> bool {
|
pub fn is_set(&self) -> bool {
|
||||||
self.is_set.load(Ordering::SeqCst)
|
self.shared.is_set.load(Ordering::SeqCst)
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the value and let waiting threads know.
|
|
||||||
#[track_caller]
|
|
||||||
pub fn set(&self, value: T) -> Result<(), ShellError> {
|
|
||||||
let mut guard = fail_if_poisoned(self.mutex.lock())?;
|
|
||||||
self.is_set.store(true, Ordering::SeqCst);
|
|
||||||
*guard = Some(value);
|
|
||||||
self.condvar.notify_all();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Clone + Send> Default for Waitable<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn set_from_other_thread() -> Result<(), ShellError> {
|
fn set_from_other_thread() -> Result<(), ShellError> {
|
||||||
use std::sync::Arc;
|
let waitable_mut = WaitableMut::new();
|
||||||
|
let waitable = waitable_mut.reader();
|
||||||
let waitable = Arc::new(Waitable::new());
|
|
||||||
let waitable_clone = waitable.clone();
|
|
||||||
|
|
||||||
assert!(!waitable.is_set());
|
assert!(!waitable.is_set());
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
waitable_clone.set(42).expect("error on set");
|
waitable_mut.set(42).expect("error on set");
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(42, waitable.get()?);
|
assert_eq!(Some(42), waitable.get()?);
|
||||||
assert_eq!(Some(42), waitable.try_get()?);
|
assert_eq!(Some(42), waitable.try_get()?);
|
||||||
assert!(waitable.is_set());
|
assert!(waitable.is_set());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dont_deadlock_if_waiting_without_writer() {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
|
let writer = WaitableMut::<()>::new();
|
||||||
|
let waitable = writer.reader();
|
||||||
|
// Ensure there are no writers
|
||||||
|
drop(writer);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = tx.send(waitable.get());
|
||||||
|
});
|
||||||
|
let result = rx
|
||||||
|
.recv_timeout(Duration::from_secs(10))
|
||||||
|
.expect("timed out")
|
||||||
|
.expect("error");
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use serde_json::{json, Value};
|
||||||
struct Options {
|
struct Options {
|
||||||
refuse_local_socket: bool,
|
refuse_local_socket: bool,
|
||||||
advertise_local_socket: bool,
|
advertise_local_socket: bool,
|
||||||
|
exit_before_hello: bool,
|
||||||
exit_early: bool,
|
exit_early: bool,
|
||||||
wrong_version: bool,
|
wrong_version: bool,
|
||||||
local_socket_path: Option<String>,
|
local_socket_path: Option<String>,
|
||||||
|
@ -28,6 +29,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let mut opts = Options {
|
let mut opts = Options {
|
||||||
refuse_local_socket: has_env("STRESS_REFUSE_LOCAL_SOCKET"),
|
refuse_local_socket: has_env("STRESS_REFUSE_LOCAL_SOCKET"),
|
||||||
advertise_local_socket: has_env("STRESS_ADVERTISE_LOCAL_SOCKET"),
|
advertise_local_socket: has_env("STRESS_ADVERTISE_LOCAL_SOCKET"),
|
||||||
|
exit_before_hello: has_env("STRESS_EXIT_BEFORE_HELLO"),
|
||||||
exit_early: has_env("STRESS_EXIT_EARLY"),
|
exit_early: has_env("STRESS_EXIT_EARLY"),
|
||||||
wrong_version: has_env("STRESS_WRONG_VERSION"),
|
wrong_version: has_env("STRESS_WRONG_VERSION"),
|
||||||
local_socket_path: None,
|
local_socket_path: None,
|
||||||
|
@ -75,6 +77,11 @@ pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
output.flush()?;
|
output.flush()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test exiting without `Hello`
|
||||||
|
if opts.exit_before_hello {
|
||||||
|
std::process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Send `Hello` message
|
// Send `Hello` message
|
||||||
write(
|
write(
|
||||||
&mut output,
|
&mut output,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::{sync::mpsc, time::Duration};
|
||||||
|
|
||||||
use nu_test_support::nu_with_plugins;
|
use nu_test_support::nu_with_plugins;
|
||||||
|
|
||||||
fn ensure_stress_env_vars_unset() {
|
fn ensure_stress_env_vars_unset() {
|
||||||
|
@ -75,6 +77,30 @@ fn test_failing_local_socket_fallback() {
|
||||||
assert!(result.out.contains("local_socket_path: None"));
|
assert!(result.out.contains("local_socket_path: None"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_exit_before_hello_stdio() {
|
||||||
|
ensure_stress_env_vars_unset();
|
||||||
|
// This can deadlock if not handled properly, so we try several times and timeout
|
||||||
|
for _ in 0..5 {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let result = nu_with_plugins!(
|
||||||
|
cwd: ".",
|
||||||
|
envs: vec![
|
||||||
|
("STRESS_EXIT_BEFORE_HELLO", "1"),
|
||||||
|
],
|
||||||
|
plugin: ("nu_plugin_stress_internals"),
|
||||||
|
"stress_internals"
|
||||||
|
);
|
||||||
|
let _ = tx.send(result);
|
||||||
|
});
|
||||||
|
let result = rx
|
||||||
|
.recv_timeout(Duration::from_secs(15))
|
||||||
|
.expect("timed out. probably a deadlock");
|
||||||
|
assert!(!result.status.success());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_exit_early_stdio() {
|
fn test_exit_early_stdio() {
|
||||||
ensure_stress_env_vars_unset();
|
ensure_stress_env_vars_unset();
|
||||||
|
|
Loading…
Reference in a new issue