Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "chrome/browser/sync/engine/conflict_resolver.h" | 5 #include "chrome/browser/sync/engine/conflict_resolver.h" |
| 6 | 6 |
| 7 #include <algorithm> | 7 #include <algorithm> |
| 8 #include <list> | |
| 8 #include <map> | 9 #include <map> |
| 9 #include <set> | 10 #include <set> |
| 10 | 11 |
| 11 #include "base/location.h" | 12 #include "base/location.h" |
| 12 #include "base/metrics/histogram.h" | 13 #include "base/metrics/histogram.h" |
| 13 #include "chrome/browser/sync/engine/syncer.h" | 14 #include "chrome/browser/sync/engine/syncer.h" |
| 14 #include "chrome/browser/sync/engine/syncer_util.h" | 15 #include "chrome/browser/sync/engine/syncer_util.h" |
| 15 #include "chrome/browser/sync/protocol/nigori_specifics.pb.h" | 16 #include "chrome/browser/sync/protocol/nigori_specifics.pb.h" |
| 16 #include "chrome/browser/sync/protocol/service_constants.h" | 17 #include "chrome/browser/sync/protocol/service_constants.h" |
| 17 #include "chrome/browser/sync/sessions/status_controller.h" | 18 #include "chrome/browser/sync/sessions/status_controller.h" |
| 18 #include "chrome/browser/sync/syncable/directory_manager.h" | 19 #include "chrome/browser/sync/syncable/directory_manager.h" |
| 19 #include "chrome/browser/sync/syncable/syncable.h" | 20 #include "chrome/browser/sync/syncable/syncable.h" |
| 20 #include "chrome/browser/sync/util/cryptographer.h" | 21 #include "chrome/browser/sync/util/cryptographer.h" |
| 21 | 22 |
| 23 using std::list; | |
| 22 using std::map; | 24 using std::map; |
| 23 using std::set; | 25 using std::set; |
| 24 using syncable::BaseTransaction; | 26 using syncable::BaseTransaction; |
| 25 using syncable::Directory; | 27 using syncable::Directory; |
| 26 using syncable::Entry; | 28 using syncable::Entry; |
| 27 using syncable::GetModelTypeFromSpecifics; | 29 using syncable::GetModelTypeFromSpecifics; |
| 28 using syncable::Id; | 30 using syncable::Id; |
| 29 using syncable::IsRealDataType; | 31 using syncable::IsRealDataType; |
| 30 using syncable::MutableEntry; | 32 using syncable::MutableEntry; |
| 31 using syncable::ScopedDirLookup; | 33 using syncable::ScopedDirLookup; |
| (...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 103 return NO_SYNC_PROGRESS; | 105 return NO_SYNC_PROGRESS; |
| 104 } | 106 } |
| 105 | 107 |
| 106 // This logic determines "client wins" vs. "server wins" strategy picking. | 108 // This logic determines "client wins" vs. "server wins" strategy picking. |
| 107 // By the time we get to this point, we rely on the following to be true: | 109 // By the time we get to this point, we rely on the following to be true: |
| 108 // a) We can decrypt both the local and server data (else we'd be in | 110 // a) We can decrypt both the local and server data (else we'd be in |
| 109 // conflict encryption and not attempting to resolve). | 111 // conflict encryption and not attempting to resolve). |
| 110 // b) All unsynced changes have been re-encrypted with the default key ( | 112 // b) All unsynced changes have been re-encrypted with the default key ( |
| 111 // occurs either in AttemptToUpdateEntry, SetPassphrase, or | 113 // occurs either in AttemptToUpdateEntry, SetPassphrase, or |
| 112 // RefreshEncryption). | 114 // RefreshEncryption). |
| 113 // c) Prev_server_specifics having a valid datatype means that we received | 115 // c) Base_server_specifics having a valid datatype means that we received |
| 114 // an undecryptable update that only changed specifics, and since then have | 116 // an undecryptable update that only changed specifics, and since then have |
| 115 // not received any further non-specifics-only or decryptable updates. | 117 // not received any further non-specifics-only or decryptable updates. |
| 116 // d) If the server_specifics match specifics, server_specifics are | 118 // d) If the server_specifics match specifics, server_specifics are |
| 117 // encrypted with the default key, and all other visible properties match, | 119 // encrypted with the default key, and all other visible properties match, |
| 118 // then we can safely ignore the local changes as redundant. | 120 // then we can safely ignore the local changes as redundant. |
| 119 // e) Otherwise if the base_server_specifics match the server_specifics, no | 121 // e) Otherwise if the base_server_specifics match the server_specifics, no |
| 120 // functional change must have been made server-side (else | 122 // functional change must have been made server-side (else |
| 121 // base_server_specifics would have been cleared), and we can therefore | 123 // base_server_specifics would have been cleared), and we can therefore |
| 122 // safely ignore the server changes as redundant. | 124 // safely ignore the server changes as redundant. |
| 123 // f) Otherwise, it's in general safer to ignore local changes, with the | 125 // f) Otherwise, it's in general safer to ignore local changes, with the |
| 124 // exception of deletion conflicts (choose to undelete) and conflicts | 126 // exception of deletion conflicts (choose to undelete) and conflicts |
| 125 // where the non_unique_name or parent don't match. | 127 // where the non_unique_name or parent don't match. |
| 126 // TODO(sync): We can't compare position here without performing the | |
| 127 // NEXT_ID/PREV_ID -> position_in_parent interpolation. Fix that, and take it | |
| 128 // into account. | |
| 129 if (!entry.Get(syncable::SERVER_IS_DEL)) { | 128 if (!entry.Get(syncable::SERVER_IS_DEL)) { |
| 130 // TODO(nick): The current logic is arbitrary; instead, it ought to be made | 129 // TODO(nick): The current logic is arbitrary; instead, it ought to be made |
| 131 // consistent with the ModelAssociator behavior for a datatype. It would | 130 // consistent with the ModelAssociator behavior for a datatype. It would |
| 132 // be nice if we could route this back to ModelAssociator code to pick one | 131 // be nice if we could route this back to ModelAssociator code to pick one |
| 133 // of three options: CLIENT, SERVER, or MERGE. Some datatypes (autofill) | 132 // of three options: CLIENT, SERVER, or MERGE. Some datatypes (autofill) |
| 134 // are easily mergeable. | 133 // are easily mergeable. |
| 135 // See http://crbug.com/77339. | 134 // See http://crbug.com/77339. |
| 136 bool name_matches = entry.Get(syncable::NON_UNIQUE_NAME) == | 135 bool name_matches = entry.Get(syncable::NON_UNIQUE_NAME) == |
| 137 entry.Get(syncable::SERVER_NON_UNIQUE_NAME); | 136 entry.Get(syncable::SERVER_NON_UNIQUE_NAME); |
| 138 bool parent_matches = entry.Get(syncable::PARENT_ID) == | 137 bool parent_matches = entry.Get(syncable::PARENT_ID) == |
| 139 entry.Get(syncable::SERVER_PARENT_ID); | 138 entry.Get(syncable::SERVER_PARENT_ID); |
| 140 bool entry_deleted = entry.Get(syncable::IS_DEL); | 139 bool entry_deleted = entry.Get(syncable::IS_DEL); |
| 140 // This positional check is conservative. It will likely be false if the | |
| 141 // predecessor is unsynced/unapplied. For this reason, we ensure conflicting | |
| 142 // predecessors are resolved before their successors. Being | |
| 143 // conservative results in being less likely to lose local positioning | |
| 144 // changes, with the cost of occasional unnecessary commits. | |
| 145 // TODO(zea): change this once we can directly compare server position to | |
| 146 // client position. | |
| 147 syncable::Id server_prev_id = entry.ComputePrevIdFromServerPosition( | |
|
rlarocque
2012/01/10 23:39:51
ComputePrevIdFromServerPosition() will not return
Nicolas Zea
2012/01/18 02:54:39
See my updated comments. Basically, this situation
rlarocque
2012/01/18 20:21:55
It sounds like you're expecting this to return the
Nicolas Zea
2012/01/18 20:40:42
No, I expect it to return the first fully synced i
| |
| 148 entry.Get(syncable::SERVER_PARENT_ID)); | |
|
rlarocque
2012/01/10 23:39:51
SERVER_PARENT_ID? Did you mean SERVER_PREV_ID?
Nicolas Zea
2012/01/18 02:54:39
No, you pass the parent to ComputePrevIdFromServer
| |
| 149 bool position_matches = parent_matches && | |
| 150 (server_prev_id == entry.Get(syncable::PREV_ID)); | |
|
ncarter (slow)
2012/01/04 21:37:59
The comment about 'conservative' could be read to
Nicolas Zea
2012/01/18 02:54:39
Upon further reflection, the code as stands actual
| |
| 151 DVLOG_IF(1, !position_matches) << "Position doesn't match, server prev id " | |
| 152 << " is " << server_prev_id << ", local prev id is " | |
| 153 << entry.Get(syncable::PREV_ID); | |
| 141 const sync_pb::EntitySpecifics& specifics = | 154 const sync_pb::EntitySpecifics& specifics = |
| 142 entry.Get(syncable::SPECIFICS); | 155 entry.Get(syncable::SPECIFICS); |
| 143 const sync_pb::EntitySpecifics& server_specifics = | 156 const sync_pb::EntitySpecifics& server_specifics = |
| 144 entry.Get(syncable::SERVER_SPECIFICS); | 157 entry.Get(syncable::SERVER_SPECIFICS); |
| 145 const sync_pb::EntitySpecifics& base_server_specifics = | 158 const sync_pb::EntitySpecifics& base_server_specifics = |
| 146 entry.Get(syncable::BASE_SERVER_SPECIFICS); | 159 entry.Get(syncable::BASE_SERVER_SPECIFICS); |
| 147 std::string decrypted_specifics, decrypted_server_specifics; | 160 std::string decrypted_specifics, decrypted_server_specifics; |
| 148 bool specifics_match = false; | 161 bool specifics_match = false; |
| 149 bool server_encrypted_with_default_key = false; | 162 bool server_encrypted_with_default_key = false; |
| 150 if (specifics.has_encrypted()) { | 163 if (specifics.has_encrypted()) { |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 211 // We deliberately leave the server's device information. This client will | 224 // We deliberately leave the server's device information. This client will |
| 212 // add it's own device information on restart. | 225 // add it's own device information on restart. |
| 213 entry.Put(syncable::SPECIFICS, specifics); | 226 entry.Put(syncable::SPECIFICS, specifics); |
| 214 DVLOG(1) << "Resovling simple conflict, merging nigori nodes: " << entry; | 227 DVLOG(1) << "Resovling simple conflict, merging nigori nodes: " << entry; |
| 215 status->increment_num_server_overwrites(); | 228 status->increment_num_server_overwrites(); |
| 216 OverwriteServerChanges(trans, &entry); | 229 OverwriteServerChanges(trans, &entry); |
| 217 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", | 230 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", |
| 218 NIGORI_MERGE, | 231 NIGORI_MERGE, |
| 219 CONFLICT_RESOLUTION_SIZE); | 232 CONFLICT_RESOLUTION_SIZE); |
| 220 } else if (!entry_deleted && name_matches && parent_matches && | 233 } else if (!entry_deleted && name_matches && parent_matches && |
| 221 specifics_match) { | 234 position_matches && specifics_match) { |
| 222 DVLOG(1) << "Resolving simple conflict, everything matches, ignoring " | 235 DVLOG(1) << "Resolving simple conflict, everything matches, ignoring " |
| 223 << "changes for: " << entry; | 236 << "changes for: " << entry; |
| 224 // This unsets both IS_UNSYNCED and IS_UNAPPLIED_UPDATE, and sets the | 237 // This unsets both IS_UNSYNCED and IS_UNAPPLIED_UPDATE, and sets the |
| 225 // BASE_VERSION to match the SERVER_VERSION. If we didn't also unset | 238 // BASE_VERSION to match the SERVER_VERSION. If we didn't also unset |
| 226 // IS_UNAPPLIED_UPDATE, then we would lose unsynced positional data | 239 // IS_UNAPPLIED_UPDATE, then we would lose unsynced positional data from |
| 227 // when the server update gets applied and the item is re-inserted into | 240 // adjacent entries when the server update gets applied and the item is |
| 228 // the PREV_ID/NEXT_ID linked list (see above TODO about comparing | 241 // re-inserted into the PREV_ID/NEXT_ID linked list. |
| 229 // positional info). | |
| 230 OverwriteServerChanges(trans, &entry); | 242 OverwriteServerChanges(trans, &entry); |
| 231 IgnoreLocalChanges(&entry); | 243 IgnoreLocalChanges(&entry); |
| 232 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", | 244 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", |
| 233 CHANGES_MATCH, | 245 CHANGES_MATCH, |
| 234 CONFLICT_RESOLUTION_SIZE); | 246 CONFLICT_RESOLUTION_SIZE); |
| 235 } else if (base_server_specifics_match) { | 247 } else if (base_server_specifics_match) { |
| 236 DVLOG(1) << "Resolving simple conflict, ignoring server encryption " | 248 DVLOG(1) << "Resolving simple conflict, ignoring server encryption " |
| 237 << " changes for: " << entry; | 249 << " changes for: " << entry; |
| 238 status->increment_num_server_overwrites(); | 250 status->increment_num_server_overwrites(); |
| 239 OverwriteServerChanges(trans, &entry); | 251 OverwriteServerChanges(trans, &entry); |
| 240 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", | 252 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", |
| 241 IGNORE_ENCRYPTION, | 253 IGNORE_ENCRYPTION, |
| 242 CONFLICT_RESOLUTION_SIZE); | 254 CONFLICT_RESOLUTION_SIZE); |
| 243 // Now that we've resolved the conflict, clear the prev server | |
| 244 // specifics. | |
| 245 entry.Put(syncable::BASE_SERVER_SPECIFICS, sync_pb::EntitySpecifics()); | |
| 246 } else if (entry_deleted || !name_matches || !parent_matches) { | 255 } else if (entry_deleted || !name_matches || !parent_matches) { |
|
rlarocque
2012/01/10 23:39:51
Assertion:
If this condition is true, then we woul
Nicolas Zea
2012/01/18 02:54:39
I'm not sure I understand this assertion. The if s
| |
| 247 OverwriteServerChanges(trans, &entry); | 256 OverwriteServerChanges(trans, &entry); |
| 248 status->increment_num_server_overwrites(); | 257 status->increment_num_server_overwrites(); |
| 249 DVLOG(1) << "Resolving simple conflict, overwriting server changes " | 258 DVLOG(1) << "Resolving simple conflict, overwriting server changes " |
| 250 << "for: " << entry; | 259 << "for: " << entry; |
| 251 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", | 260 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", |
| 252 OVERWRITE_SERVER, | 261 OVERWRITE_SERVER, |
| 253 CONFLICT_RESOLUTION_SIZE); | 262 CONFLICT_RESOLUTION_SIZE); |
| 254 } else { | 263 } else { |
|
rlarocque
2012/01/10 23:39:51
I believe this is where your change on line 234 st
Nicolas Zea
2012/01/18 02:54:39
See above comment. When the position doesn't match
rlarocque
2012/01/18 20:21:55
As I understand it, most conflicts end up being re
Nicolas Zea
2012/01/18 20:40:42
Okay, I think we're in agreement here then. Yes, t
| |
| 255 DVLOG(1) << "Resolving simple conflict, ignoring local changes for: " | 264 DVLOG(1) << "Resolving simple conflict, ignoring local changes for: " |
| 256 << entry; | 265 << entry; |
| 257 IgnoreLocalChanges(&entry); | 266 IgnoreLocalChanges(&entry); |
| 258 status->increment_num_local_overwrites(); | 267 status->increment_num_local_overwrites(); |
| 259 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", | 268 UMA_HISTOGRAM_ENUMERATION("Sync.ResolveSimpleConflict", |
| 260 OVERWRITE_LOCAL, | 269 OVERWRITE_LOCAL, |
| 261 CONFLICT_RESOLUTION_SIZE); | 270 CONFLICT_RESOLUTION_SIZE); |
| 262 } | 271 } |
| 272 // Now that we've resolved the conflict, clear the prev server | |
| 273 // specifics. | |
| 274 entry.Put(syncable::BASE_SERVER_SPECIFICS, sync_pb::EntitySpecifics()); | |
| 263 return SYNC_PROGRESS; | 275 return SYNC_PROGRESS; |
| 264 } else { // SERVER_IS_DEL is true | 276 } else { // SERVER_IS_DEL is true |
| 265 // If a server deleted folder has local contents we should be in a set. | 277 // If a server deleted folder has local contents we should be in a set. |
| 266 if (entry.Get(syncable::IS_DIR)) { | 278 if (entry.Get(syncable::IS_DIR)) { |
| 267 Directory::ChildHandles children; | 279 Directory::ChildHandles children; |
| 268 trans->directory()->GetChildHandlesById(trans, | 280 trans->directory()->GetChildHandlesById(trans, |
| 269 entry.Get(syncable::ID), | 281 entry.Get(syncable::ID), |
| 270 &children); | 282 &children); |
| 271 if (0 != children.size()) { | 283 if (0 != children.size()) { |
| 272 DVLOG(1) << "Entry is a server deleted directory with local contents, " | 284 DVLOG(1) << "Entry is a server deleted directory with local contents, " |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 311 } | 323 } |
| 312 | 324 |
| 313 bool ConflictResolver::ResolveSimpleConflicts( | 325 bool ConflictResolver::ResolveSimpleConflicts( |
| 314 syncable::WriteTransaction* trans, | 326 syncable::WriteTransaction* trans, |
| 315 const Cryptographer* cryptographer, | 327 const Cryptographer* cryptographer, |
| 316 const ConflictProgress& progress, | 328 const ConflictProgress& progress, |
| 317 sessions::StatusController* status) { | 329 sessions::StatusController* status) { |
| 318 bool forward_progress = false; | 330 bool forward_progress = false; |
| 319 // First iterate over simple conflict items (those that belong to no set). | 331 // First iterate over simple conflict items (those that belong to no set). |
| 320 set<Id>::const_iterator conflicting_item_it; | 332 set<Id>::const_iterator conflicting_item_it; |
| 333 set<Id> processed_items; | |
| 321 for (conflicting_item_it = progress.ConflictingItemsBegin(); | 334 for (conflicting_item_it = progress.ConflictingItemsBegin(); |
| 322 conflicting_item_it != progress.ConflictingItemsEnd(); | 335 conflicting_item_it != progress.ConflictingItemsEnd(); |
| 323 ++conflicting_item_it) { | 336 ++conflicting_item_it) { |
| 324 Id id = *conflicting_item_it; | 337 Id id = *conflicting_item_it; |
| 338 if (processed_items.count(id) > 0) | |
| 339 continue; | |
| 325 map<Id, ConflictSet*>::const_iterator item_set_it = | 340 map<Id, ConflictSet*>::const_iterator item_set_it = |
| 326 progress.IdToConflictSetFind(id); | 341 progress.IdToConflictSetFind(id); |
| 327 if (item_set_it == progress.IdToConflictSetEnd() || | 342 if (item_set_it == progress.IdToConflictSetEnd() || |
| 328 0 == item_set_it->second) { | 343 0 == item_set_it->second) { |
| 329 // We have a simple conflict. | 344 // We have a simple conflict. In order check if positions have changed, |
|
rlarocque
2012/01/18 20:21:55
I don't understand why this is necessary.
I think
Nicolas Zea
2012/01/18 20:40:42
You're right, we don't modify anything except IS_
| |
| 330 switch (ProcessSimpleConflict(trans, id, cryptographer, status)) { | 345 // we need to process conflicting predecessors before successors. Traverse |
| 331 case NO_SYNC_PROGRESS: | 346 // backwards through all continuous conflicting predecessors, building a |
| 347 // stack of items to resolve in predecessor->successor order, then process | |
| 348 // each item individually. | |
| 349 list<Id> predecessors; | |
| 350 Id prev_id = id; | |
| 351 do { | |
| 352 predecessors.push_back(prev_id); | |
| 353 Entry entry(trans, syncable::GET_BY_ID, prev_id); | |
| 354 // Any entry in conflict must be valid. | |
| 355 CHECK(entry.good()); | |
| 356 Id new_prev_id = entry.Get(syncable::PREV_ID); | |
| 357 if (new_prev_id == prev_id) | |
| 332 break; | 358 break; |
| 333 case SYNC_PROGRESS: | 359 prev_id = new_prev_id; |
| 334 forward_progress = true; | 360 } while (processed_items.count(prev_id) == 0 && |
| 335 break; | 361 progress.HasSimpleConflictItem(prev_id)); // Excludes root. |
| 362 while (!predecessors.empty()) { | |
| 363 id = predecessors.back(); | |
| 364 predecessors.pop_back(); | |
| 365 switch (ProcessSimpleConflict(trans, id, cryptographer, status)) { | |
| 366 case NO_SYNC_PROGRESS: | |
| 367 break; | |
| 368 case SYNC_PROGRESS: | |
| 369 forward_progress = true; | |
| 370 break; | |
| 371 } | |
| 372 processed_items.insert(id); | |
| 336 } | 373 } |
| 337 } | 374 } |
| 338 } | 375 } |
| 339 return forward_progress; | 376 return forward_progress; |
| 340 } | 377 } |
| 341 | 378 |
| 342 bool ConflictResolver::ResolveConflicts(syncable::WriteTransaction* trans, | 379 bool ConflictResolver::ResolveConflicts(syncable::WriteTransaction* trans, |
| 343 const Cryptographer* cryptographer, | 380 const Cryptographer* cryptographer, |
| 344 const ConflictProgress& progress, | 381 const ConflictProgress& progress, |
| 345 sessions::StatusController* status) { | 382 sessions::StatusController* status) { |
| 346 // TODO(rlarocque): A good amount of code related to the resolution of | 383 // TODO(rlarocque): A good amount of code related to the resolution of |
| 347 // conflict sets has been deleted here. This was done because the code had | 384 // conflict sets has been deleted here. This was done because the code had |
| 348 // not been used in years. An unrelated bug fix accidentally re-enabled the | 385 // not been used in years. An unrelated bug fix accidentally re-enabled the |
| 349 // code, forcing us to make a decision about what we should do with the code. | 386 // code, forcing us to make a decision about what we should do with the code. |
| 350 // We decided to do the safe thing and delete it for now. This restores the | 387 // We decided to do the safe thing and delete it for now. This restores the |
| 351 // behaviour we've relied on for quite some time. We should think about what | 388 // behaviour we've relied on for quite some time. We should think about what |
| 352 // that code was trying to do and consider re-enabling parts of it. | 389 // that code was trying to do and consider re-enabling parts of it. |
| 353 | 390 |
| 354 if (progress.ConflictSetsSize() > 0) { | 391 if (progress.ConflictSetsSize() > 0) { |
| 355 DVLOG(1) << "Detected " << progress.IdToConflictSetSize() | 392 DVLOG(1) << "Detected " << progress.IdToConflictSetSize() |
| 356 << " non-simple conflicting entries in " << progress.ConflictSetsSize() | 393 << " non-simple conflicting entries in " << progress.ConflictSetsSize() |
| 357 << " unprocessed conflict sets."; | 394 << " unprocessed conflict sets."; |
| 358 } | 395 } |
| 359 | 396 |
| 360 return ResolveSimpleConflicts(trans, cryptographer, progress, status); | 397 return ResolveSimpleConflicts(trans, cryptographer, progress, status); |
| 361 } | 398 } |
| 362 | 399 |
| 363 } // namespace browser_sync | 400 } // namespace browser_sync |
| OLD | NEW |