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 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;
}

View file

@ -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()

View file

@ -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:

View file

@ -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?

View file

@ -35,6 +35,7 @@ pub struct Backend {
progress_callback: Option<ProtoProgressCallback>,
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<Backend, String> {
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<Backend> {
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);

View file

@ -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