From 874bc085fe35515c05200a1dd231b172af816846 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 14 Mar 2020 09:08:02 +1000 Subject: [PATCH] support opening and closing the DB while keeping backend alive This is safer than just dropping the backend, as .close() will block if something else is holding the mutex. Also means we can drop the extra I18nBackend code. Media syncing still needs fixing. --- proto/backend.proto | 22 ++++++---- pylib/anki/collection.py | 3 +- pylib/anki/rsbackend.py | 30 ++++++++------ pylib/anki/storage.py | 14 +++++-- rslib/src/backend/mod.rs | 86 ++++++++++++++++++++++++++-------------- rslib/src/err.rs | 3 ++ 6 files changed, 103 insertions(+), 55 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 16eadd120..18b6c174c 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -7,13 +7,9 @@ package backend_proto; message Empty {} message BackendInit { - string collection_path = 1; - string media_folder_path = 2; - string media_db_path = 3; - repeated string preferred_langs = 4; - string locale_folder_path = 5; - string log_path = 6; - bool server = 7; + repeated string preferred_langs = 1; + string locale_folder_path = 2; + bool server = 3; } message I18nBackendInit { @@ -45,6 +41,8 @@ message BackendInput { CongratsLearnMsgIn congrats_learn_msg = 33; Empty empty_trash = 34; Empty restore_trash = 35; + OpenCollectionIn open_collection = 36; + Empty close_collection = 37; } } @@ -73,6 +71,8 @@ message BackendOutput { Empty trash_media_files = 29; Empty empty_trash = 34; Empty restore_trash = 35; + Empty open_collection = 36; + Empty close_collection = 37; BackendError error = 2047; } @@ -91,7 +91,6 @@ message BackendError { SyncError sync_error = 7; // user interrupted operation Empty interrupted = 8; - Empty collection_not_open = 9; } } @@ -326,3 +325,10 @@ message CongratsLearnMsgIn { float next_due = 1; uint32 remaining = 2; } + +message OpenCollectionIn { + string collection_path = 1; + string media_folder_path = 2; + string media_db_path = 3; + string log_path = 4; +} diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 680b98434..3408f2e2a 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -256,8 +256,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""", self.save(trx=False) if not self.server: self.db.execute("pragma journal_mode = delete") - self.db = None - self.backend = None + self.backend.close_collection() self.media.close() self._closeLog() diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index a15444a53..5b57224cc 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -200,22 +200,11 @@ def _on_progress(progress_bytes: bytes) -> bool: class RustBackend: - def __init__( - self, - col_path: str, - media_folder_path: str, - media_db_path: str, - log_path: str, - server: bool, - ) -> None: + def __init__(self, server: bool = False) -> None: ftl_folder = os.path.join(anki.lang.locale_folder, "fluent") init_msg = pb.BackendInit( - collection_path=col_path, - media_folder_path=media_folder_path, - media_db_path=media_db_path, locale_folder_path=ftl_folder, preferred_langs=[anki.lang.currentLang], - log_path=log_path, server=server, ) self._backend = ankirspy.open_backend(init_msg.SerializeToString()) @@ -234,6 +223,23 @@ class RustBackend: else: return output + def open_collection( + self, col_path: str, media_folder_path: str, media_db_path: str, log_path: str + ): + self._run_command( + pb.BackendInput( + open_collection=pb.OpenCollectionIn( + collection_path=col_path, + media_folder_path=media_folder_path, + media_db_path=media_db_path, + log_path=log_path, + ) + ) + ) + + def close_collection(self): + self._run_command(pb.BackendInput(close_collection=pb.Empty())) + def template_requirements( self, template_fronts: List[str], field_map: Dict[str, int] ) -> AllTemplateReqs: diff --git a/pylib/anki/storage.py b/pylib/anki/storage.py index 10dca3cd1..f19c4d327 100644 --- a/pylib/anki/storage.py +++ b/pylib/anki/storage.py @@ -27,18 +27,24 @@ class ServerData: minutes_west: Optional[int] = None -def Collection(path: str, server: Optional[ServerData] = None) -> _Collection: +def Collection( + path: str, + backend: Optional[RustBackend] = None, + server: Optional[ServerData] = None, +) -> _Collection: "Open a new or existing collection. Path must be unicode." assert path.endswith(".anki2") + if backend is None: + backend = RustBackend(server=server is not None) + (media_dir, media_db) = media_paths_from_col_path(path) log_path = "" if not server: log_path = path.replace(".anki2", "2.log") path = os.path.abspath(path) + # connect - backend = RustBackend( - path, media_dir, media_db, log_path, server=server is not None - ) + backend.open_collection(path, media_dir, media_db, log_path) db = DBProxy(weakref.proxy(backend), path) # initial setup required? diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 2b0957865..0025e6f97 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -35,6 +35,7 @@ pub struct Backend { progress_callback: Option, i18n: I18n, log: Logger, + server: bool, } enum Progress<'a> { @@ -56,7 +57,8 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { } AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }), AnkiError::Interrupted => V::Interrupted(Empty {}), - AnkiError::CollectionNotOpen => V::CollectionNotOpen(pb::Empty {}), + AnkiError::CollectionNotOpen => V::InvalidInput(pb::Empty {}), + AnkiError::CollectionAlreadyOpen => V::InvalidInput(pb::Empty {}), }; pb::BackendError { @@ -105,46 +107,24 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result { Err(_) => return Err("couldn't decode init request".into()), }; - let mut path = input.collection_path.clone(); - path.push_str(".log"); - - let log_path = match input.log_path.as_str() { - "" => None, - path => Some(path), - }; - let logger = - default_logger(log_path).map_err(|e| format!("Unable to open log file: {:?}", e))?; - let i18n = I18n::new( &input.preferred_langs, input.locale_folder_path, log::terminal(), ); - let col = open_collection( - input.collection_path, - input.media_folder_path, - input.media_db_path, - input.server, - i18n.clone(), - logger.clone(), - ) - .map_err(|e| format!("Unable to open collection: {:?}", e))?; - - match Backend::new(col, i18n, logger) { - Ok(backend) => Ok(backend), - Err(e) => Err(format!("{:?}", e)), - } + Ok(Backend::new(i18n, log::terminal(), input.server)) } impl Backend { - pub fn new(col: Collection, i18n: I18n, log: Logger) -> Result { - Ok(Backend { - col: Arc::new(Mutex::new(Some(col))), + pub fn new(i18n: I18n, log: Logger, server: bool) -> Backend { + Backend { + col: Arc::new(Mutex::new(None)), progress_callback: None, i18n, log, - }) + server, + } } pub fn i18n(&self) -> &I18n { @@ -259,9 +239,57 @@ impl Backend { self.restore_trash()?; OValue::RestoreTrash(Empty {}) } + Value::OpenCollection(input) => { + self.open_collection(input)?; + OValue::OpenCollection(Empty {}) + } + Value::CloseCollection(_) => { + self.close_collection()?; + OValue::CloseCollection(Empty {}) + } }) } + fn open_collection(&self, input: pb::OpenCollectionIn) -> Result<()> { + let mut mutex = self.col.lock().unwrap(); + if mutex.is_some() { + return Err(AnkiError::CollectionAlreadyOpen); + } + + let mut path = input.collection_path.clone(); + path.push_str(".log"); + + let log_path = match input.log_path.as_str() { + "" => None, + path => Some(path), + }; + let logger = default_logger(log_path)?; + + let col = open_collection( + input.collection_path, + input.media_folder_path, + input.media_db_path, + self.server, + self.i18n.clone(), + logger, + )?; + + *mutex = Some(col); + + Ok(()) + } + + fn close_collection(&self) -> Result<()> { + let mut mutex = self.col.lock().unwrap(); + if mutex.is_none() { + return Err(AnkiError::CollectionNotOpen); + } + + *mutex = None; + + Ok(()) + } + fn fire_progress_callback(&self, progress: Progress) -> bool { if let Some(cb) = &self.progress_callback { let bytes = progress_to_proto_bytes(progress, &self.i18n); diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 1ec149bc4..8208370d5 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -36,6 +36,9 @@ pub enum AnkiError { #[fail(display = "Operation requires an open collection.")] CollectionNotOpen, + + #[fail(display = "Close the existing collection first.")] + CollectionAlreadyOpen, } // error helpers