/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::{cell::RefCell, fmt::Write, mem, sync::Arc};
use atomic_refcell::AtomicRefCell;
use dogear::Store;
use log::LevelFilter;
use moz_task::{Task, TaskRunnable, ThreadPtrHandle, ThreadPtrHolder};
use nserror::{nsresult, NS_ERROR_NOT_AVAILABLE, NS_OK};
use nsstring::nsString;
use storage::Conn;
use xpcom::{
interfaces::{
mozIPlacesPendingOperation, mozIServicesLogSink, mozIStorageConnection,
mozISyncedBookmarksMirrorCallback, mozISyncedBookmarksMirrorProgressListener,
},
RefPtr, XpCom,
};
use crate::driver::{AbortController, Driver, Logger};
use crate::error;
use crate::store;
#[xpcom(implement(mozISyncedBookmarksMerger), nonatomic)]
pub struct SyncedBookmarksMerger {
db: RefCell>,
logger: RefCell >>,
}
impl SyncedBookmarksMerger {
pub fn new() -> RefPtr {
SyncedBookmarksMerger::allocate(InitSyncedBookmarksMerger {
db: RefCell::default(),
logger: RefCell::default(),
})
}
xpcom_method!(get_db => GetDb() -> *const mozIStorageConnection);
fn get_db(&self) -> Result, nsresult> {
self.db
.borrow()
.as_ref()
.map(|db| RefPtr::new(db.connection()))
.ok_or(NS_OK)
}
xpcom_method!(set_db => SetDb(connection: *const mozIStorageConnection));
fn set_db(&self, connection: Option<&mozIStorageConnection>) -> Result<(), nsresult> {
self.db
.replace(connection.map(|connection| Conn::wrap(RefPtr::new(connection))));
Ok(())
}
xpcom_method!(get_logger => GetLogger() -> *const mozIServicesLogSink);
fn get_logger(&self) -> Result, nsresult> {
match *self.logger.borrow() {
Some(ref logger) => Ok(logger.clone()),
None => Err(NS_OK),
}
}
xpcom_method!(set_logger => SetLogger(logger: *const mozIServicesLogSink));
fn set_logger(&self, logger: Option<&mozIServicesLogSink>) -> Result<(), nsresult> {
self.logger.replace(logger.map(RefPtr::new));
Ok(())
}
xpcom_method!(
merge => Merge(
local_time_seconds: i64,
remote_time_seconds: i64,
callback: *const mozISyncedBookmarksMirrorCallback
) -> *const mozIPlacesPendingOperation
);
fn merge(
&self,
local_time_seconds: i64,
remote_time_seconds: i64,
callback: &mozISyncedBookmarksMirrorCallback,
) -> Result, nsresult> {
let callback = RefPtr::new(callback);
let db = match *self.db.borrow() {
Some(ref db) => db.clone(),
None => return Err(NS_ERROR_NOT_AVAILABLE),
};
let logger = &*self.logger.borrow();
let async_thread = db.thread()?;
let controller = Arc::new(AbortController::default());
let task = MergeTask::new(
&db,
Arc::clone(&controller),
logger.as_ref().cloned(),
local_time_seconds,
remote_time_seconds,
callback,
)?;
let runnable = TaskRunnable::new(
"bookmark_sync::SyncedBookmarksMerger::merge",
Box::new(task),
)?;
TaskRunnable::dispatch(runnable, &async_thread)?;
let op = MergeOp::new(controller);
Ok(RefPtr::new(op.coerce()))
}
xpcom_method!(reset => Reset());
fn reset(&self) -> Result<(), nsresult> {
mem::drop(self.db.borrow_mut().take());
mem::drop(self.logger.borrow_mut().take());
Ok(())
}
}
struct MergeTask {
db: Conn,
controller: Arc,
max_log_level: LevelFilter,
logger: Option>,
local_time_millis: i64,
remote_time_millis: i64,
progress: Option>,
callback: ThreadPtrHandle,
result: AtomicRefCell>,
}
impl MergeTask {
fn new(
db: &Conn,
controller: Arc,
logger: Option>,
local_time_seconds: i64,
remote_time_seconds: i64,
callback: RefPtr,
) -> Result {
let max_log_level = logger
.as_ref()
.and_then(|logger| {
let mut level = 0i16;
unsafe { logger.GetMaxLevel(&mut level) }.to_result().ok()?;
Some(level)
})
.map(|level| match level {
mozIServicesLogSink::LEVEL_ERROR => LevelFilter::Error,
mozIServicesLogSink::LEVEL_WARN => LevelFilter::Warn,
mozIServicesLogSink::LEVEL_DEBUG => LevelFilter::Debug,
mozIServicesLogSink::LEVEL_TRACE => LevelFilter::Trace,
_ => LevelFilter::Off,
})
.unwrap_or(LevelFilter::Off);
let logger = match logger {
Some(logger) => Some(ThreadPtrHolder::new(cstr!("mozIServicesLogSink"), logger)?),
None => None,
};
let progress = callback
.query_interface::()
.and_then(|p| {
ThreadPtrHolder::new(cstr!("mozISyncedBookmarksMirrorProgressListener"), p).ok()
});
Ok(MergeTask {
db: db.clone(),
controller,
max_log_level,
logger,
local_time_millis: local_time_seconds * 1000,
remote_time_millis: remote_time_seconds * 1000,
progress,
callback: ThreadPtrHolder::new(cstr!("mozISyncedBookmarksMirrorCallback"), callback)?,
result: AtomicRefCell::new(Err(error::Error::DidNotRun)),
})
}
fn merge(&self) -> error::Result {
let mut db = self.db.clone();
if db.transaction_in_progress()? {
// If a transaction is already open, we can avoid an unnecessary
// merge, since we won't be able to apply the merged tree back to
// Places. This is common, especially if the user makes lots of
// changes at once. In that case, our merge task might run in the
// middle of a `Sqlite.sys.mjs` transaction, and fail when we try to
// open our own transaction in `Store::apply`. Since the local
// tree might be in an inconsistent state, we can't safely update
// Places.
return Err(error::Error::StorageBusy);
}
let log = Logger::new(self.max_log_level, self.logger.clone());
let driver = Driver::new(log, self.progress.clone());
let mut store = store::Store::new(
&mut db,
&driver,
&self.controller,
self.local_time_millis,
self.remote_time_millis,
);
store.validate()?;
store.prepare()?;
let status = store.merge_with_driver(&driver, &*self.controller)?;
Ok(status)
}
}
impl Task for MergeTask {
fn run(&self) {
*self.result.borrow_mut() = self.merge();
}
fn done(&self) -> Result<(), nsresult> {
let callback = self.callback.get().unwrap();
match mem::replace(&mut *self.result.borrow_mut(), Err(error::Error::DidNotRun)) {
Ok(status) => unsafe { callback.HandleSuccess(status.into()) },
Err(err) => {
let mut message = nsString::new();
write!(message, "{}", err).unwrap();
unsafe { callback.HandleError(err.into(), &*message) }
}
}
.to_result()
}
}
#[xpcom(implement(mozIPlacesPendingOperation), atomic)]
pub struct MergeOp {
controller: Arc,
}
impl MergeOp {
pub fn new(controller: Arc) -> RefPtr {
MergeOp::allocate(InitMergeOp { controller })
}
xpcom_method!(cancel => Cancel());
fn cancel(&self) -> Result<(), nsresult> {
self.controller.abort();
Ok(())
}
}