1use super::TabsRecord;
6use crate::schema;
7use crate::storage::{ClientRemoteTabs, TABS_CLIENT_TTL};
8use crate::store::TabsStore;
9use anyhow::Result;
10use error_support::{debug, info, trace, warn};
11
12use std::collections::HashMap;
13use std::sync::{Arc, Mutex, RwLock, Weak};
14use sync15::bso::{IncomingBso, OutgoingBso, OutgoingEnvelope};
15use sync15::engine::{
16 CollSyncIds, CollectionRequest, EngineSyncAssociation, SyncEngine, SyncEngineId,
17};
18use sync15::{telemetry, ClientData, CollectionName, RemoteClient, ServerTimestamp};
19use sync_guid::Guid;
20
21lazy_static::lazy_static! {
23 static ref STORE_FOR_MANAGER: Mutex<Weak<TabsStore>> = Mutex::new(Weak::new());
25}
26
27pub fn get_registered_sync_engine(
30 engine_id: &SyncEngineId,
31) -> Option<Box<dyn sync15::engine::SyncEngine>> {
32 let weak = STORE_FOR_MANAGER.lock().unwrap();
33 match weak.upgrade() {
34 None => None,
35 Some(store) => match engine_id {
36 SyncEngineId::Tabs => Some(Box::new(TabsEngine::new(Arc::clone(&store)))),
37 _ => unreachable!("can't provide unknown engine: {}", engine_id),
40 },
41 }
42}
43
44impl ClientRemoteTabs {
45 pub(crate) fn from_record(
46 client_id: String,
47 last_modified: ServerTimestamp,
48 remote_client: &RemoteClient,
49 record: TabsRecord,
50 ) -> Self {
51 Self {
52 client_id,
53 client_name: remote_client.device_name.clone(),
54 device_type: remote_client.device_type,
55 last_modified: last_modified.as_millis(),
56 remote_tabs: record.tabs.into_iter().map(Into::into).collect(),
57 tab_groups: record
58 .tab_groups
59 .into_iter()
60 .map(|(n, v)| (n, v.into()))
61 .collect(),
62 windows: record
63 .windows
64 .into_iter()
65 .map(|(n, v)| (n, v.into()))
66 .collect(),
67 }
68 }
69}
70
71pub struct TabsEngine {
74 pub(super) store: Arc<TabsStore>,
75 pub local_id: RwLock<String>,
77}
78
79impl TabsEngine {
80 pub fn new(store: Arc<TabsStore>) -> Self {
81 Self {
82 store,
83 local_id: Default::default(),
84 }
85 }
86
87 fn set_last_sync(&self, last_sync: ServerTimestamp) -> Result<()> {
90 let mut storage = self.store.storage.lock().unwrap();
91 debug!("Updating last sync to {}", last_sync);
92 let last_sync_millis = last_sync.as_millis();
93 Ok(storage.put_meta(schema::LAST_SYNC_META_KEY, &last_sync_millis)?)
94 }
95}
96
97impl SyncEngine for TabsEngine {
98 fn collection_name(&self) -> CollectionName {
99 "tabs".into()
100 }
101
102 fn last_sync(&self) -> Result<Option<ServerTimestamp>> {
103 let mut storage = self.store.storage.lock().unwrap();
104 let millis = storage.get_meta::<i64>(schema::LAST_SYNC_META_KEY)?;
105 Ok(millis.map(ServerTimestamp))
106 }
107
108 fn reset_last_sync(&self) -> Result<()> {
109 self.set_last_sync(ServerTimestamp(0))
110 }
111
112 fn set_clients(&self, get_client_data: &dyn Fn() -> ClientData) -> Result<()> {
113 let mut storage = self.store.storage.lock().unwrap();
114 let client_data = get_client_data();
118 storage.put_meta(
119 schema::REMOTE_CLIENTS_KEY,
120 &serde_json::to_string(&client_data.recent_clients)?,
121 )?;
122 *self.local_id.write().unwrap() = client_data.local_client_id;
123 Ok(())
124 }
125
126 fn stage_incoming(
127 &self,
128 inbound: Vec<IncomingBso>,
129 telem: &mut telemetry::Engine,
130 ) -> Result<()> {
131 let local_id = &*self.local_id.read().unwrap();
133 let mut remote_tabs = Vec::with_capacity(inbound.len());
134
135 let mut incoming_telemetry = telemetry::EngineIncoming::new();
136 for incoming in inbound {
137 if incoming.envelope.id == *local_id {
138 continue;
140 }
141 let modified = incoming.envelope.modified;
142 let record = match incoming.into_content::<TabsRecord>().content() {
143 Some(record) => record,
144 None => {
145 warn!("Ignoring incoming invalid tab");
147 incoming_telemetry.failed(1);
148 continue;
149 }
150 };
151 incoming_telemetry.applied(1);
152 remote_tabs.push((record, modified));
153 }
154 telem.incoming(incoming_telemetry);
155 let mut storage = self.store.storage.lock().unwrap();
156 if !remote_tabs.is_empty() {
159 storage.replace_remote_tabs(&remote_tabs)?;
160 }
161 storage.remove_stale_clients()?;
162 storage.remove_old_pending_closures(&remote_tabs)?;
163 Ok(())
164 }
165
166 fn apply(
167 &self,
168 timestamp: ServerTimestamp,
169 _telem: &mut telemetry::Engine,
170 ) -> Result<Vec<OutgoingBso>> {
171 let local_id = &*self.local_id.read().unwrap();
173 if timestamp.0 != 0 {
175 self.set_last_sync(timestamp)?;
176 }
177
178 let mut storage = self.store.storage.lock().unwrap();
179 let remote_clients: HashMap<String, RemoteClient> = {
180 match storage.get_meta::<String>(schema::REMOTE_CLIENTS_KEY)? {
181 None => HashMap::default(),
182 Some(json) => serde_json::from_str(&json).unwrap(),
183 }
184 };
185
186 let Some(ref tabs_info) = *storage.local_tabs.borrow() else {
187 warn!("syncing without local tabs");
191 return Ok(vec![]);
192 };
193
194 let client_name = remote_clients
195 .get(local_id)
196 .map(|client| client.device_name.clone())
197 .unwrap_or_default();
198
199 let mut record = TabsRecord {
200 id: local_id.clone(),
201 client_name,
202 tabs: tabs_info
203 .tabs
204 .iter()
205 .map(Clone::clone)
206 .map(Into::into)
207 .collect(),
208 windows: tabs_info
209 .windows
210 .iter()
211 .map(|(n, v)| (n.clone(), v.clone().into()))
212 .collect(),
213 tab_groups: tabs_info
214 .tab_groups
215 .iter()
216 .map(|(n, v)| (n.clone(), v.clone().into()))
217 .collect(),
218 };
219 super::prepare_for_upload(&mut record);
220
221 trace!("outgoing {record}");
222 let envelope = OutgoingEnvelope {
223 id: local_id.as_str().into(),
224 ttl: Some(TABS_CLIENT_TTL),
225 ..Default::default()
226 };
227 Ok(vec![OutgoingBso::from_content(envelope, record)?])
229 }
230
231 fn set_uploaded(&self, new_timestamp: ServerTimestamp, ids: Vec<Guid>) -> Result<()> {
232 info!("sync uploaded {} records", ids.len());
233 self.set_last_sync(new_timestamp)?;
234 Ok(())
235 }
236
237 fn get_collection_request(
238 &self,
239 server_timestamp: ServerTimestamp,
240 ) -> Result<Option<CollectionRequest>> {
241 let since = self.last_sync()?.unwrap_or_default();
242 Ok(if since == server_timestamp {
243 None
244 } else {
245 Some(
246 CollectionRequest::new("tabs".into())
247 .full()
248 .newer_than(since),
249 )
250 })
251 }
252
253 fn reset(&self, assoc: &EngineSyncAssociation) -> Result<()> {
254 self.set_last_sync(ServerTimestamp(0))?;
255 let mut storage = self.store.storage.lock().unwrap();
256 storage.delete_meta(schema::REMOTE_CLIENTS_KEY)?;
257 storage.wipe_remote_tabs()?;
258 match assoc {
259 EngineSyncAssociation::Disconnected => {
260 storage.delete_meta(schema::GLOBAL_SYNCID_META_KEY)?;
261 storage.delete_meta(schema::COLLECTION_SYNCID_META_KEY)?;
262 }
263 EngineSyncAssociation::Connected(ids) => {
264 storage.put_meta(schema::GLOBAL_SYNCID_META_KEY, &ids.global.to_string())?;
265 storage.put_meta(schema::COLLECTION_SYNCID_META_KEY, &ids.coll.to_string())?;
266 }
267 };
268 Ok(())
269 }
270
271 fn wipe(&self) -> Result<()> {
272 self.reset(&EngineSyncAssociation::Disconnected)?;
273 self.store.storage.lock().unwrap().wipe_local_tabs();
276 Ok(())
277 }
278
279 fn get_sync_assoc(&self) -> Result<EngineSyncAssociation> {
280 let mut storage = self.store.storage.lock().unwrap();
281 let global = storage.get_meta::<String>(schema::GLOBAL_SYNCID_META_KEY)?;
282 let coll = storage.get_meta::<String>(schema::COLLECTION_SYNCID_META_KEY)?;
283 Ok(if let (Some(global), Some(coll)) = (global, coll) {
284 EngineSyncAssociation::Connected(CollSyncIds {
285 global: Guid::from_string(global),
286 coll: Guid::from_string(coll),
287 })
288 } else {
289 EngineSyncAssociation::Disconnected
290 })
291 }
292}
293
294impl crate::TabsStore {
295 pub fn register_with_sync_manager(self: Arc<Self>) {
301 let mut state = STORE_FOR_MANAGER.lock().unwrap();
302 *state = Arc::downgrade(&self);
303 }
304}
305
306#[cfg(test)]
307pub mod test {
308 use super::*;
309 use crate::DeviceType;
310 use serde_json::json;
311 use sync15::bso::IncomingBso;
312
313 #[test]
314 fn test_incoming_tabs() {
315 error_support::init_for_tests();
316
317 let engine = TabsEngine::new(Arc::new(TabsStore::new_with_mem_path("test-incoming")));
318
319 let client_data = ClientData {
320 local_client_id: "my-device".to_string(),
321 recent_clients: HashMap::from([
322 (
323 "my-device".to_string(),
324 RemoteClient {
325 fxa_device_id: None,
326 device_name: "my device".to_string(),
327 device_type: sync15::DeviceType::Unknown,
328 },
329 ),
330 (
331 "device-no-tabs".to_string(),
332 RemoteClient {
333 fxa_device_id: None,
334 device_name: "device with no tabs".to_string(),
335 device_type: DeviceType::Unknown,
336 },
337 ),
338 (
339 "device-with-a-tab".to_string(),
340 RemoteClient {
341 fxa_device_id: None,
342 device_name: "device with an updated tab".to_string(),
343 device_type: DeviceType::Unknown,
344 },
345 ),
346 ]),
347 };
348 engine
349 .set_clients(&|| client_data.clone())
350 .expect("should work");
351
352 let records = vec![
353 json!({
354 "id": "device-no-tabs",
355 "clientName": "device with no tabs",
356 "tabs": [],
357 }),
358 json!({
359 "id": "device-with-a-tab",
360 "clientName": "device with a tab",
361 "tabs": [{
362 "title": "the title",
363 "urlHistory": [
364 "https://mozilla.org/"
365 ],
366 "icon": "https://mozilla.org/icon",
367 "lastUsed": 1643764207
368 }]
369 }),
370 json!({
371 "id": "device-with-a-tab",
372 "clientName": "device with an updated tab",
373 "tabs": [{
374 "title": "the new title",
375 "urlHistory": [
376 "https://mozilla.org/"
377 ],
378 "icon": "https://mozilla.org/icon",
379 "lastUsed": 1643764208
380 }]
381 }),
382 json!({
384 "id": "device-with-invalid-tab",
385 "clientName": "device with a tab",
386 "tabs": [{
387 "foo": "bar",
388 }]
389 }),
390 json!({
392 "id": "invalid-tab",
393 "foo": "bar"
394 }),
395 ];
396
397 let mut telem = telemetry::Engine::new("tabs");
398 let incoming = records
399 .into_iter()
400 .map(IncomingBso::from_test_content)
401 .collect();
402 engine
403 .stage_incoming(incoming, &mut telem)
404 .expect("Should apply incoming and stage outgoing records");
405 let outgoing = engine
406 .apply(ServerTimestamp(0), &mut telem)
407 .expect("should apply");
408
409 assert!(outgoing.is_empty());
410
411 let mut storage = engine.store.storage.lock().unwrap();
413 let mut crts = storage.get_remote_tabs().expect("should work");
414 crts.sort_by(|a, b| a.client_name.partial_cmp(&b.client_name).unwrap());
415 assert_eq!(crts.len(), 2, "we currently include devices with no tabs");
416 let crt = &crts[0];
417 assert_eq!(crt.client_name, "device with an updated tab");
418 assert_eq!(crt.device_type, DeviceType::Unknown);
419 assert_eq!(crt.remote_tabs.len(), 1);
420 assert_eq!(crt.remote_tabs[0].title, "the new title");
421
422 let crt = &crts[1];
423 assert_eq!(crt.client_name, "device with no tabs");
424 assert_eq!(crt.device_type, DeviceType::Unknown);
425 assert_eq!(crt.remote_tabs.len(), 0);
426 }
427
428 #[test]
429 fn test_no_incoming_doesnt_write() {
430 error_support::init_for_tests();
431
432 let engine = TabsEngine::new(Arc::new(TabsStore::new_with_mem_path(
433 "test_no_incoming_doesnt_write",
434 )));
435
436 let client_data = ClientData {
437 local_client_id: "my-device".to_string(),
438 recent_clients: HashMap::from([(
439 "device-with-a-tab".to_string(),
440 RemoteClient {
441 fxa_device_id: None,
442 device_name: "device-with-a-tab".to_string(),
443 device_type: DeviceType::Unknown,
444 },
445 )]),
446 };
447 engine
448 .set_clients(&|| client_data.clone())
449 .expect("should work");
450
451 let records = vec![json!({
452 "id": "device-with-a-tab",
453 "clientName": "device with a tab",
454 "tabs": [{
455 "title": "the title",
456 "urlHistory": [
457 "https://mozilla.org/"
458 ],
459 "icon": "https://mozilla.org/icon",
460 "lastUsed": 1643764207
461 }]
462 })];
463
464 let mut telem = telemetry::Engine::new("tabs");
465 let incoming = records
466 .into_iter()
467 .map(IncomingBso::from_test_content)
468 .collect();
469 engine
470 .stage_incoming(incoming, &mut telem)
471 .expect("Should apply incoming and stage outgoing records");
472 engine
473 .apply(ServerTimestamp(0), &mut telem)
474 .expect("should apply");
475
476 {
478 let mut storage = engine.store.storage.lock().unwrap();
479 assert_eq!(storage.get_remote_tabs().expect("should work").len(), 1);
480 }
481
482 engine
485 .stage_incoming(vec![], &mut telemetry::Engine::new("tabs"))
486 .expect("Should succeed applying zero records");
487
488 {
489 let mut storage = engine.store.storage.lock().unwrap();
490 assert_eq!(storage.get_remote_tabs().expect("should work").len(), 1);
491 }
492 }
493
494 #[test]
495 fn test_sync_manager_registration() {
496 let store = Arc::new(TabsStore::new_with_mem_path("test-registration"));
497 assert_eq!(Arc::strong_count(&store), 1);
498 assert_eq!(Arc::weak_count(&store), 0);
499 Arc::clone(&store).register_with_sync_manager();
500 assert_eq!(Arc::strong_count(&store), 1);
501 assert_eq!(Arc::weak_count(&store), 1);
502 let registered = STORE_FOR_MANAGER
503 .lock()
504 .unwrap()
505 .upgrade()
506 .expect("should upgrade");
507 assert!(Arc::ptr_eq(&store, ®istered));
508 drop(registered);
509 assert_eq!(Arc::strong_count(&store), 1);
511 assert_eq!(Arc::weak_count(&store), 1);
512 drop(store);
514 assert!(STORE_FOR_MANAGER.lock().unwrap().upgrade().is_none());
515 }
516
517 #[test]
518 fn test_apply_timestamp() {
519 error_support::init_for_tests();
520
521 let engine = TabsEngine::new(Arc::new(TabsStore::new_with_mem_path(
522 "test-apply-timestamp",
523 )));
524
525 let records = vec![json!({
526 "id": "device-no-tabs",
527 "clientName": "device with no tabs",
528 "tabs": [],
529 })];
530
531 let mut telem = telemetry::Engine::new("tabs");
532 engine
533 .set_last_sync(ServerTimestamp::from_millis(123))
534 .unwrap();
535 let incoming = records
536 .into_iter()
537 .map(IncomingBso::from_test_content)
538 .collect();
539 engine
540 .stage_incoming(incoming, &mut telem)
541 .expect("Should apply incoming and stage outgoing records");
542 engine
543 .apply(ServerTimestamp(0), &mut telem)
544 .expect("should apply");
545
546 assert_eq!(
547 engine
548 .last_sync()
549 .expect("should work")
550 .expect("should have a value"),
551 ServerTimestamp::from_millis(123),
552 "didn't set a zero timestamp"
553 )
554 }
555}