1 /******************************************************************************
2 *
3 * Copyright 2018 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at:
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 ******************************************************************************/
18
19 #include "connection_manager.h"
20
21 #include <base/functional/bind.h>
22 #include <base/functional/callback.h>
23 #include <base/location.h>
24 #include <bluetooth/log.h>
25
26 #include <map>
27 #include <memory>
28 #include <set>
29
30 #include "main/shim/acl_api.h"
31 #include "main/shim/le_scanning_manager.h"
32 #include "os/logging/log_adapter.h"
33 #include "osi/include/alarm.h"
34 #include "stack/btm/btm_dev.h"
35 #include "stack/include/advertise_data_parser.h"
36 #include "stack/include/bt_types.h"
37 #include "stack/include/btm_ble_api.h"
38 #include "stack/include/btm_client_interface.h"
39 #include "stack/include/btm_log_history.h"
40 #include "stack/include/main_thread.h"
41 #include "types/raw_address.h"
42
43 #define DIRECT_CONNECT_TIMEOUT (30 * 1000) /* 30 seconds */
44
45 using namespace bluetooth;
46
47 constexpr char kBtmLogTag[] = "TA";
48
49 struct closure_data {
50 base::OnceClosure user_task;
51 base::Location posted_from;
52 };
53
alarm_closure_cb(void * p)54 static void alarm_closure_cb(void* p) {
55 closure_data* data = (closure_data*)p;
56 log::verbose("executing timer scheduled at {}", data->posted_from.ToString());
57 std::move(data->user_task).Run();
58 delete data;
59 }
60
61 // Periodic alarms are not supported, because we clean up data in callback
alarm_set_closure(const base::Location & posted_from,alarm_t * alarm,uint64_t interval_ms,base::OnceClosure user_task)62 static void alarm_set_closure(const base::Location& posted_from, alarm_t* alarm,
63 uint64_t interval_ms, base::OnceClosure user_task) {
64 closure_data* data = new closure_data;
65 data->posted_from = posted_from;
66 data->user_task = std::move(user_task);
67 log::verbose("scheduling timer {}", data->posted_from.ToString());
68 alarm_set_on_mloop(alarm, interval_ms, alarm_closure_cb, data);
69 }
70
71 using unique_alarm_ptr = std::unique_ptr<alarm_t, decltype(&alarm_free)>;
72
73 namespace connection_manager {
74
75 struct tAPPS_CONNECTING {
76 // ids of clients doing background connection to given device
77 std::set<tAPP_ID> doing_bg_conn;
78 std::set<tAPP_ID> doing_targeted_announcements_conn;
79 bool is_in_accept_list;
80
81 // Apps trying to do direct connection.
82 std::map<tAPP_ID, unique_alarm_ptr> doing_direct_conn;
83 };
84
85 namespace {
86 // Maps address to apps trying to connect to it
87 std::map<RawAddress, tAPPS_CONNECTING> bgconn_dev;
88
num_of_targeted_announcements_users(void)89 int num_of_targeted_announcements_users(void) {
90 return std::count_if(bgconn_dev.begin(), bgconn_dev.end(), [](const auto& pair) {
91 return !pair.second.is_in_accept_list && !pair.second.doing_targeted_announcements_conn.empty();
92 });
93 }
94
is_anyone_interested_to_use_accept_list(const std::map<RawAddress,tAPPS_CONNECTING>::iterator it)95 bool is_anyone_interested_to_use_accept_list(
96 const std::map<RawAddress, tAPPS_CONNECTING>::iterator it) {
97 if (!it->second.doing_targeted_announcements_conn.empty()) {
98 return !it->second.doing_direct_conn.empty();
99 }
100 return !it->second.doing_bg_conn.empty() || !it->second.doing_direct_conn.empty();
101 }
102
is_anyone_connecting(const std::map<RawAddress,tAPPS_CONNECTING>::iterator it)103 bool is_anyone_connecting(const std::map<RawAddress, tAPPS_CONNECTING>::iterator it) {
104 return !it->second.doing_bg_conn.empty() || !it->second.doing_direct_conn.empty() ||
105 !it->second.doing_targeted_announcements_conn.empty();
106 }
107
108 } // namespace
109
110 /** background connection device from the list. Returns pointer to the device
111 * record, or nullptr if not found */
get_apps_connecting_to(const RawAddress & address)112 std::set<tAPP_ID> get_apps_connecting_to(const RawAddress& address) {
113 log::debug("address={}", address);
114 auto it = bgconn_dev.find(address);
115 return (it != bgconn_dev.end()) ? it->second.doing_bg_conn : std::set<tAPP_ID>();
116 }
117
IsTargetedAnnouncement(const uint8_t * p_eir,uint16_t eir_len)118 static bool IsTargetedAnnouncement(const uint8_t* p_eir, uint16_t eir_len) {
119 const uint8_t* p_service_data = p_eir;
120 uint8_t service_data_len = 0;
121
122 while ((p_service_data = AdvertiseDataParser::GetFieldByType(
123 p_service_data + service_data_len,
124 eir_len - (p_service_data - p_eir) - service_data_len,
125 BTM_BLE_AD_TYPE_SERVICE_DATA_TYPE, &service_data_len))) {
126 uint16_t uuid;
127 uint8_t announcement_type;
128 const uint8_t* p_tmp = p_service_data;
129
130 if (service_data_len < 3) {
131 continue;
132 }
133
134 STREAM_TO_UINT16(uuid, p_tmp);
135 log::debug("Found UUID 0x{:04x}", uuid);
136
137 if (uuid != 0x184E && uuid != 0x1853) {
138 continue;
139 }
140
141 STREAM_TO_UINT8(announcement_type, p_tmp);
142 log::debug("Found announcement_type 0x{:02x}", announcement_type);
143 if (announcement_type == 0x01) {
144 return true;
145 }
146 }
147 return false;
148 }
149
150 static void schedule_direct_connect_add(uint8_t app_id, const RawAddress& address);
151
target_announcement_observe_results_cb(tBTM_INQ_RESULTS * p_inq,const uint8_t * p_eir,uint16_t eir_len)152 static void target_announcement_observe_results_cb(tBTM_INQ_RESULTS* p_inq, const uint8_t* p_eir,
153 uint16_t eir_len) {
154 auto addr = p_inq->remote_bd_addr;
155 auto it = bgconn_dev.find(addr);
156 if (it == bgconn_dev.end() || it->second.doing_targeted_announcements_conn.empty()) {
157 return;
158 }
159
160 if (!IsTargetedAnnouncement(p_eir, eir_len)) {
161 log::debug("Not a targeted announcement for device {}", addr);
162 return;
163 }
164
165 log::info("Found targeted announcement for device {}", addr);
166
167 if (it->second.is_in_accept_list) {
168 log::info("Device {} is already connecting", addr);
169 return;
170 }
171
172 if (get_btm_client_interface().peer.BTM_GetHCIConnHandle(addr, BT_TRANSPORT_LE) != 0xFFFF) {
173 log::debug("Device {} already connected", addr);
174 return;
175 }
176
177 BTM_LogHistory(kBtmLogTag, addr, "Found TA from");
178
179 /* Take fist app_id and use it for direct_connect */
180 auto app_id = *(it->second.doing_targeted_announcements_conn.begin());
181
182 /* If scan is ongoing lets stop it */
183 do_in_main_thread(base::BindOnce(schedule_direct_connect_add, app_id, addr));
184 }
185
target_announcements_filtering_set(bool enable)186 static void target_announcements_filtering_set(bool enable) {
187 log::debug("enable {}", enable);
188 BTM_LogHistory(kBtmLogTag, RawAddress::kEmpty, (enable ? "Start filtering" : "Stop filtering"));
189
190 /* Safe to call as if there is no support for filtering, this call will be
191 * ignored. */
192 bluetooth::shim::set_target_announcements_filter(enable);
193 BTM_BleTargetAnnouncementObserve(enable, target_announcement_observe_results_cb);
194 }
195
196 /** Add a device to the background connection list for targeted announcements.
197 * Returns
198 * true if device added to the list, or already in list,
199 * false otherwise
200 */
background_connect_targeted_announcement_add(tAPP_ID app_id,const RawAddress & address)201 bool background_connect_targeted_announcement_add(tAPP_ID app_id, const RawAddress& address) {
202 log::info("app_id={}, address={}", static_cast<int>(app_id), address);
203
204 bool disable_accept_list = false;
205
206 auto it = bgconn_dev.find(address);
207 if (it != bgconn_dev.end()) {
208 // check if filtering already enabled
209 if (it->second.doing_targeted_announcements_conn.count(app_id)) {
210 log::info("app_id={}, already doing targeted announcement filtering to address={}",
211 static_cast<int>(app_id), address);
212 return true;
213 }
214
215 bool targeted_filtering_enabled = !it->second.doing_targeted_announcements_conn.empty();
216
217 // Check if connecting
218 if (!it->second.doing_direct_conn.empty()) {
219 log::info("app_id={}, address={}, already in direct connection", static_cast<int>(app_id),
220 address);
221
222 } else if (!targeted_filtering_enabled && !it->second.doing_bg_conn.empty()) {
223 // device is already in the acceptlist so we would have to remove it
224 log::info("already doing background connection to address={}. Need to disable it.", address);
225 disable_accept_list = true;
226 }
227 }
228
229 if (disable_accept_list) {
230 bluetooth::shim::ACL_IgnoreLeConnectionFrom(BTM_Sec_GetAddressWithType(address));
231 bgconn_dev[address].is_in_accept_list = false;
232 }
233
234 bgconn_dev[address].doing_targeted_announcements_conn.insert(app_id);
235 if (bgconn_dev[address].doing_targeted_announcements_conn.size() == 1) {
236 BTM_LogHistory(kBtmLogTag, address, "Allow connection from");
237 }
238
239 if (num_of_targeted_announcements_users() == 1) {
240 target_announcements_filtering_set(true);
241 }
242
243 return true;
244 }
245
246 /** Add a device from the background connection list. Returns true if device
247 * added to the list, or already in list, false otherwise */
background_connect_add(uint8_t app_id,const RawAddress & address)248 bool background_connect_add(uint8_t app_id, const RawAddress& address) {
249 log::debug("app_id={}, address={}", static_cast<int>(app_id), address);
250 auto it = bgconn_dev.find(address);
251 bool in_acceptlist = false;
252 bool is_targeted_announcement_enabled = false;
253 if (it != bgconn_dev.end()) {
254 // device already in the acceptlist, just add interested app to the list
255 if (it->second.doing_bg_conn.count(app_id)) {
256 log::debug("app_id={}, already doing background connection to address={}",
257 static_cast<int>(app_id), address);
258 return true;
259 }
260
261 // Already in acceptlist ?
262 if (it->second.is_in_accept_list) {
263 log::debug("app_id={}, address={}, already in accept list", static_cast<int>(app_id),
264 address);
265 in_acceptlist = true;
266 } else {
267 is_targeted_announcement_enabled = !it->second.doing_targeted_announcements_conn.empty();
268 }
269 }
270
271 if (!in_acceptlist) {
272 // the device is not in the acceptlist
273 if (is_targeted_announcement_enabled) {
274 log::debug("Targeted announcement enabled, do not add to AcceptList");
275 } else {
276 if (!bluetooth::shim::ACL_AcceptLeConnectionFrom(BTM_Sec_GetAddressWithType(address),
277 false)) {
278 log::warn("Failed to add device {} to accept list for app {}", address,
279 static_cast<int>(app_id));
280 return false;
281 }
282 bgconn_dev[address].is_in_accept_list = true;
283 }
284 }
285
286 // create entry for address, and insert app_id.
287 // new tAPPS_CONNECTING will be default constructed if not exist
288 bgconn_dev[address].doing_bg_conn.insert(app_id);
289 return true;
290 }
291
292 /** Removes all registrations for connection for given device.
293 * Returns true if anything was removed, false otherwise */
remove_unconditional(const RawAddress & address)294 bool remove_unconditional(const RawAddress& address) {
295 log::debug("address={}", address);
296 auto it = bgconn_dev.find(address);
297 if (it == bgconn_dev.end()) {
298 log::warn("address {} is not found", address);
299 return false;
300 }
301
302 bluetooth::shim::ACL_IgnoreLeConnectionFrom(BTM_Sec_GetAddressWithType(address));
303 bgconn_dev.erase(it);
304 return true;
305 }
306
307 /** Remove device from the background connection device list or listening to
308 * advertising list. Returns true if device was on the list and was
309 * successfully removed */
background_connect_remove(uint8_t app_id,const RawAddress & address)310 bool background_connect_remove(uint8_t app_id, const RawAddress& address) {
311 log::debug("app_id={}, address={}", static_cast<int>(app_id), address);
312 auto it = bgconn_dev.find(address);
313 if (it == bgconn_dev.end()) {
314 log::warn("address {} is not found", address);
315 return false;
316 }
317
318 bool accept_list_enabled = it->second.is_in_accept_list;
319 auto num_of_targeted_announcements_before_remove =
320 it->second.doing_targeted_announcements_conn.size();
321
322 bool removed_from_bg_conn = (it->second.doing_bg_conn.erase(app_id) > 0);
323 bool removed_from_ta = (it->second.doing_targeted_announcements_conn.erase(app_id) > 0);
324 if (!removed_from_bg_conn && !removed_from_ta) {
325 log::warn("Failed to remove background connection app {} for address {}",
326 static_cast<int>(app_id), address);
327 return false;
328 }
329
330 if (removed_from_ta && it->second.doing_targeted_announcements_conn.size() == 0) {
331 BTM_LogHistory(kBtmLogTag, address, "Ignore connection from");
332 }
333
334 if (is_anyone_connecting(it)) {
335 log::debug("some device is still connecting, app_id={}, address={}", static_cast<int>(app_id),
336 address);
337 /* Check which method should be used now.*/
338 if (!accept_list_enabled) {
339 /* Accept list was not used */
340 if (!it->second.doing_targeted_announcements_conn.empty()) {
341 /* Keep using filtering */
342 log::debug("Keep using target announcement filtering");
343 } else if (!it->second.doing_bg_conn.empty()) {
344 if (!bluetooth::shim::ACL_AcceptLeConnectionFrom(BTM_Sec_GetAddressWithType(address),
345 false)) {
346 log::warn("Could not re add device to accept list");
347 } else {
348 bgconn_dev[address].is_in_accept_list = true;
349 }
350 }
351 }
352 return true;
353 }
354
355 bgconn_dev.erase(it);
356
357 // no more apps interested - remove from accept list and delete record
358 if (accept_list_enabled) {
359 bluetooth::shim::ACL_IgnoreLeConnectionFrom(BTM_Sec_GetAddressWithType(address));
360 return true;
361 }
362
363 if ((num_of_targeted_announcements_before_remove > 0) &&
364 num_of_targeted_announcements_users() == 0) {
365 target_announcements_filtering_set(true);
366 }
367
368 return true;
369 }
370
is_background_connection(const RawAddress & address)371 bool is_background_connection(const RawAddress& address) {
372 auto it = bgconn_dev.find(address);
373 if (it == bgconn_dev.end()) {
374 return false;
375 }
376 return it->second.is_in_accept_list;
377 }
378
379 /** deregister all related background connetion device. */
on_app_deregistered(uint8_t app_id)380 void on_app_deregistered(uint8_t app_id) {
381 log::debug("app_id={}", static_cast<int>(app_id));
382 auto it = bgconn_dev.begin();
383 auto end = bgconn_dev.end();
384 /* update the BG conn device list */
385 while (it != end) {
386 it->second.doing_bg_conn.erase(app_id);
387
388 it->second.doing_direct_conn.erase(app_id);
389
390 if (is_anyone_connecting(it)) {
391 it++;
392 continue;
393 }
394
395 bluetooth::shim::ACL_IgnoreLeConnectionFrom(BTM_Sec_GetAddressWithType(it->first));
396 it = bgconn_dev.erase(it);
397 }
398 }
399
remove_all_clients_with_pending_connections(const RawAddress & address)400 static void remove_all_clients_with_pending_connections(const RawAddress& address) {
401 log::debug("address={}", address);
402 auto it = bgconn_dev.find(address);
403 while (it != bgconn_dev.end() && !it->second.doing_direct_conn.empty()) {
404 uint8_t app_id = it->second.doing_direct_conn.begin()->first;
405 direct_connect_remove(app_id, address);
406 it = bgconn_dev.find(address);
407 }
408 }
409
on_connection_complete(const RawAddress & address)410 void on_connection_complete(const RawAddress& address) {
411 log::info("Le connection completed to device:{}", address);
412
413 remove_all_clients_with_pending_connections(address);
414 }
415
on_connection_timed_out_from_shim(const RawAddress & address)416 void on_connection_timed_out_from_shim(const RawAddress& address) {
417 log::info("Connection failed {}", address);
418 on_connection_timed_out(0x00, address);
419 }
420
421 /** Reset bg device list. If called after controller reset, set |after_reset|
422 * to true, as there is no need to wipe controller acceptlist in this case. */
reset(bool after_reset)423 void reset(bool after_reset) {
424 bgconn_dev.clear();
425 if (!after_reset) {
426 target_announcements_filtering_set(false);
427 bluetooth::shim::ACL_IgnoreAllLeConnections();
428 }
429 }
430
wl_direct_connect_timeout_cb(uint8_t app_id,const RawAddress & address)431 static void wl_direct_connect_timeout_cb(uint8_t app_id, const RawAddress& address) {
432 log::debug("app_id={}, address={}", static_cast<int>(app_id), address);
433 on_connection_timed_out(app_id, address);
434
435 // TODO: this would free the timer, from within the timer callback, which is
436 // bad.
437 direct_connect_remove(app_id, address, true);
438 }
439
find_in_device_record(const RawAddress & bd_addr,tBLE_BD_ADDR * address_with_type)440 static void find_in_device_record(const RawAddress& bd_addr, tBLE_BD_ADDR* address_with_type) {
441 const tBTM_SEC_DEV_REC* p_dev_rec = btm_find_dev(bd_addr);
442 if (p_dev_rec == nullptr) {
443 return;
444 }
445
446 if (p_dev_rec->device_type & BT_DEVICE_TYPE_BLE) {
447 if (p_dev_rec->ble.identity_address_with_type.bda.IsEmpty()) {
448 *address_with_type = {.type = p_dev_rec->ble.AddressType(), .bda = bd_addr};
449 return;
450 }
451 *address_with_type = p_dev_rec->ble.identity_address_with_type;
452 return;
453 }
454 *address_with_type = {.type = BLE_ADDR_PUBLIC, .bda = bd_addr};
455 return;
456 }
457
create_le_connection(uint8_t,const RawAddress & bd_addr,tBLE_ADDR_TYPE addr_type)458 bool create_le_connection(uint8_t /* id */, const RawAddress& bd_addr, tBLE_ADDR_TYPE addr_type) {
459 tBLE_BD_ADDR address_with_type{
460 .type = addr_type,
461 .bda = bd_addr,
462 };
463
464 find_in_device_record(bd_addr, &address_with_type);
465
466 log::debug("Creating le direct connection to:{} type:{} (initial type: {})", address_with_type,
467 AddressTypeText(address_with_type.type), AddressTypeText(addr_type));
468
469 if (address_with_type.type == BLE_ADDR_ANONYMOUS) {
470 log::warn(
471 "Creating le direct connection to:{}, address type 'anonymous' is "
472 "invalid",
473 address_with_type);
474 return false;
475 }
476
477 bluetooth::shim::ACL_AcceptLeConnectionFrom(address_with_type,
478 /* is_direct */ true);
479 return true;
480 }
481
482 /** Add a device to the direct connection list. Returns true if device
483 * added to the list, false otherwise */
direct_connect_add(uint8_t app_id,const RawAddress & address)484 bool direct_connect_add(uint8_t app_id, const RawAddress& address) {
485 log::debug("app_id={}, address={}", static_cast<int>(app_id), address);
486 bool in_acceptlist = false;
487 auto it = bgconn_dev.find(address);
488 if (it != bgconn_dev.end()) {
489 // app already trying to connect to this particular device
490 if (it->second.doing_direct_conn.count(app_id)) {
491 log::info("direct connect attempt from app_id=0x{:x} already in progress", app_id);
492 return false;
493 }
494
495 // are we already in the acceptlist ?
496 if (it->second.is_in_accept_list) {
497 log::warn("Background connection attempt already in progress app_id={:x}", app_id);
498 in_acceptlist = true;
499 }
500 }
501
502 if (!in_acceptlist) {
503 if (!bluetooth::shim::ACL_AcceptLeConnectionFrom(BTM_Sec_GetAddressWithType(address), true)) {
504 // if we can't add to acceptlist, turn parameters back to slow.
505 log::warn("Unable to add le device to acceptlist");
506 return false;
507 }
508 bgconn_dev[address].is_in_accept_list = true;
509 }
510
511 // Setup a timer
512 alarm_t* timeout = alarm_new("wl_conn_params_30s");
513 alarm_set_closure(FROM_HERE, timeout, DIRECT_CONNECT_TIMEOUT,
514 base::BindOnce(&wl_direct_connect_timeout_cb, app_id, address));
515
516 bgconn_dev[address].doing_direct_conn.emplace(app_id, unique_alarm_ptr(timeout, &alarm_free));
517
518 return true;
519 }
520
schedule_direct_connect_add(uint8_t app_id,const RawAddress & address)521 static void schedule_direct_connect_add(uint8_t app_id, const RawAddress& address) {
522 direct_connect_add(app_id, address);
523 }
524
direct_connect_remove(uint8_t app_id,const RawAddress & address,bool connection_timeout)525 bool direct_connect_remove(uint8_t app_id, const RawAddress& address, bool connection_timeout) {
526 log::debug("app_id={}, address={}", static_cast<int>(app_id), address);
527 auto it = bgconn_dev.find(address);
528 if (it == bgconn_dev.end()) {
529 log::warn("Unable to find background connection to remove peer:{}", address);
530 return false;
531 }
532
533 auto app_it = it->second.doing_direct_conn.find(app_id);
534 if (app_it == it->second.doing_direct_conn.end()) {
535 log::warn("Unable to find direct connection to remove peer:{}", address);
536 return false;
537 }
538
539 /* Let see if the device was connected due to Target Announcements.*/
540 bool is_targeted_announcement_enabled = !it->second.doing_targeted_announcements_conn.empty();
541
542 // this will free the alarm
543 it->second.doing_direct_conn.erase(app_it);
544
545 if (is_anyone_interested_to_use_accept_list(it)) {
546 if (connection_timeout) {
547 /* In such case we need to add device back to allow list because,
548 * when connection timeout out, the lower layer removes device from
549 * the allow list.
550 */
551 if (!bluetooth::shim::ACL_AcceptLeConnectionFrom(BTM_Sec_GetAddressWithType(address),
552 false)) {
553 log::warn("Failed to re-add device {} to accept list after connection timeout", address);
554 }
555 }
556 return true;
557 }
558
559 // no more apps interested - remove from acceptlist
560 bluetooth::shim::ACL_IgnoreLeConnectionFrom(BTM_Sec_GetAddressWithType(address));
561
562 if (!is_targeted_announcement_enabled) {
563 bgconn_dev.erase(it);
564 } else {
565 it->second.is_in_accept_list = false;
566 }
567
568 return true;
569 }
570
dump(int fd)571 void dump(int fd) {
572 dprintf(fd, "\nconnection_manager state:\n");
573 if (bgconn_dev.empty()) {
574 dprintf(fd, "\tno Low Energy connection attempts\n");
575 return;
576 }
577
578 dprintf(fd, "\tdevices attempting connection: %d", (int)bgconn_dev.size());
579 for (const auto& entry : bgconn_dev) {
580 // TODO: confirm whether we need to replace this
581 dprintf(fd, "\n\t * %s:\t\tin_accept_list: %s\t cap_targeted_announcements: %s",
582 ADDRESS_TO_LOGGABLE_CSTR(entry.first),
583 entry.second.is_in_accept_list ? "true" : "false",
584 entry.second.doing_targeted_announcements_conn.empty() ? "false" : "true");
585
586 if (!entry.second.doing_direct_conn.empty()) {
587 dprintf(fd, "\n\t\tapps doing direct connect: ");
588 for (const auto& id : entry.second.doing_direct_conn) {
589 dprintf(fd, "%d, ", id.first);
590 }
591 }
592
593 if (!entry.second.doing_bg_conn.empty()) {
594 dprintf(fd, "\n\t\tapps doing background connect: ");
595 for (const auto& id : entry.second.doing_bg_conn) {
596 dprintf(fd, "%d, ", id);
597 }
598 }
599 }
600 dprintf(fd, "\n");
601 }
602
603 } // namespace connection_manager
604