Skip to content
11 changes: 11 additions & 0 deletions linux/crates/app/src/application/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ pub(super) enum RestoreState {
/// The record is being read, and the windows open once it is.
Loading,
Done,
/// An approved quit is writing the record. A launch meanwhile asks
/// for it to be restored once written: read earlier, it would race
/// the write, and the quit then empties the list it restored into.
Quitting {
relaunched: bool,
},
}

#[glib::object_subclass]
Expand Down Expand Up @@ -89,6 +95,7 @@ impl ApplicationImpl for TableProApplication {
match self.restore.get() {
RestoreState::NotAttempted => app.restore_session(),
RestoreState::Loading => {}
RestoreState::Quitting { .. } => self.restore.set(RestoreState::Quitting { relaunched: true }),
RestoreState::Done => {
app.open_window(WindowLaunch::Picker);
}
Expand All @@ -108,6 +115,10 @@ impl ApplicationImpl for TableProApplication {
self.obj().restore_session();
}
RestoreState::Loading => self.pending_files.borrow_mut().extend_from_slice(files),
RestoreState::Quitting { .. } => {
self.pending_files.borrow_mut().extend_from_slice(files);
self.restore.set(RestoreState::Quitting { relaunched: true });
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion linux/crates/app/src/application/open_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ impl TableProApplication {
};
let (path, uri) = (file.path(), file.uri().to_string());
let connections = services.storage.connections().clone();
let drivers = services.drivers.clone();
let resolving = services
.tasks
.spawn_blocking_task(move || resolve_file_blocking(path, uri, &connections));
.spawn_blocking_task(move || resolve_file_blocking(path, uri, &connections, &drivers));
// An instance D-Bus started for this file would otherwise exit
// before its window opens.
let hold = self.hold();
Expand Down
15 changes: 12 additions & 3 deletions linux/crates/app/src/application/quit_sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use crate::workspace::{CloseDecision, CloseReason};
///
/// An approved quit can leave the process running with no window while
/// a session closes, and a second launch then activates it. That
/// activate restores the record as a new process would.
/// activate restores the record as a new process would, after the
/// record is written if it lands while the quit writes it.
pub struct QuitSequence;

impl QuitSequence {
Expand All @@ -49,11 +50,19 @@ impl QuitSequence {
if let Some(services) = &services {
match decision {
CloseDecision::Approved => {
let restore = &app.imp().restore;
let restorable = restore.get() == RestoreState::Done;
if restorable {
restore.set(RestoreState::Quitting { relaunched: false });
}
services.open_sessions().flush().await;
services.open_sessions().end_quit();
let restore = &app.imp().restore;
if restore.get() == RestoreState::Done {
if restorable {
let relaunched = restore.get() == RestoreState::Quitting { relaunched: true };
restore.set(RestoreState::NotAttempted);
if relaunched {
app.restore_session();
}
}
}
CloseDecision::Cancelled => services.open_sessions().thaw(|key| {
Expand Down
46 changes: 46 additions & 0 deletions linux/crates/app/src/application/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,52 @@ fn an_activate_after_an_approved_quit_restores_its_record() {
drop(closing);
}

/// A launch that lands while an approved quit writes the record waits
/// for the write, then reopens what it wrote, once.
#[gtk4::test]
fn an_activate_during_the_quit_flush_restores_the_record_once_written() {
let test = TestApp::started("QuitFlushActivate");
let (first, second) = (test.database("first.db"), test.database("second.db"));
test.record(&[first.clone(), second.clone()]);
test.app.activate();
test.wait_for_sessions(&[first.clone(), second.clone()]);
let services = test.services();
let closing = services.hold().acquire();
let decision = Rc::new(std::cell::RefCell::new(None));
let (app, slot) = (test.app.clone(), decision.clone());
glib::spawn_future_local(async move {
slot.replace(Some(QuitSequence::run(app).await));
});

let mut activated_during_flush = false;
wait_until(WAIT, || {
let flushing =
decision.borrow().is_none() && test.app.windows().is_empty() && test.app.imp().quit_asking.get().is_none();
if flushing && !activated_during_flush {
test.app.activate();
test.app.activate();
activated_during_flush = true;
}
decision.borrow().is_some()
})
.expect("the quit to finish");

assert!(
activated_during_flush,
"the quit ended before a launch could land in its flush"
);
assert_eq!(*decision.borrow(), Some(CloseDecision::Approved));
test.wait_for_sessions(&[first.clone(), second.clone()]);
let _ = wait_until(Duration::from_millis(300), || test.app.windows().len() > 2);
assert_eq!(
test.app.windows().len(),
2,
"a picker opened beside the restored windows"
);
assert_eq!(services.open_sessions().keys(), [first, second]);
drop(closing);
}

#[gtk4::test]
fn a_cancelled_quit_forgets_a_restore_that_failed_during_it() {
let test = TestApp::started("QuitFailedRestore");
Expand Down
66 changes: 56 additions & 10 deletions linux/crates/app/src/services/file_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use std::path::{Path, PathBuf};

use tablepro_core::DriverRegistry;
use tablepro_session::SessionKey;
use tablepro_session::registry::FileIdentity;
use tablepro_storage::{ConnectionStore, SavedConnection};
Expand Down Expand Up @@ -51,7 +52,12 @@ pub fn resolve_file_identity_blocking(path: Option<&Path>, uri: &str) -> Result<

/// Resolve the file, then find whether a saved connection opens the
/// same one.
pub fn resolve_file_blocking(path: Option<PathBuf>, uri: String, connections: &ConnectionStore) -> ResolvedFile {
pub fn resolve_file_blocking(
path: Option<PathBuf>,
uri: String,
connections: &ConnectionStore,
drivers: &DriverRegistry,
) -> ResolvedFile {
let target = resolve_file_identity_blocking(path.as_deref(), &uri).map(|identity| {
let saved = match connections.load_blocking() {
Ok(saved) => saved.to_vec(),
Expand All @@ -60,7 +66,12 @@ pub fn resolve_file_blocking(path: Option<PathBuf>, uri: String, connections: &C
Vec::new()
}
};
match SessionKey::for_file(&identity, &saved) {
let is_file_driver = |id: &str| {
drivers
.get(id)
.is_some_and(|driver| driver.capabilities().is_file_based())
};
match SessionKey::for_file(&identity, &saved, is_file_driver) {
SessionKey::Saved(id) => match saved.into_iter().find(|connection| connection.id == id) {
Some(connection) => FileTarget::Saved(Box::new(connection)),
None => FileTarget::File(identity),
Expand All @@ -73,6 +84,8 @@ pub fn resolve_file_blocking(path: Option<PathBuf>, uri: String, connections: &C

#[cfg(test)]
mod tests {
use tablepro_test_support::{ConnectionScript, FakeDriver};

use super::*;

#[test]
Expand All @@ -89,11 +102,24 @@ mod tests {
));
}

#[test]
fn a_saved_file_opens_its_saved_connection() {
fn drivers(capabilities: &'static tablepro_core::DriverCapabilities) -> DriverRegistry {
let mut drivers = DriverRegistry::new();
drivers.register(std::sync::Arc::new(FakeDriver::connecting(
capabilities,
ConnectionScript::default(),
)));
drivers
}

/// `database` saved as a connection of the fake driver, and what
/// opening the file then resolves to.
fn resolve_saved(
database: &str,
capabilities: &'static tablepro_core::DriverCapabilities,
) -> (uuid::Uuid, ResolvedFile) {
let root = tempfile::tempdir().expect("tempdir");
let database = root.path().join("app.db");
std::fs::write(&database, b"").expect("seed");
let path = std::fs::canonicalize(root.path()).expect("canonical").join(database);
std::fs::write(&path, b"").expect("seed");
let store = ConnectionStore::new(&tablepro_storage::StoragePaths::under(
root.path(),
"tablepro-test",
Expand All @@ -102,10 +128,10 @@ mod tests {
let saved = SavedConnection {
id: uuid::Uuid::new_v4(),
name: "local".to_owned(),
driver_id: "sqlite".to_owned(),
driver_id: "fake".to_owned(),
host: String::new(),
port: 0,
database: database.display().to_string(),
database: path.display().to_string(),
username: String::new(),
tls: tablepro_core::TlsConfig::disabled(),
read_only: false,
Expand All @@ -116,12 +142,32 @@ mod tests {
group: None,
};
store.upsert_blocking(saved.clone()).expect("saved");
let uri = format!("file://{}", path.display());

(
saved.id,
resolve_file_blocking(Some(path), uri, &store, &drivers(capabilities)),
)
}

let resolved = resolve_file_blocking(Some(database.clone()), format!("file://{}", database.display()), &store);
#[test]
fn a_saved_file_opens_its_saved_connection() {
let (id, resolved) = resolve_saved("app.db", &FakeDriver::FILE_CAPABILITIES);

match resolved.target {
Ok(FileTarget::Saved(connection)) => assert_eq!(connection.id, saved.id),
Ok(FileTarget::Saved(connection)) => assert_eq!(connection.id, id),
other => panic!("the saved connection was not found: {other:?}"),
}
}

#[test]
fn a_server_whose_database_names_the_file_is_not_opened() {
let (_, resolved) = resolve_saved("shop", &FakeDriver::NETWORK_CAPABILITIES);

assert!(
matches!(resolved.target, Ok(FileTarget::File(_))),
"{:?}",
resolved.target
);
}
}
Loading
Loading