OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "chrome/browser/sync/engine/syncer_proto_util.h" | |
6 | |
7 #include "base/format_macros.h" | |
8 #include "base/stringprintf.h" | |
9 #include "chrome/browser/sync/engine/net/server_connection_manager.h" | |
10 #include "chrome/browser/sync/engine/syncer.h" | |
11 #include "chrome/browser/sync/engine/syncer_types.h" | |
12 #include "chrome/browser/sync/protocol/service_constants.h" | |
13 #include "chrome/browser/sync/protocol/sync_protocol_error.h" | |
14 #include "chrome/browser/sync/sessions/sync_session.h" | |
15 #include "chrome/browser/sync/syncable/model_type.h" | |
16 #include "chrome/browser/sync/syncable/syncable-inl.h" | |
17 #include "chrome/browser/sync/syncable/syncable.h" | |
18 #include "chrome/browser/sync/util/time.h" | |
19 #include "sync/protocol/sync.pb.h" | |
20 #include "sync/protocol/sync_enums.pb.h" | |
21 | |
22 using browser_sync::SyncProtocolErrorType; | |
23 using std::string; | |
24 using std::stringstream; | |
25 using syncable::BASE_VERSION; | |
26 using syncable::CTIME; | |
27 using syncable::ID; | |
28 using syncable::IS_DEL; | |
29 using syncable::IS_DIR; | |
30 using syncable::IS_UNSYNCED; | |
31 using syncable::MTIME; | |
32 using syncable::PARENT_ID; | |
33 | |
34 namespace browser_sync { | |
35 using sessions::SyncSession; | |
36 | |
37 namespace { | |
38 | |
39 // Time to backoff syncing after receiving a throttled response. | |
40 const int kSyncDelayAfterThrottled = 2 * 60 * 60; // 2 hours | |
41 | |
42 void LogResponseProfilingData(const ClientToServerResponse& response) { | |
43 if (response.has_profiling_data()) { | |
44 stringstream response_trace; | |
45 response_trace << "Server response trace:"; | |
46 | |
47 if (response.profiling_data().has_user_lookup_time()) { | |
48 response_trace << " user lookup: " | |
49 << response.profiling_data().user_lookup_time() << "ms"; | |
50 } | |
51 | |
52 if (response.profiling_data().has_meta_data_write_time()) { | |
53 response_trace << " meta write: " | |
54 << response.profiling_data().meta_data_write_time() | |
55 << "ms"; | |
56 } | |
57 | |
58 if (response.profiling_data().has_meta_data_read_time()) { | |
59 response_trace << " meta read: " | |
60 << response.profiling_data().meta_data_read_time() << "ms"; | |
61 } | |
62 | |
63 if (response.profiling_data().has_file_data_write_time()) { | |
64 response_trace << " file write: " | |
65 << response.profiling_data().file_data_write_time() | |
66 << "ms"; | |
67 } | |
68 | |
69 if (response.profiling_data().has_file_data_read_time()) { | |
70 response_trace << " file read: " | |
71 << response.profiling_data().file_data_read_time() << "ms"; | |
72 } | |
73 | |
74 if (response.profiling_data().has_total_request_time()) { | |
75 response_trace << " total time: " | |
76 << response.profiling_data().total_request_time() << "ms"; | |
77 } | |
78 DVLOG(1) << response_trace.str(); | |
79 } | |
80 } | |
81 | |
82 SyncerError ServerConnectionErrorAsSyncerError( | |
83 const HttpResponse::ServerConnectionCode server_status) { | |
84 switch (server_status) { | |
85 case HttpResponse::CONNECTION_UNAVAILABLE: | |
86 return NETWORK_CONNECTION_UNAVAILABLE; | |
87 case HttpResponse::IO_ERROR: | |
88 return NETWORK_IO_ERROR; | |
89 case HttpResponse::SYNC_SERVER_ERROR: | |
90 // FIXME what does this mean? | |
91 return SYNC_SERVER_ERROR; | |
92 case HttpResponse::SYNC_AUTH_ERROR: | |
93 return SYNC_AUTH_ERROR; | |
94 case HttpResponse::RETRY: | |
95 return SERVER_RETURN_TRANSIENT_ERROR; | |
96 case HttpResponse::SERVER_CONNECTION_OK: | |
97 case HttpResponse::NONE: | |
98 default: | |
99 NOTREACHED(); | |
100 return UNSET; | |
101 } | |
102 } | |
103 | |
104 } // namespace | |
105 | |
106 // static | |
107 void SyncerProtoUtil::HandleMigrationDoneResponse( | |
108 const sync_pb::ClientToServerResponse* response, | |
109 sessions::SyncSession* session) { | |
110 LOG_IF(ERROR, 0 >= response->migrated_data_type_id_size()) | |
111 << "MIGRATION_DONE but no types specified."; | |
112 syncable::ModelTypeSet to_migrate; | |
113 for (int i = 0; i < response->migrated_data_type_id_size(); i++) { | |
114 to_migrate.Put(syncable::GetModelTypeFromSpecificsFieldNumber( | |
115 response->migrated_data_type_id(i))); | |
116 } | |
117 // TODO(akalin): This should be a set union. | |
118 session->mutable_status_controller()-> | |
119 set_types_needing_local_migration(to_migrate); | |
120 } | |
121 | |
122 // static | |
123 bool SyncerProtoUtil::VerifyResponseBirthday(syncable::Directory* dir, | |
124 const ClientToServerResponse* response) { | |
125 | |
126 std::string local_birthday = dir->store_birthday(); | |
127 | |
128 if (local_birthday.empty()) { | |
129 if (!response->has_store_birthday()) { | |
130 LOG(WARNING) << "Expected a birthday on first sync."; | |
131 return false; | |
132 } | |
133 | |
134 DVLOG(1) << "New store birthday: " << response->store_birthday(); | |
135 dir->set_store_birthday(response->store_birthday()); | |
136 return true; | |
137 } | |
138 | |
139 // Error situation, but we're not stuck. | |
140 if (!response->has_store_birthday()) { | |
141 LOG(WARNING) << "No birthday in server response?"; | |
142 return true; | |
143 } | |
144 | |
145 if (response->store_birthday() != local_birthday) { | |
146 LOG(WARNING) << "Birthday changed, showing syncer stuck"; | |
147 return false; | |
148 } | |
149 | |
150 return true; | |
151 } | |
152 | |
153 // static | |
154 void SyncerProtoUtil::AddRequestBirthday(syncable::Directory* dir, | |
155 ClientToServerMessage* msg) { | |
156 if (!dir->store_birthday().empty()) | |
157 msg->set_store_birthday(dir->store_birthday()); | |
158 } | |
159 | |
160 // static | |
161 bool SyncerProtoUtil::PostAndProcessHeaders(ServerConnectionManager* scm, | |
162 sessions::SyncSession* session, | |
163 const ClientToServerMessage& msg, | |
164 ClientToServerResponse* response) { | |
165 ServerConnectionManager::PostBufferParams params; | |
166 msg.SerializeToString(¶ms.buffer_in); | |
167 | |
168 ScopedServerStatusWatcher server_status_watcher(scm, ¶ms.response); | |
169 // Fills in params.buffer_out and params.response. | |
170 if (!scm->PostBufferWithCachedAuth(¶ms, &server_status_watcher)) { | |
171 LOG(WARNING) << "Error posting from syncer:" << params.response; | |
172 return false; | |
173 } | |
174 | |
175 std::string new_token = params.response.update_client_auth_header; | |
176 if (!new_token.empty()) { | |
177 SyncEngineEvent event(SyncEngineEvent::UPDATED_TOKEN); | |
178 event.updated_token = new_token; | |
179 session->context()->NotifyListeners(event); | |
180 } | |
181 | |
182 if (response->ParseFromString(params.buffer_out)) { | |
183 // TODO(tim): This is an egregious layering violation (bug 35060). | |
184 switch (response->error_code()) { | |
185 case sync_pb::SyncEnums::ACCESS_DENIED: | |
186 case sync_pb::SyncEnums::AUTH_INVALID: | |
187 case sync_pb::SyncEnums::USER_NOT_ACTIVATED: | |
188 // Fires on ScopedServerStatusWatcher | |
189 params.response.server_status = HttpResponse::SYNC_AUTH_ERROR; | |
190 return false; | |
191 default: | |
192 return true; | |
193 } | |
194 } | |
195 | |
196 return false; | |
197 } | |
198 | |
199 base::TimeDelta SyncerProtoUtil::GetThrottleDelay( | |
200 const sync_pb::ClientToServerResponse& response) { | |
201 base::TimeDelta throttle_delay = | |
202 base::TimeDelta::FromSeconds(kSyncDelayAfterThrottled); | |
203 if (response.has_client_command()) { | |
204 const sync_pb::ClientCommand& command = response.client_command(); | |
205 if (command.has_throttle_delay_seconds()) { | |
206 throttle_delay = | |
207 base::TimeDelta::FromSeconds(command.throttle_delay_seconds()); | |
208 } | |
209 } | |
210 return throttle_delay; | |
211 } | |
212 | |
213 void SyncerProtoUtil::HandleThrottleError( | |
214 const SyncProtocolError& error, | |
215 const base::TimeTicks& throttled_until, | |
216 sessions::SyncSessionContext* context, | |
217 sessions::SyncSession::Delegate* delegate) { | |
218 DCHECK_EQ(error.error_type, browser_sync::THROTTLED); | |
219 if (error.error_data_types.Empty()) { | |
220 // No datatypes indicates the client should be completely throttled. | |
221 delegate->OnSilencedUntil(throttled_until); | |
222 } else { | |
223 context->SetUnthrottleTime(error.error_data_types, throttled_until); | |
224 } | |
225 } | |
226 | |
227 namespace { | |
228 | |
229 // Helper function for an assertion in PostClientToServerMessage. | |
230 bool IsVeryFirstGetUpdates(const ClientToServerMessage& message) { | |
231 if (!message.has_get_updates()) | |
232 return false; | |
233 DCHECK_LT(0, message.get_updates().from_progress_marker_size()); | |
234 for (int i = 0; i < message.get_updates().from_progress_marker_size(); ++i) { | |
235 if (!message.get_updates().from_progress_marker(i).token().empty()) | |
236 return false; | |
237 } | |
238 return true; | |
239 } | |
240 | |
241 SyncProtocolErrorType ConvertSyncProtocolErrorTypePBToLocalType( | |
242 const sync_pb::SyncEnums::ErrorType& error_type) { | |
243 switch (error_type) { | |
244 case sync_pb::SyncEnums::SUCCESS: | |
245 return browser_sync::SYNC_SUCCESS; | |
246 case sync_pb::SyncEnums::NOT_MY_BIRTHDAY: | |
247 return browser_sync::NOT_MY_BIRTHDAY; | |
248 case sync_pb::SyncEnums::THROTTLED: | |
249 return browser_sync::THROTTLED; | |
250 case sync_pb::SyncEnums::CLEAR_PENDING: | |
251 return browser_sync::CLEAR_PENDING; | |
252 case sync_pb::SyncEnums::TRANSIENT_ERROR: | |
253 return browser_sync::TRANSIENT_ERROR; | |
254 case sync_pb::SyncEnums::MIGRATION_DONE: | |
255 return browser_sync::MIGRATION_DONE; | |
256 case sync_pb::SyncEnums::UNKNOWN: | |
257 return browser_sync::UNKNOWN_ERROR; | |
258 case sync_pb::SyncEnums::USER_NOT_ACTIVATED: | |
259 case sync_pb::SyncEnums::AUTH_INVALID: | |
260 case sync_pb::SyncEnums::ACCESS_DENIED: | |
261 return browser_sync::INVALID_CREDENTIAL; | |
262 default: | |
263 NOTREACHED(); | |
264 return browser_sync::UNKNOWN_ERROR; | |
265 } | |
266 } | |
267 | |
268 browser_sync::ClientAction ConvertClientActionPBToLocalClientAction( | |
269 const sync_pb::ClientToServerResponse::Error::Action& action) { | |
270 switch (action) { | |
271 case ClientToServerResponse::Error::UPGRADE_CLIENT: | |
272 return browser_sync::UPGRADE_CLIENT; | |
273 case ClientToServerResponse::Error::CLEAR_USER_DATA_AND_RESYNC: | |
274 return browser_sync::CLEAR_USER_DATA_AND_RESYNC; | |
275 case ClientToServerResponse::Error::ENABLE_SYNC_ON_ACCOUNT: | |
276 return browser_sync::ENABLE_SYNC_ON_ACCOUNT; | |
277 case ClientToServerResponse::Error::STOP_AND_RESTART_SYNC: | |
278 return browser_sync::STOP_AND_RESTART_SYNC; | |
279 case ClientToServerResponse::Error::DISABLE_SYNC_ON_CLIENT: | |
280 return browser_sync::DISABLE_SYNC_ON_CLIENT; | |
281 case ClientToServerResponse::Error::UNKNOWN_ACTION: | |
282 return browser_sync::UNKNOWN_ACTION; | |
283 default: | |
284 NOTREACHED(); | |
285 return browser_sync::UNKNOWN_ACTION; | |
286 } | |
287 } | |
288 | |
289 browser_sync::SyncProtocolError ConvertErrorPBToLocalType( | |
290 const sync_pb::ClientToServerResponse::Error& error) { | |
291 browser_sync::SyncProtocolError sync_protocol_error; | |
292 sync_protocol_error.error_type = ConvertSyncProtocolErrorTypePBToLocalType( | |
293 error.error_type()); | |
294 sync_protocol_error.error_description = error.error_description(); | |
295 sync_protocol_error.url = error.url(); | |
296 sync_protocol_error.action = ConvertClientActionPBToLocalClientAction( | |
297 error.action()); | |
298 | |
299 if (error.error_data_type_ids_size() > 0) { | |
300 // THROTTLED is currently the only error code that uses |error_data_types|. | |
301 DCHECK_EQ(error.error_type(), sync_pb::SyncEnums::THROTTLED); | |
302 for (int i = 0; i < error.error_data_type_ids_size(); ++i) { | |
303 sync_protocol_error.error_data_types.Put( | |
304 syncable::GetModelTypeFromSpecificsFieldNumber( | |
305 error.error_data_type_ids(i))); | |
306 } | |
307 } | |
308 | |
309 return sync_protocol_error; | |
310 } | |
311 | |
312 // TODO(lipalani) : Rename these function names as per the CR for issue 7740067. | |
313 browser_sync::SyncProtocolError ConvertLegacyErrorCodeToNewError( | |
314 const sync_pb::SyncEnums::ErrorType& error_type) { | |
315 browser_sync::SyncProtocolError error; | |
316 error.error_type = ConvertSyncProtocolErrorTypePBToLocalType(error_type); | |
317 if (error_type == sync_pb::SyncEnums::CLEAR_PENDING || | |
318 error_type == sync_pb::SyncEnums::NOT_MY_BIRTHDAY) { | |
319 error.action = browser_sync::DISABLE_SYNC_ON_CLIENT; | |
320 } // There is no other action we can compute for legacy server. | |
321 return error; | |
322 } | |
323 | |
324 } // namespace | |
325 | |
326 // static | |
327 SyncerError SyncerProtoUtil::PostClientToServerMessage( | |
328 const ClientToServerMessage& msg, | |
329 ClientToServerResponse* response, | |
330 SyncSession* session) { | |
331 | |
332 CHECK(response); | |
333 DCHECK(!msg.get_updates().has_from_timestamp()); // Deprecated. | |
334 DCHECK(!msg.get_updates().has_requested_types()); // Deprecated. | |
335 DCHECK(msg.has_store_birthday() || IsVeryFirstGetUpdates(msg)) | |
336 << "Must call AddRequestBirthday to set birthday."; | |
337 | |
338 syncable::Directory* dir = session->context()->directory(); | |
339 | |
340 if (!PostAndProcessHeaders(session->context()->connection_manager(), session, | |
341 msg, response)) { | |
342 // There was an error establishing communication with the server. | |
343 // We can not proceed beyond this point. | |
344 const browser_sync::HttpResponse::ServerConnectionCode server_status = | |
345 session->context()->connection_manager()->server_status(); | |
346 | |
347 DCHECK_NE(server_status, browser_sync::HttpResponse::NONE); | |
348 DCHECK_NE(server_status, browser_sync::HttpResponse::SERVER_CONNECTION_OK); | |
349 | |
350 return ServerConnectionErrorAsSyncerError(server_status); | |
351 } | |
352 | |
353 browser_sync::SyncProtocolError sync_protocol_error; | |
354 | |
355 // Birthday mismatch overrides any error that is sent by the server. | |
356 if (!VerifyResponseBirthday(dir, response)) { | |
357 sync_protocol_error.error_type = browser_sync::NOT_MY_BIRTHDAY; | |
358 sync_protocol_error.action = | |
359 browser_sync::DISABLE_SYNC_ON_CLIENT; | |
360 } else if (response->has_error()) { | |
361 // This is a new server. Just get the error from the protocol. | |
362 sync_protocol_error = ConvertErrorPBToLocalType(response->error()); | |
363 } else { | |
364 // Legacy server implementation. Compute the error based on |error_code|. | |
365 sync_protocol_error = ConvertLegacyErrorCodeToNewError( | |
366 response->error_code()); | |
367 } | |
368 | |
369 // Now set the error into the status so the layers above us could read it. | |
370 sessions::StatusController* status = session->mutable_status_controller(); | |
371 status->set_sync_protocol_error(sync_protocol_error); | |
372 | |
373 // Inform the delegate of the error we got. | |
374 session->delegate()->OnSyncProtocolError(session->TakeSnapshot()); | |
375 | |
376 // Now do any special handling for the error type and decide on the return | |
377 // value. | |
378 switch (sync_protocol_error.error_type) { | |
379 case browser_sync::UNKNOWN_ERROR: | |
380 LOG(WARNING) << "Sync protocol out-of-date. The server is using a more " | |
381 << "recent version."; | |
382 return SERVER_RETURN_UNKNOWN_ERROR; | |
383 case browser_sync::SYNC_SUCCESS: | |
384 LogResponseProfilingData(*response); | |
385 return SYNCER_OK; | |
386 case browser_sync::THROTTLED: | |
387 LOG(WARNING) << "Client silenced by server."; | |
388 HandleThrottleError(sync_protocol_error, | |
389 base::TimeTicks::Now() + GetThrottleDelay(*response), | |
390 session->context(), | |
391 session->delegate()); | |
392 return SERVER_RETURN_THROTTLED; | |
393 case browser_sync::TRANSIENT_ERROR: | |
394 return SERVER_RETURN_TRANSIENT_ERROR; | |
395 case browser_sync::MIGRATION_DONE: | |
396 HandleMigrationDoneResponse(response, session); | |
397 return SERVER_RETURN_MIGRATION_DONE; | |
398 case browser_sync::CLEAR_PENDING: | |
399 return SERVER_RETURN_CLEAR_PENDING; | |
400 case browser_sync::NOT_MY_BIRTHDAY: | |
401 return SERVER_RETURN_NOT_MY_BIRTHDAY; | |
402 default: | |
403 NOTREACHED(); | |
404 return UNSET; | |
405 } | |
406 } | |
407 | |
408 // static | |
409 bool SyncerProtoUtil::Compare(const syncable::Entry& local_entry, | |
410 const SyncEntity& server_entry) { | |
411 const std::string name = NameFromSyncEntity(server_entry); | |
412 | |
413 CHECK(local_entry.Get(ID) == server_entry.id()) << | |
414 " SyncerProtoUtil::Compare precondition not met."; | |
415 CHECK(server_entry.version() == local_entry.Get(BASE_VERSION)) << | |
416 " SyncerProtoUtil::Compare precondition not met."; | |
417 CHECK(!local_entry.Get(IS_UNSYNCED)) << | |
418 " SyncerProtoUtil::Compare precondition not met."; | |
419 | |
420 if (local_entry.Get(IS_DEL) && server_entry.deleted()) | |
421 return true; | |
422 if (local_entry.Get(CTIME) != ProtoTimeToTime(server_entry.ctime())) { | |
423 LOG(WARNING) << "ctime mismatch"; | |
424 return false; | |
425 } | |
426 | |
427 // These checks are somewhat prolix, but they're easier to debug than a big | |
428 // boolean statement. | |
429 string client_name = local_entry.Get(syncable::NON_UNIQUE_NAME); | |
430 if (client_name != name) { | |
431 LOG(WARNING) << "Client name mismatch"; | |
432 return false; | |
433 } | |
434 if (local_entry.Get(PARENT_ID) != server_entry.parent_id()) { | |
435 LOG(WARNING) << "Parent ID mismatch"; | |
436 return false; | |
437 } | |
438 if (local_entry.Get(IS_DIR) != server_entry.IsFolder()) { | |
439 LOG(WARNING) << "Dir field mismatch"; | |
440 return false; | |
441 } | |
442 if (local_entry.Get(IS_DEL) != server_entry.deleted()) { | |
443 LOG(WARNING) << "Deletion mismatch"; | |
444 return false; | |
445 } | |
446 if (!local_entry.Get(IS_DIR) && | |
447 (local_entry.Get(MTIME) != ProtoTimeToTime(server_entry.mtime()))) { | |
448 LOG(WARNING) << "mtime mismatch"; | |
449 return false; | |
450 } | |
451 | |
452 return true; | |
453 } | |
454 | |
455 // static | |
456 void SyncerProtoUtil::CopyProtoBytesIntoBlob(const std::string& proto_bytes, | |
457 syncable::Blob* blob) { | |
458 syncable::Blob proto_blob(proto_bytes.begin(), proto_bytes.end()); | |
459 blob->swap(proto_blob); | |
460 } | |
461 | |
462 // static | |
463 bool SyncerProtoUtil::ProtoBytesEqualsBlob(const std::string& proto_bytes, | |
464 const syncable::Blob& blob) { | |
465 if (proto_bytes.size() != blob.size()) | |
466 return false; | |
467 return std::equal(proto_bytes.begin(), proto_bytes.end(), blob.begin()); | |
468 } | |
469 | |
470 // static | |
471 void SyncerProtoUtil::CopyBlobIntoProtoBytes(const syncable::Blob& blob, | |
472 std::string* proto_bytes) { | |
473 std::string blob_string(blob.begin(), blob.end()); | |
474 proto_bytes->swap(blob_string); | |
475 } | |
476 | |
477 // static | |
478 const std::string& SyncerProtoUtil::NameFromSyncEntity( | |
479 const sync_pb::SyncEntity& entry) { | |
480 if (entry.has_non_unique_name()) | |
481 return entry.non_unique_name(); | |
482 return entry.name(); | |
483 } | |
484 | |
485 // static | |
486 const std::string& SyncerProtoUtil::NameFromCommitEntryResponse( | |
487 const CommitResponse_EntryResponse& entry) { | |
488 if (entry.has_non_unique_name()) | |
489 return entry.non_unique_name(); | |
490 return entry.name(); | |
491 } | |
492 | |
493 std::string SyncerProtoUtil::SyncEntityDebugString( | |
494 const sync_pb::SyncEntity& entry) { | |
495 const std::string& mtime_str = | |
496 GetTimeDebugString(ProtoTimeToTime(entry.mtime())); | |
497 const std::string& ctime_str = | |
498 GetTimeDebugString(ProtoTimeToTime(entry.ctime())); | |
499 return base::StringPrintf( | |
500 "id: %s, parent_id: %s, " | |
501 "version: %"PRId64"d, " | |
502 "mtime: %" PRId64"d (%s), " | |
503 "ctime: %" PRId64"d (%s), " | |
504 "name: %s, sync_timestamp: %" PRId64"d, " | |
505 "%s ", | |
506 entry.id_string().c_str(), | |
507 entry.parent_id_string().c_str(), | |
508 entry.version(), | |
509 entry.mtime(), mtime_str.c_str(), | |
510 entry.ctime(), ctime_str.c_str(), | |
511 entry.name().c_str(), entry.sync_timestamp(), | |
512 entry.deleted() ? "deleted, ":""); | |
513 } | |
514 | |
515 namespace { | |
516 std::string GetUpdatesResponseString( | |
517 const sync_pb::GetUpdatesResponse& response) { | |
518 std::string output; | |
519 output.append("GetUpdatesResponse:\n"); | |
520 for (int i = 0; i < response.entries_size(); i++) { | |
521 output.append(SyncerProtoUtil::SyncEntityDebugString(response.entries(i))); | |
522 output.append("\n"); | |
523 } | |
524 return output; | |
525 } | |
526 } // namespace | |
527 | |
528 std::string SyncerProtoUtil::ClientToServerResponseDebugString( | |
529 const sync_pb::ClientToServerResponse& response) { | |
530 // Add more handlers as needed. | |
531 std::string output; | |
532 if (response.has_get_updates()) | |
533 output.append(GetUpdatesResponseString(response.get_updates())); | |
534 return output; | |
535 } | |
536 | |
537 } // namespace browser_sync | |
OLD | NEW |