use std::{
collections::{HashSet, VecDeque},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
use crate::{
PluginPaths,
hook::{
Hook,
executable::ExecutableHookHandle,
wasm::{WASMHookHandle, loader::WASMLoader},
},
host::PluginContext,
plugin::HookSubscription,
};
use anyhow::Context;
use nitro_shared::output::{MessageContents, NitroOutput, NoOp};
use tokio::sync::Mutex;
use crate::{
input_output::{CommandResult, InputAction},
plugin::PluginPersistence,
};
pub struct HookCallArg<'a, H: Hook> {
pub cmd: &'a str,
pub arg: &'a H::Arg,
pub additional_args: &'a [String],
pub working_dir: Option<&'a Path>,
pub ctx: HookCallContext<'a>,
pub use_base64: bool,
pub persistence: Arc<Mutex<PluginPersistence>>,
pub paths: &'a PluginPaths,
pub plugin_id: &'a str,
pub protocol_version: u16,
pub wasm_loader: Arc<Mutex<WASMLoader>>,
}
pub struct HookCallContext<'a> {
pub subscriptions: &'a HashSet<HookSubscription>,
pub nitro_version: Option<&'a str>,
pub custom_config: Option<String>,
pub plugin_list: &'a [String],
pub global_context: Option<&'a Arc<dyn PluginContext>>,
}
#[must_use]
pub struct HookHandle<H: Hook> {
inner: HookHandleInner<H>,
plugin_persistence: Option<Arc<Mutex<PluginPersistence>>>,
plugin_id: String,
command_results: VecDeque<CommandResult>,
start_time: Option<Instant>,
is_finished: bool,
}
impl<H: Hook> HookHandle<H> {
pub fn constant(result: H::Result, plugin_id: String) -> Self {
Self {
inner: HookHandleInner::Constant(result),
plugin_persistence: None,
plugin_id,
command_results: VecDeque::new(),
start_time: None,
is_finished: true,
}
}
pub(super) fn executable(
inner: ExecutableHookHandle<H>,
plugin_id: String,
plugin_persistence: Arc<Mutex<PluginPersistence>>,
) -> Self {
Self {
inner: HookHandleInner::Executable(inner),
plugin_persistence: Some(plugin_persistence),
plugin_id,
command_results: VecDeque::new(),
start_time: None,
is_finished: false,
}
}
pub(super) fn wasm(
inner: WASMHookHandle<H>,
plugin_id: String,
plugin_persistence: Arc<Mutex<PluginPersistence>>,
) -> Self {
let start_time = if std::env::var("NITRO_PLUGIN_PROFILE").is_ok_and(|x| x == "1") {
Some(Instant::now())
} else {
None
};
Self {
inner: HookHandleInner::WASM(inner),
plugin_persistence: Some(plugin_persistence),
plugin_id,
command_results: VecDeque::new(),
start_time,
is_finished: false,
}
}
pub fn get_id(&self) -> &String {
&self.plugin_id
}
pub async fn ensure_started(&mut self, o: &mut impl NitroOutput) -> anyhow::Result<()> {
match &mut self.inner {
HookHandleInner::Executable(inner) => {
inner
.ensure_started(
&mut self.plugin_persistence,
&mut self.command_results,
&mut self.start_time,
o,
)
.await?;
}
HookHandleInner::WASM(inner) => {
inner.run(o).await?;
}
HookHandleInner::Constant(..) => {}
}
Ok(())
}
pub async fn poll(&mut self, o: &mut impl NitroOutput) -> anyhow::Result<bool> {
if self.is_finished {
return Ok(true);
}
let finished = match &mut self.inner {
HookHandleInner::Executable(inner) => inner
.poll(
&mut self.plugin_persistence,
&mut self.command_results,
&mut self.start_time,
o,
)
.await
.context("Failed to poll executable hook")?,
HookHandleInner::WASM(inner) => {
inner.run(o).await?;
inner.has_result()
}
HookHandleInner::Constant(..) => true,
};
if finished {
self.is_finished = true;
if let Some(start_time) = &self.start_time {
let now = Instant::now();
let delta = now.duration_since(*start_time);
o.display(MessageContents::Simple(format!(
"Plugin '{}' took {delta:?} to run hook '{}'",
self.plugin_id,
H::get_name_static()
)));
}
}
Ok(finished)
}
pub async fn send_input_action(&mut self, action: InputAction) -> anyhow::Result<()> {
if let HookHandleInner::Executable(inner) = &mut self.inner {
inner.send_input_action(action).await?;
}
Ok(())
}
pub async fn result(mut self, o: &mut impl NitroOutput) -> anyhow::Result<H::Result> {
match &mut self.inner {
HookHandleInner::Executable(..) => loop {
let result = self.poll(o).await?;
if result {
break;
}
tokio::time::sleep(Duration::from_micros(50)).await;
},
HookHandleInner::WASM(inner) => {
inner.run(o).await?;
}
HookHandleInner::Constant(..) => {}
}
match self.inner {
HookHandleInner::Constant(result) => Ok(result),
HookHandleInner::Executable(inner) => inner.result().await,
HookHandleInner::WASM(inner) => {
inner.result().await.context("Failed to get hook result")
}
}
}
pub async fn kill(self, o: &mut impl NitroOutput) -> anyhow::Result<Option<H::Result>> {
let _ = o;
match self.inner {
HookHandleInner::Constant(result) => Ok(Some(result)),
HookHandleInner::Executable(inner) => inner.kill().await,
HookHandleInner::WASM(inner) => inner.result().await.map(Some),
}
}
pub async fn terminate(mut self) {
let result = self.send_input_action(InputAction::Terminate).await;
if result.is_err() {
let _ = self.kill(&mut NoOp).await;
}
}
pub fn pop_command_result(&mut self) -> Option<CommandResult> {
self.command_results.pop_front()
}
}
enum HookHandleInner<H: Hook> {
Executable(ExecutableHookHandle<H>),
WASM(WASMHookHandle<H>),
Constant(H::Result),
}
pub struct HookHandles<H: Hook> {
handles: VecDeque<HookHandle<H>>,
}
impl<H: Hook> HookHandles<H> {
pub(crate) async fn new(
mut handles: VecDeque<HookHandle<H>>,
o: &mut impl NitroOutput,
) -> anyhow::Result<Self> {
if H::is_asynchronous() {
for handle in &mut handles {
handle.ensure_started(o).await?;
}
}
Ok(Self { handles })
}
pub fn is_empty(&self) -> bool {
self.handles.is_empty()
}
pub fn len(&self) -> usize {
self.handles.len()
}
pub fn next(&mut self) -> Option<HookHandle<H>> {
self.handles.pop_front()
}
pub async fn next_result(
&mut self,
o: &mut impl NitroOutput,
) -> anyhow::Result<Option<H::Result>> {
let Some(next) = self.next() else {
return Ok(None);
};
next.result(o).await.map(Some)
}
pub async fn all_results(mut self, o: &mut impl NitroOutput) -> anyhow::Result<Vec<H::Result>> {
let mut out = Vec::with_capacity(self.len());
while let Some(result) = self.next_result(o).await? {
out.push(result);
}
Ok(out)
}
pub async fn poll_all(&mut self, o: &mut impl NitroOutput) -> anyhow::Result<()> {
for handle in &mut self.handles {
handle.poll(o).await?;
}
Ok(())
}
pub async fn terminate(self) {
for handle in self.handles {
handle.terminate().await;
}
}
pub async fn kill(self, o: &mut impl NitroOutput) -> anyhow::Result<Vec<H::Result>> {
let mut error = None;
let mut out = Vec::new();
for handle in self.handles {
match handle.kill(o).await {
Ok(result) => out.extend(result),
Err(e) => error = Some(e),
}
}
if let Some(error) = error {
Err(error)
} else {
Ok(out)
}
}
}
impl<H: Hook, T> HookHandles<H>
where
H::Result: IntoIterator<Item = T>,
{
pub async fn flatten_all_results(mut self, o: &mut impl NitroOutput) -> anyhow::Result<Vec<T>> {
let mut out = Vec::new();
while let Some(result) = self.next_result(o).await? {
out.extend(result);
}
Ok(out)
}
pub async fn flatten_all_results_with_ids(
mut self,
o: &mut impl NitroOutput,
) -> anyhow::Result<Vec<(String, T)>> {
let mut out = Vec::new();
while let Some(result) = self.next() {
let id = result.get_id().clone();
let result = result.result(o).await?;
out.extend(result.into_iter().map(|x| (id.clone(), x)));
}
Ok(out)
}
}
impl<H: Hook, T> HookHandles<H>
where
H::Result: OptionLike<Type = T>,
{
pub async fn first_some(mut self, o: &mut impl NitroOutput) -> anyhow::Result<Option<T>> {
while let Some(result) = self.next_result(o).await? {
if let Some(result) = result.into_option() {
return Ok(Some(result));
}
}
Ok(None)
}
}
pub trait OptionLike {
type Type;
fn into_option(self) -> Option<Self::Type>;
}
impl<T> OptionLike for Option<T> {
type Type = T;
fn into_option(self) -> Option<Self::Type> {
self
}
}