qsu 0.10.1

Service subsystem utilities and runtime wrapper.
Documentation
use std::sync::OnceLock;

use tokio::sync::broadcast;

#[cfg(feature = "tokio")]
use {killswitch::KillSwitch, tokio::signal};

use windows_sys::{
  Win32::{
    Foundation::{FALSE, TRUE},
    System::Console::{
      CTRL_BREAK_EVENT, CTRL_C_EVENT, CTRL_CLOSE_EVENT, SetConsoleCtrlHandler
    }
  },
  core::BOOL
};

use crate::{
  err::Error,
  rt::{Demise, SvcEvt}
};


static CELL: OnceLock<Box<dyn Fn(u32) -> BOOL + Send + Sync>> =
  OnceLock::new();

/// Async task used to wait for Ctrl+C to be signalled.
///
/// Whenever a Ctrl+C is signalled the closure in `f` is called and
/// the task is terminated.
#[cfg(feature = "tokio")]
pub async fn wait_shutdown<F>(f: F, ks: KillSwitch)
where
  F: FnOnce() + Send
{
  tracing::trace!("CTRL+C task launched");

  tokio::select! {
    _ = signal::ctrl_c() => {
      tracing::debug!("Received Ctrl+C");
      // Once any process termination signal has been received post call the
      // callback.
      f();
    },
    () = ks.wait() => {
      tracing::debug!("killswitch triggered");
    }
  }

  tracing::trace!("wait_shutdown() terminating");
}

#[cfg(feature = "tokio")]
pub async fn wait_term<F>(f: F, ks: KillSwitch)
where
  F: FnOnce() + Send
{
  tracing::trace!("CTRL+Break/Close task launched");

  let Ok(mut cbreak) = signal::windows::ctrl_break() else {
    log::error!("Unable to create Ctrl+Break monitor");
    return;
  };

  let Ok(mut cclose) = signal::windows::ctrl_close() else {
    log::error!("Unable to create Close monitor");
    return;
  };

  tokio::select! {
    _ = cbreak.recv() => {
      tracing::debug!("Received Ctrl+Break");
      // Once any process termination signal has been received post call the
      // callback.
      f();
    },
    _ = cclose.recv() => {
      tracing::debug!("Received Close");
      // Once any process termination signal has been received post call the
      // callback.
      f();
    },
    () = ks.wait() => {
      tracing::debug!("killswitch triggered");
    }
  }

  tracing::trace!("wait_term() terminating");
}


pub fn sync_kill_to_event(
  tx: broadcast::Sender<SvcEvt>,
  test_mode: bool
) -> Result<(), Error> {
  setup_sync_fg_kill_handler(
    move |ty| {
      match ty {
        CTRL_C_EVENT => {
          tracing::trace!(
            "Received some kind of event that should trigger a shutdown."
          );
          if tx.send(SvcEvt::Shutdown(Demise::Interrupted)).is_ok() {
            // We handled this event
            TRUE
          } else {
            FALSE
          }
        }
        CTRL_BREAK_EVENT | CTRL_CLOSE_EVENT => {
          tracing::trace!(
            "Received some kind of event that should trigger a termination."
          );
          if tx.send(SvcEvt::Shutdown(Demise::Terminated)).is_ok() {
            // We handled this event
            TRUE
          } else {
            FALSE
          }
        }
        _ => FALSE
      }
    },
    test_mode
  )?;
  Ok(())
}


pub fn setup_sync_fg_kill_handler<F>(
  f: F,
  test_mode: bool
) -> Result<(), Error>
where
  F: Fn(u32) -> BOOL + Send + Sync + 'static
{
  // The proper way to do this is to use CELL.set(), because this can only
  // be done once for each process.  Tests may run in the same process (on
  // separate threads), which will cause globals like this to initialized
  // multiple times (which is bad).
  //
  // The workaround is to use get_or_init() instead, but only do it if test
  // mode has been requested.
  if test_mode {
    let _ = CELL.get_or_init(|| Box::new(f));
  } else {
    CELL
      .set(Box::new(f))
      .map_err(|_| Error::internal("Unable to set shared OnceLock cell"))?;
  }

  let rc = unsafe { SetConsoleCtrlHandler(Some(ctrlhandler), 1) };
  // Returns non-zero on success
  (rc != 0)
    .then_some(())
    .ok_or_else(|| Error::internal("SetConsoleCtrlHandler failed"))?;

  Ok(())
}

unsafe extern "system" fn ctrlhandler(ty: u32) -> BOOL {
  let Some(f) = CELL.get() else {
    return FALSE;
  };

  f(ty)
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :