Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions backend/nyanpasu-platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ backon = { version = '1.0.1', features = ['tokio-sleep'] }
base64 = '0.23'
hex = '0.4'
nanoid = '0.5.0'
parking_lot = { version = '0.13.0' }
percent-encoding = '2.3.1'
reqwest = { workspace = true }
serde = { version = '1.0', features = ['derive', 'rc'] }
Expand Down Expand Up @@ -61,6 +62,17 @@ thiserror = { workspace = true }
tokio = { workspace = true, features = ['full'] }
tracing = { workspace = true }

[target.'cfg(windows)'.dependencies]
oneshot = '0.2'
windows-core = '0.62'
windows-sys = { version = '0.61', features = [
'Win32_Foundation',
'Win32_Graphics_Gdi',
'Win32_System_LibraryLoader',
'Win32_System_Shutdown',
'Win32_UI_WindowsAndMessaging',
] }

[dev-dependencies]
nyanpasu-core = { path = '../nyanpasu-core', features = ['test-support'] }
nyanpasu-ipc = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions backend/nyanpasu-platform/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod changelog_http;
pub mod enhance;
pub mod profiles;
pub mod session_end;
8 changes: 8 additions & 0 deletions backend/nyanpasu-platform/src/session_end/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! Platform session-end handling and synchronous host cleanup acknowledgement.

mod protocol;

#[cfg(windows)]
pub mod windows;

pub use protocol::ShutdownCompletion;
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
//! Session-end decisions and the synchronous Windows cleanup acknowledgement.

use std::sync::Arc;
use std::{sync::Arc, time::Instant};

#[cfg(any(windows, test))]
use std::time::Instant;
use std::time::Duration;

use parking_lot::{Condvar, Mutex};

#[cfg(any(windows, test))]
use nyanpasu_core::client::session_end::SessionEndSource;

#[cfg(any(windows, test))]
const SESSION_END_WAIT: std::time::Duration = std::time::Duration::from_secs(30);

#[cfg(any(windows, test))]
pub(crate) enum SessionMessage {
pub(super) enum SessionMessage {
Query,
End { ending: bool },
}

#[cfg(any(windows, test))]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum SessionAction {
pub(super) enum SessionAction {
Query,
Cancel,
Confirm {
Expand All @@ -31,21 +28,23 @@ pub(crate) enum SessionAction {
}

#[cfg(any(windows, test))]
pub(crate) struct SessionEndHook {
pub(super) struct SessionEndHook {
source: SessionEndSource,
wait: Duration,
confirmed: Option<Instant>,
}

#[cfg(any(windows, test))]
impl SessionEndHook {
pub(crate) fn new(source: SessionEndSource) -> Self {
pub(super) fn new(source: SessionEndSource, wait: Duration) -> Self {
Self {
source,
wait,
confirmed: None,
}
}

pub(crate) fn on_message(&mut self, message: SessionMessage) -> SessionAction {
pub(super) fn on_message(&mut self, message: SessionMessage) -> SessionAction {
match message {
SessionMessage::Query => {
self.source.set_pending(true);
Expand All @@ -62,7 +61,7 @@ impl SessionEndHook {
let request_shutdown = self.confirmed.is_none();
let deadline = *self
.confirmed
.get_or_insert_with(|| Instant::now() + SESSION_END_WAIT);
.get_or_insert_with(|| Instant::now() + self.wait);
SessionAction::Confirm {
deadline,
request_shutdown,
Expand All @@ -78,17 +77,19 @@ struct CompletionState {
changed: Condvar,
}

/// Signals that the host has finished draining its shutdown owners.
#[derive(Clone, Debug, Default)]
pub(crate) struct ShutdownCompletion(Arc<CompletionState>);
pub struct ShutdownCompletion(Arc<CompletionState>);

impl ShutdownCompletion {
pub(crate) fn complete(&self) {
/// Releases current and future waiters after host cleanup has finished.
pub fn complete(&self) {
*self.0.ready.lock() = true;
self.0.changed.notify_all();
}

#[cfg(any(windows, test))]
pub(crate) fn wait_until(&self, deadline: Instant) -> bool {
/// Blocks until cleanup completes, returning false if the deadline expires first.
pub fn wait_until(&self, deadline: Instant) -> bool {
let mut ready = self.0.ready.lock();
while !*ready {
if self.0.changed.wait_until(&mut ready, deadline).timed_out() {
Expand All @@ -107,7 +108,7 @@ mod tests {
fn query_and_cancel_do_not_confirm_shutdown() {
let source = SessionEndSource::new();
let reader = source.subscribe();
let mut hook = SessionEndHook::new(source);
let mut hook = SessionEndHook::new(source, Duration::from_secs(30));
assert_eq!(hook.on_message(SessionMessage::Query), SessionAction::Query);
assert!(reader.is_pending());
assert_eq!(hook.on_message(SessionMessage::Query), SessionAction::Query);
Expand All @@ -121,9 +122,10 @@ mod tests {

#[test]
fn confirmation_starts_shutdown_once_and_keeps_its_deadline() {
let wait = Duration::from_secs(7);
let source = SessionEndSource::new();
let reader = source.subscribe();
let mut hook = SessionEndHook::new(source);
let mut hook = SessionEndHook::new(source, wait);
let before = Instant::now();
let SessionAction::Confirm {
deadline,
Expand All @@ -133,8 +135,8 @@ mod tests {
panic!("confirmation requests shutdown");
};
assert!(request_shutdown);
assert!(deadline >= before + SESSION_END_WAIT);
assert!(deadline <= Instant::now() + SESSION_END_WAIT);
assert!(deadline >= before + wait);
assert!(deadline <= Instant::now() + wait);
assert_eq!(
hook.on_message(SessionMessage::End { ending: true }),
SessionAction::Confirm {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! The hidden Windows window that receives session-end messages.

use std::{cell::RefCell, sync::mpsc};
use std::{cell::RefCell, sync::mpsc, time::Duration};

use snafu::ResultExt as _;
use windows_core::{Error as WindowsError, PCWSTR, w};
Expand All @@ -21,12 +21,11 @@ use windows_sys::Win32::{

use nyanpasu_core::client::session_end::SessionEndSource;

use crate::utils::session_end::{
SessionAction, SessionEndHook, SessionMessage, ShutdownCompletion,
};
use super::protocol::{SessionAction, SessionEndHook, SessionMessage, ShutdownCompletion};

/// A failure to initialize the session-end window or its worker threads.
#[derive(Debug, snafu::Snafu)]
pub(crate) enum Error {
pub enum Error {
#[snafu(display("failed to start shutdown hook window thread"))]
StartWindowThread { source: std::io::Error },
#[snafu(display("failed to start shutdown hook callback thread"))]
Expand All @@ -41,15 +40,21 @@ pub(crate) enum Error {
SendInitialized,
}

pub(crate) fn setup_shutdown_hook(
/// Installs a hidden window that requests host shutdown on session confirmation.
/// The first confirmation fixes the deadline using `wait`; later messages retain it.
///
/// # Errors
/// Returns an error if its threads or hidden window cannot be initialized.
pub fn setup_shutdown_hook(
source: SessionEndSource,
completion: ShutdownCompletion,
wait: Duration,
f: impl Fn() + Send + Sync + 'static,
) -> Result<(), Error> {
let (initd_tx, initd_rx) = oneshot::channel();
let handle = std::thread::Builder::new()
.name("shutdown-hook-window".into())
.spawn(move || setup_shutdown_hook_inner(source, completion, f, initd_tx))
.spawn(move || setup_shutdown_hook_inner(source, completion, wait, f, initd_tx))
.context(StartWindowThreadSnafu)?;
if let Err(oneshot::RecvError) = initd_rx.recv() {
handle
Expand Down Expand Up @@ -167,7 +172,7 @@ unsafe extern "system" fn callback(hwnd: HWND, msg: u32, wparam: WPARAM, lparam:
return 0;
}
if !context.completion.wait_until(deadline) {
tracing::warn!("session-end cleanup exceeded its 30 second wait");
tracing::warn!(?deadline, "session-end cleanup exceeded its wait deadline");
}
return 0;
}
Expand Down Expand Up @@ -214,6 +219,7 @@ fn create_hidden_window(
fn setup_shutdown_hook_inner(
source: SessionEndSource,
completion: ShutdownCompletion,
wait: Duration,
f: impl Fn() + Send + Sync + 'static,
initd_tx: oneshot::Sender<()>,
) -> Result<(), Error> {
Expand Down Expand Up @@ -251,7 +257,7 @@ fn setup_shutdown_hook_inner(
class_name,
h_instance,
Box::new(HookContext {
protocol: RefCell::new(SessionEndHook::new(source)),
protocol: RefCell::new(SessionEndHook::new(source, wait)),
completion,
sender,
}),
Expand Down Expand Up @@ -304,7 +310,7 @@ mod tests {
class_name,
instance,
Box::new(HookContext {
protocol: RefCell::new(SessionEndHook::new(source)),
protocol: RefCell::new(SessionEndHook::new(source, Duration::from_secs(30))),
completion: completion.clone(),
sender,
}),
Expand Down Expand Up @@ -365,7 +371,7 @@ mod tests {
w!("NYANPASU_UNREGISTERED_TEST_CLASS"),
std::ptr::null_mut(),
Box::new(HookContext {
protocol: RefCell::new(SessionEndHook::new(source)),
protocol: RefCell::new(SessionEndHook::new(source, Duration::from_secs(30))),
completion: ShutdownCompletion::default(),
sender,
}),
Expand Down
5 changes: 0 additions & 5 deletions backend/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ nyanpasu-panics = { path = '../nyanpasu-panics' }
# Common Utilities
tokio = { workspace = true, features = ['test-util'] }
tokio-util = { version = '0.7', features = ['full'] }
oneshot = '0.2'
futures = '0.3'
futures-util = '0.3'
glob = '0.3.1'
Expand Down Expand Up @@ -242,13 +241,9 @@ deelevate = '0.2.0'
winreg = { version = '0.56', features = ['transactions'] }
windows-registry = '0.6.0'
windows-sys = { version = '0.61', features = [
'Win32_System_LibraryLoader',
'Win32_System_SystemInformation',
'Win32_System_Threading',
'Win32_UI_WindowsAndMessaging',
'Win32_System_Shutdown',
'Win32_Graphics_Dwm',
'Win32_Graphics_Gdi',
] }
# windows-core and webview2-com must stay in lockstep with the versions used
# by tauri/wry, since we cast COM interfaces obtained from tauri's webview.
Expand Down
2 changes: 0 additions & 2 deletions backend/tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ mod startup_failure;
mod unified_rpc;

mod host_paths;
#[cfg(windows)]
mod shutdown_hook;
mod utils;
mod widget;
mod window;
Expand Down
6 changes: 4 additions & 2 deletions backend/tauri/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use nyanpasu_core::client::{
};

use nyanpasu_paths::PathResolver;
use nyanpasu_platform::session_end::ShutdownCompletion;
use std::sync::Arc;

use crate::{
Expand Down Expand Up @@ -51,7 +52,7 @@ mod tests;
#[derive(Clone, Default)]
pub(crate) struct StartupOwners {
pub(crate) session_end: nyanpasu_core::client::session_end::SessionEndSource,
pub(crate) completion: crate::utils::session_end::ShutdownCompletion,
pub(crate) completion: ShutdownCompletion,
shutdown: CancellationToken,
tasks: TaskTracker,
core: Option<nyanpasu_core::actor_v2::CoreClient>,
Expand Down Expand Up @@ -221,9 +222,10 @@ pub fn setup<M: tauri::Manager<tauri::Wry>>(
#[cfg(target_os = "windows")]
{
let shutdown_handle = app_handle.clone();
if let Err(error) = super::shutdown_hook::setup_shutdown_hook(
if let Err(error) = nyanpasu_platform::session_end::windows::setup_shutdown_hook(
owners.session_end.clone(),
owners.completion.clone(),
std::time::Duration::from_secs(30),
move || {
tracing::info!("Shutdown hook triggered, exiting app...");
shutdown_handle.exit(0);
Expand Down
2 changes: 1 addition & 1 deletion backend/tauri/src/setup_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ async fn cleanup_completion_waits_for_partial_owners_and_handles_no_owner() {
send.send(()).unwrap();
drain.await;
assert!(completion.wait_until(std::time::Instant::now()));
let completion = crate::utils::session_end::ShutdownCompletion::default();
let completion = nyanpasu_platform::session_end::ShutdownCompletion::default();
crate::utils::exit::drain_shutdown_owners(None, None, completion.clone()).await;
assert!(completion.wait_until(std::time::Instant::now()));
}
2 changes: 1 addition & 1 deletion backend/tauri/src/utils/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tauri::{AppHandle, ExitRequestApi, Manager, RESTART_EXIT_CODE, process::curr

use nyanpasu_core::client::{NyanpasuClient, session_end::SessionEnding};

use super::session_end::ShutdownCompletion;
use nyanpasu_platform::session_end::ShutdownCompletion;

/// What the app does once its owners have shut down.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
1 change: 0 additions & 1 deletion backend/tauri/src/utils/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ pub mod main_thread;
pub mod profiling;
pub mod proxy_env;
pub mod resolve;
pub(crate) mod session_end;
// mod winhelp;

pub mod collect;
Expand Down
Loading