Add orderly shutdown on window close
This commit is contained in:
+53
-3
@@ -82,6 +82,8 @@ const CHAT_MIN_H: f32 = 110.0;
|
||||
const ABOVE_CHAT_MIN_H: f32 = 300.0;
|
||||
/// Thickness of a draggable divider (px).
|
||||
const DIVIDER_THICKNESS: f32 = 8.0;
|
||||
/// Upper bound for waiting on orderly core shutdown before letting the window exit.
|
||||
const SHUTDOWN_TIMEOUT_SECS: u64 = 5;
|
||||
|
||||
/// Clamp the Participants panel width so neither it nor the Controls panel drops
|
||||
/// below its minimum, given the current window width.
|
||||
@@ -221,6 +223,10 @@ pub enum AppMessage {
|
||||
ToggleScreenShare,
|
||||
/// Watch a peer's screen share, identified by their pixelpass ticket.
|
||||
WatchShare(String),
|
||||
/// Result of asynchronously enqueueing the core shutdown command.
|
||||
ShutdownCommandSent(bool),
|
||||
/// Fallback close if the core does not acknowledge shutdown promptly.
|
||||
ShutdownTimeout,
|
||||
}
|
||||
|
||||
fn core_subscription() -> impl iced::futures::Stream<Item = UiEvent> {
|
||||
@@ -324,6 +330,8 @@ pub struct AppState {
|
||||
friend_add_name: String,
|
||||
/// Inline feedback for the add-friend form (e.g. a bad id), cleared on edit.
|
||||
friend_add_error: Option<String>,
|
||||
/// Window close has been requested and the GUI is waiting for core teardown.
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@@ -441,6 +449,7 @@ impl Default for AppState {
|
||||
friend_add_id: String::new(),
|
||||
friend_add_name: String::new(),
|
||||
friend_add_error: None,
|
||||
closing: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,6 +529,20 @@ fn subscription(_state: &AppState) -> Subscription<AppMessage> {
|
||||
Subscription::batch(vec![core_sub, event_sub])
|
||||
}
|
||||
|
||||
fn shutdown_timeout_task() -> Task<AppMessage> {
|
||||
Task::perform(
|
||||
async {
|
||||
let (tx, rx) = iced::futures::channel::oneshot::channel();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_secs(SHUTDOWN_TIMEOUT_SECS));
|
||||
let _ = tx.send(());
|
||||
});
|
||||
let _ = rx.await;
|
||||
},
|
||||
|_| AppMessage::ShutdownTimeout,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reconnect-chime edge trigger for `UiEvent::PeerConnecting`. Marks the peer as
|
||||
/// connecting and returns `Some(Sound::ReconnectAttempt)` exactly once per outage:
|
||||
/// only when the peer had a live link before (a genuine reconnect, not a first
|
||||
@@ -865,6 +888,11 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.status_message =
|
||||
"Discoverable timed out — back to Normal".to_string();
|
||||
}
|
||||
UiEvent::ShutdownComplete => {
|
||||
if state.closing {
|
||||
return iced::exit();
|
||||
}
|
||||
}
|
||||
UiEvent::Error(err) => {
|
||||
state.status_message = format!("Error: {}", err);
|
||||
}
|
||||
@@ -1318,13 +1346,35 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
state.config.window_y = Some(position.y as i32);
|
||||
}
|
||||
AppMessage::EventOccurred(Event::Window(iced::window::Event::CloseRequested)) => {
|
||||
if state.closing {
|
||||
return Task::none();
|
||||
}
|
||||
// We took over the close path (exit_on_close_request:false) so we can
|
||||
// persist the final window size + position before quitting. Both are
|
||||
// already mirrored into config by the Resized/Moved handlers above.
|
||||
// persist the final window size + position and give core a chance to
|
||||
// leave the room/finalize recordings before quitting.
|
||||
state.config.save();
|
||||
return iced::exit();
|
||||
state.closing = true;
|
||||
state.status_message = "Shutting down...".to_string();
|
||||
let tx = state.controller.command_sender();
|
||||
return Task::batch(vec![
|
||||
Task::perform(
|
||||
async move { tx.send(CoreCommand::Shutdown).await.is_ok() },
|
||||
AppMessage::ShutdownCommandSent,
|
||||
),
|
||||
shutdown_timeout_task(),
|
||||
]);
|
||||
}
|
||||
AppMessage::EventOccurred(_) => {}
|
||||
AppMessage::ShutdownCommandSent(sent) => {
|
||||
if !sent {
|
||||
return iced::exit();
|
||||
}
|
||||
}
|
||||
AppMessage::ShutdownTimeout => {
|
||||
if state.closing {
|
||||
return iced::exit();
|
||||
}
|
||||
}
|
||||
AppMessage::NavigateToSettings => {
|
||||
state.current_screen = Screen::Settings;
|
||||
state.layout_picker_open = false;
|
||||
|
||||
@@ -11,6 +11,10 @@ pub enum CoreCommand {
|
||||
/// for a NEW room; it's ignored when joining (the label rides in the ticket).
|
||||
Join { name: String, ticket: String, room_name: String, input_device: Option<String>, output_device: Option<String>, echo_cancellation: bool, avatar: crate::avatar::Avatar },
|
||||
Leave,
|
||||
/// Orderly app shutdown: finalize recordings, leave any active room, stop local
|
||||
/// audio/screen-share work, close the persistent network stack, then ack with
|
||||
/// [`UiEvent::ShutdownComplete`].
|
||||
Shutdown,
|
||||
ToggleMute,
|
||||
/// Change our avatar (W4) and re-announce it to the room over presence.
|
||||
SetAvatar(crate::avatar::Avatar),
|
||||
@@ -126,5 +130,7 @@ pub enum UiEvent {
|
||||
/// Discoverable. Distinct from a user-driven change so the GUI knows to update
|
||||
/// without having issued the command itself.
|
||||
PresenceModeReverted { mode: PresenceMode },
|
||||
/// Core finished orderly app shutdown and the GUI can exit.
|
||||
ShutdownComplete,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
@@ -50,6 +50,12 @@ impl CoreController {
|
||||
pub fn send(&self, cmd: CoreCommand) -> bool {
|
||||
self.cmd_tx.try_send(cmd).is_ok()
|
||||
}
|
||||
|
||||
/// Clone the command sender for asynchronous one-shot sends that should wait
|
||||
/// for channel capacity instead of failing immediately on a full queue.
|
||||
pub fn command_sender(&self) -> mpsc::Sender<CoreCommand> {
|
||||
self.cmd_tx.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a peer may stay "reconnecting" after a transient drop before we give
|
||||
@@ -856,6 +862,23 @@ async fn run_core_loop(
|
||||
}
|
||||
};
|
||||
match cmd {
|
||||
CoreCommand::Shutdown => {
|
||||
crate::log_msg("Core shutdown requested");
|
||||
// Finalize recordings while capture/mixer feeders are still alive.
|
||||
stop_recording(&recorder, &is_recording, &multitrack, &is_multitrack, &ui_tx).await;
|
||||
stop_mic_monitor(&audio_backend, mic_monitor.take());
|
||||
|
||||
if let Some(session) = active_session.take() {
|
||||
session.shutdown(audio_backend.clone()).await;
|
||||
net.audio_router.clear();
|
||||
}
|
||||
*current_room.lock().unwrap() = None;
|
||||
|
||||
net.shutdown().await;
|
||||
let _ = ui_tx.send(UiEvent::ShutdownComplete).await;
|
||||
break;
|
||||
}
|
||||
|
||||
CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => {
|
||||
current_name = name.clone();
|
||||
current_avatar = avatar;
|
||||
|
||||
@@ -61,8 +61,18 @@ Reason for not implementing: the current `run_playback` / `run_capture` code doe
|
||||
|
||||
Unverified: the same-identity peer restart has not been exercised in a live 2-machine call; the WAV fix is counter/size-field tested, not a real >12h recording.
|
||||
|
||||
## Backlog A14 - orderly window-close shutdown
|
||||
|
||||
- Added `CoreCommand::Shutdown` and `UiEvent::ShutdownComplete`.
|
||||
- Window close now saves config, marks the GUI as closing, asynchronously queues `Shutdown`, and exits only after the core acknowledges completion or after a 5-second fallback timeout.
|
||||
- Core shutdown finalizes active mixed/multitrack recordings before session teardown, stops the standalone mic monitor, runs `ActiveSession::shutdown()` for active calls, clears room presence/routing, closes the persistent network stack, sends `ShutdownComplete`, and ends the core loop.
|
||||
- The shutdown command is queued with an awaited `mpsc::Sender::send` task instead of the best-effort `try_send`, so a full command queue does not immediately drop the close command.
|
||||
|
||||
Unverified: actual GUI window-close behavior during a live call/recording still needs a manual run; tests/builds only prove the path compiles and existing unit coverage still passes.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo check` passed.
|
||||
- `cargo test --lib` passed: 288 passed, 0 failed, 2 ignored.
|
||||
- `cargo clippy --all-targets` passed.
|
||||
- `cargo build --release` passed.
|
||||
|
||||
Reference in New Issue
Block a user