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.
This commit is contained in:
Damien Elmes 2020-03-14 09:08:02 +10:00
parent 94e4c40ebf
commit 874bc085fe
6 changed files with 103 additions and 55 deletions

View file

@ -7,13 +7,9 @@ package backend_proto;
message Empty {} message Empty {}
message BackendInit { message BackendInit {
string collection_path = 1; repeated string preferred_langs = 1;
string media_folder_path = 2; string locale_folder_path = 2;
string media_db_path = 3; bool server = 3;
repeated string preferred_langs = 4;
string locale_folder_path = 5;
string log_path = 6;
bool server = 7;
} }
message I18nBackendInit { message I18nBackendInit {
@ -45,6 +41,8 @@ message BackendInput {
CongratsLearnMsgIn congrats_learn_msg = 33; CongratsLearnMsgIn congrats_learn_msg = 33;
Empty empty_trash = 34; Empty empty_trash = 34;
Empty restore_trash = 35; Empty restore_trash = 35;
OpenCollectionIn open_collection = 36;
Empty close_collection = 37;
} }
} }
@ -73,6 +71,8 @@ message BackendOutput {
Empty trash_media_files = 29; Empty trash_media_files = 29;
Empty empty_trash = 34; Empty empty_trash = 34;
Empty restore_trash = 35; Empty restore_trash = 35;
Empty open_collection = 36;
Empty close_collection = 37;
BackendError error = 2047; BackendError error = 2047;
} }
@ -91,7 +91,6 @@ message BackendError {
SyncError sync_error = 7; SyncError sync_error = 7;
// user interrupted operation // user interrupted operation
Empty interrupted = 8; Empty interrupted = 8;
Empty collection_not_open = 9;
} }
} }
@ -326,3 +325,10 @@ message CongratsLearnMsgIn {
float next_due = 1; float next_due = 1;
uint32 remaining = 2; uint32 remaining = 2;
} }
message OpenCollectionIn {
string collection_path = 1;
string media_folder_path = 2;
string media_db_path = 3;
string log_path = 4;
}

View file

@ -256,8 +256,7 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
self.save(trx=False) self.save(trx=False)
if not self.server: if not self.server:
self.db.execute("pragma journal_mode = delete") self.db.execute("pragma journal_mode = delete")
self.db = None self.backend.close_collection()
self.backend = None
self.media.close() self.media.close()
self._closeLog() self._closeLog()

View file

@ -200,22 +200,11 @@ def _on_progress(progress_bytes: bytes) -> bool:
class RustBackend: class RustBackend:
def __init__( def __init__(self, server: bool = False) -> None:
self,
col_path: str,
media_folder_path: str,
media_db_path: str,
log_path: str,
server: bool,
) -> None:
ftl_folder = os.path.join(anki.lang.locale_folder, "fluent") ftl_folder = os.path.join(anki.lang.locale_folder, "fluent")
init_msg = pb.BackendInit( 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, locale_folder_path=ftl_folder,
preferred_langs=[anki.lang.currentLang], preferred_langs=[anki.lang.currentLang],
log_path=log_path,
server=server, server=server,
) )
self._backend = ankirspy.open_backend(init_msg.SerializeToString()) self._backend = ankirspy.open_backend(init_msg.SerializeToString())
@ -234,6 +223,23 @@ class RustBackend:
else: else:
return output 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( def template_requirements(
self, template_fronts: List[str], field_map: Dict[str, int] self, template_fronts: List[str], field_map: Dict[str, int]
) -> AllTemplateReqs: ) -> AllTemplateReqs:

View file

@ -27,18 +27,24 @@ class ServerData:
minutes_west: Optional[int] = None 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." "Open a new or existing collection. Path must be unicode."
assert path.endswith(".anki2") 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) (media_dir, media_db) = media_paths_from_col_path(path)
log_path = "" log_path = ""
if not server: if not server:
log_path = path.replace(".anki2", "2.log") log_path = path.replace(".anki2", "2.log")
path = os.path.abspath(path) path = os.path.abspath(path)
# connect # connect
backend = RustBackend( backend.open_collection(path, media_dir, media_db, log_path)
path, media_dir, media_db, log_path, server=server is not None
)
db = DBProxy(weakref.proxy(backend), path) db = DBProxy(weakref.proxy(backend), path)
# initial setup required? # initial setup required?

View file

@ -35,6 +35,7 @@ pub struct Backend {
progress_callback: Option<ProtoProgressCallback>, progress_callback: Option<ProtoProgressCallback>,
i18n: I18n, i18n: I18n,
log: Logger, log: Logger,
server: bool,
} }
enum Progress<'a> { 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::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }),
AnkiError::Interrupted => V::Interrupted(Empty {}), 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 { pb::BackendError {
@ -105,46 +107,24 @@ pub fn init_backend(init_msg: &[u8]) -> std::result::Result<Backend, String> {
Err(_) => return Err("couldn't decode init request".into()), 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( let i18n = I18n::new(
&input.preferred_langs, &input.preferred_langs,
input.locale_folder_path, input.locale_folder_path,
log::terminal(), log::terminal(),
); );
let col = open_collection( Ok(Backend::new(i18n, log::terminal(), input.server))
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)),
}
} }
impl Backend { impl Backend {
pub fn new(col: Collection, i18n: I18n, log: Logger) -> Result<Backend> { pub fn new(i18n: I18n, log: Logger, server: bool) -> Backend {
Ok(Backend { Backend {
col: Arc::new(Mutex::new(Some(col))), col: Arc::new(Mutex::new(None)),
progress_callback: None, progress_callback: None,
i18n, i18n,
log, log,
}) server,
}
} }
pub fn i18n(&self) -> &I18n { pub fn i18n(&self) -> &I18n {
@ -259,9 +239,57 @@ impl Backend {
self.restore_trash()?; self.restore_trash()?;
OValue::RestoreTrash(Empty {}) 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 { fn fire_progress_callback(&self, progress: Progress) -> bool {
if let Some(cb) = &self.progress_callback { if let Some(cb) = &self.progress_callback {
let bytes = progress_to_proto_bytes(progress, &self.i18n); let bytes = progress_to_proto_bytes(progress, &self.i18n);

View file

@ -36,6 +36,9 @@ pub enum AnkiError {
#[fail(display = "Operation requires an open collection.")] #[fail(display = "Operation requires an open collection.")]
CollectionNotOpen, CollectionNotOpen,
#[fail(display = "Close the existing collection first.")]
CollectionAlreadyOpen,
} }
// error helpers // error helpers