ref(jsonrpc): Getting backup provider QR code now blocks (#4198)

This changes the JSON-RPC APIs to get a QR code from the backup
provider to block.  It means once you have a (blocking) call to
provide_backup() you can call get_backup_qr() or get_backup_qr_svg()
and they will block until the QR code is available.

Calling get_backup_qr() or get_backup_qr_svg() when there is no backup
provider will immediately error.
This commit is contained in:
Floris Bruynooghe
2023-03-22 12:45:38 +01:00
committed by GitHub
parent 7ec3a1a9a2
commit e985588c6c
3 changed files with 107 additions and 54 deletions

View File

@@ -4,6 +4,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result}; use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts; pub use deltachat::accounts::Accounts;
use deltachat::qr::Qr;
use deltachat::{ use deltachat::{
chat::{ chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
@@ -29,7 +30,8 @@ use deltachat::{
webxdc::StatusUpdateSerial, webxdc::StatusUpdateSerial,
}; };
use sanitize_filename::is_sanitized; use sanitize_filename::is_sanitized;
use tokio::{fs, sync::RwLock}; use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use walkdir::WalkDir; use walkdir::WalkDir;
use yerpc::rpc; use yerpc::rpc;
@@ -57,21 +59,45 @@ use self::types::{
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult}; use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject; use crate::api::types::qr::QrObject;
#[derive(Debug)]
struct AccountState {
/// The Qr code for current [`CommandApi::provide_backup`] call.
///
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
/// `Pending` or `Ready`, otherwise `NoProvider`.
backup_provider_qr: watch::Sender<ProviderQr>,
}
impl Default for AccountState {
fn default() -> Self {
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
Self {
backup_provider_qr: tx,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommandApi { pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>, pub(crate) accounts: Arc<RwLock<Accounts>>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
} }
impl CommandApi { impl CommandApi {
pub fn new(accounts: Accounts) -> Self { pub fn new(accounts: Accounts) -> Self {
CommandApi { CommandApi {
accounts: Arc::new(RwLock::new(accounts)), accounts: Arc::new(RwLock::new(accounts)),
states: Arc::new(Mutex::new(BTreeMap::new())),
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self { pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi { accounts } CommandApi {
accounts,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
} }
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> { async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
@@ -83,6 +109,38 @@ impl CommandApi {
.ok_or_else(|| anyhow!("account with id {} not found", id))?; .ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc) Ok(sc)
} }
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
where
F: FnOnce(&AccountState) -> T,
{
let mut states = self.states.lock().await;
let state = states.entry(id).or_insert_with(Default::default);
with_state(state)
}
async fn inner_get_backup_qr(&self, account_id: u32) -> Result<Qr> {
let mut receiver = self
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
.await;
let val: ProviderQr = receiver.borrow_and_update().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => loop {
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
let val: ProviderQr = receiver.borrow().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => continue,
ProviderQr::Ready(qr) => break Ok(qr),
};
},
ProviderQr::Ready(qr) => Ok(qr),
}
}
} }
#[rpc(all_positional, ts_outdir = "typescript/generated")] #[rpc(all_positional, ts_outdir = "typescript/generated")]
@@ -115,7 +173,13 @@ impl CommandApi {
} }
async fn remove_account(&self, account_id: u32) -> Result<()> { async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts.write().await.remove_account(account_id).await self.accounts
.write()
.await
.remove_account(account_id)
.await?;
self.states.lock().await.remove(&account_id);
Ok(())
} }
async fn get_all_account_ids(&self) -> Vec<u32> { async fn get_all_account_ids(&self) -> Vec<u32> {
@@ -1358,31 +1422,35 @@ impl CommandApi {
/// ///
/// This **stops IO** while it is running. /// This **stops IO** while it is running.
/// ///
/// Returns once a remote device has retrieved the backup. /// Returns once a remote device has retrieved the backup, or is cancelled.
async fn provide_backup(&self, account_id: u32) -> Result<()> { async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
ctx.stop_io().await; self.with_state(account_id, |state| {
let provider = match imex::BackupProvider::prepare(&ctx).await { state.backup_provider_qr.send_replace(ProviderQr::Pending);
Ok(provider) => provider, })
Err(err) => { .await;
ctx.start_io().await;
return Err(err); let provider = imex::BackupProvider::prepare(&ctx).await?;
} self.with_state(account_id, |state| {
}; state
let res = provider.await; .backup_provider_qr
ctx.start_io().await; .send_replace(ProviderQr::Ready(provider.qr()));
res })
.await;
provider.await
} }
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`]. /// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
/// ///
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to /// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device. /// retrieve the backup and setup this second device.
///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> { async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?; let qr = self.inner_get_backup_qr(account_id).await?;
let qr = ctx
.backup_export_qr()
.ok_or(anyhow!("no backup being exported"))?;
qr::format_backup(&qr) qr::format_backup(&qr)
} }
@@ -1391,12 +1459,14 @@ impl CommandApi {
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to /// This QR code can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device. /// retrieve the backup and setup this second device.
/// ///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
///
/// Returns the QR code rendered as an SVG image. /// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> { async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?; let ctx = self.get_context(account_id).await?;
let qr = ctx let qr = self.inner_get_backup_qr(account_id).await?;
.backup_export_qr()
.ok_or(anyhow!("no backup being exported"))?;
generate_backup_qr(&ctx, &qr).await generate_backup_qr(&ctx, &qr).await
} }
@@ -1900,3 +1970,15 @@ async fn get_config(
.await .await
} }
} }
/// Whether a QR code for a BackupProvider is currently available.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum ProviderQr {
/// There is no provider, asking for a QR is an error.
NoProvider,
/// There is a provider, the QR code is pending.
Pending,
/// There is a provider and QR code.
Ready(Qr),
}

View File

@@ -23,7 +23,6 @@ use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey}; use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam; use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId}; use crate::message::{self, MessageState, MsgId};
use crate::qr::Qr;
use crate::quota::QuotaInfo; use crate::quota::QuotaInfo;
use crate::scheduler::SchedulerState; use crate::scheduler::SchedulerState;
use crate::sql::Sql; use crate::sql::Sql;
@@ -241,14 +240,6 @@ pub struct InnerContext {
/// If debug logging is enabled, this contains all necessary information /// If debug logging is enabled, this contains all necessary information
pub(crate) debug_logging: RwLock<Option<DebugLogging>>, pub(crate) debug_logging: RwLock<Option<DebugLogging>>,
/// QR code for currently running [`BackupProvider`].
///
/// This is only available if a backup export is currently running, it will also be
/// holding the ongoing process while running.
///
/// [`BackupProvider`]: crate::imex::BackupProvider
pub(crate) export_provider: std::sync::Mutex<Option<Qr>>,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -393,7 +384,6 @@ impl Context {
last_full_folder_scan: Mutex::new(None), last_full_folder_scan: Mutex::new(None),
last_error: std::sync::RwLock::new("".to_string()), last_error: std::sync::RwLock::new("".to_string()),
debug_logging: RwLock::new(None), debug_logging: RwLock::new(None),
export_provider: std::sync::Mutex::new(None),
}; };
let ctx = Context { let ctx = Context {
@@ -568,17 +558,6 @@ impl Context {
} }
} }
/// Returns the QR-code of the currently running [`BackupProvider`].
///
/// [`BackupProvider`]: crate::imex::BackupProvider
pub fn backup_export_qr(&self) -> Option<Qr> {
self.export_provider
.lock()
.expect("poisoned lock")
.as_ref()
.cloned()
}
/******************************************************************************* /*******************************************************************************
* UI chat/message related API * UI chat/message related API
******************************************************************************/ ******************************************************************************/

View File

@@ -126,16 +126,14 @@ impl BackupProvider {
let handle = { let handle = {
let context = context.clone(); let context = context.clone();
tokio::spawn(async move { tokio::spawn(async move {
let res = Self::watch_provider(&context, provider, cancel_token, dbfile).await; let res = Self::watch_provider(&context, provider, cancel_token).await;
context.free_ongoing().await; context.free_ongoing().await;
paused_guard.resume().await; paused_guard.resume().await;
drop(dbfile);
res res
}) })
}; };
let slf = Self { handle, ticket }; Ok(Self { handle, ticket })
let qr = slf.qr();
*context.export_provider.lock().expect("poisoned lock") = Some(qr);
Ok(slf)
} }
/// Creates the provider task. /// Creates the provider task.
@@ -189,7 +187,6 @@ impl BackupProvider {
context: &Context, context: &Context,
mut provider: Provider, mut provider: Provider,
cancel_token: Receiver<()>, cancel_token: Receiver<()>,
_dbfile: TempPathGuard,
) -> Result<()> { ) -> Result<()> {
// _dbfile exists so we can clean up the file once it is no longer needed // _dbfile exists so we can clean up the file once it is no longer needed
let mut events = provider.subscribe(); let mut events = provider.subscribe();
@@ -255,11 +252,6 @@ impl BackupProvider {
}, },
} }
}; };
context
.export_provider
.lock()
.expect("poisoned lock")
.take();
match &res { match &res {
Ok(_) => context.emit_event(SendProgress::Completed.into()), Ok(_) => context.emit_event(SendProgress::Completed.into()),
Err(err) => { Err(err) => {