Home | History | Annotate | Download | only in buffet
      1 // Copyright 2015 The Android Open Source Project
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //      http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 #include "buffet/shill_client.h"
     16 
     17 #include <set>
     18 
     19 #include <base/message_loop/message_loop.h>
     20 #include <base/stl_util.h>
     21 #include <brillo/any.h>
     22 #include <brillo/errors/error.h>
     23 #include <brillo/variant_dictionary.h>
     24 #include <dbus/shill/dbus-constants.h>
     25 #include <weave/enum_to_string.h>
     26 
     27 #include "buffet/ap_manager_client.h"
     28 #include "buffet/socket_stream.h"
     29 #include "buffet/weave_error_conversion.h"
     30 
     31 using brillo::Any;
     32 using brillo::VariantDictionary;
     33 using dbus::ObjectPath;
     34 using org::chromium::flimflam::DeviceProxy;
     35 using org::chromium::flimflam::ServiceProxy;
     36 using std::map;
     37 using std::set;
     38 using std::string;
     39 using std::vector;
     40 using weave::EnumToString;
     41 using weave::provider::Network;
     42 
     43 namespace buffet {
     44 
     45 namespace {
     46 
     47 void IgnoreDetachEvent() {}
     48 
     49 bool GetStateForService(ServiceProxy* service, string* state) {
     50   CHECK(service) << "|service| was nullptr in GetStateForService()";
     51   VariantDictionary properties;
     52   if (!service->GetProperties(&properties, nullptr)) {
     53     LOG(WARNING) << "Failed to read properties from service.";
     54     return false;
     55   }
     56   auto property_it = properties.find(shill::kStateProperty);
     57   if (property_it == properties.end()) {
     58     LOG(WARNING) << "No state found in service properties.";
     59     return false;
     60   }
     61   string new_state = property_it->second.TryGet<string>();
     62   if (new_state.empty()) {
     63     LOG(WARNING) << "Invalid state value.";
     64     return false;
     65   }
     66   *state = new_state;
     67   return true;
     68 }
     69 
     70 Network::State ShillServiceStateToNetworkState(const string& state) {
     71   // TODO(wiley) What does "unconfigured" mean in a world with multiple sets
     72   //             of WiFi credentials?
     73   // TODO(wiley) Detect disabled devices, update state appropriately.
     74   if ((state.compare(shill::kStateReady) == 0) ||
     75       (state.compare(shill::kStatePortal) == 0) ||
     76       (state.compare(shill::kStateOnline) == 0)) {
     77     return Network::State::kOnline;
     78   }
     79   if ((state.compare(shill::kStateAssociation) == 0) ||
     80       (state.compare(shill::kStateConfiguration) == 0)) {
     81     return Network::State::kConnecting;
     82   }
     83   if ((state.compare(shill::kStateFailure) == 0) ||
     84       (state.compare(shill::kStateActivationFailure) == 0)) {
     85     // TODO(wiley) Get error information off the service object.
     86     return Network::State::kError;
     87   }
     88   if ((state.compare(shill::kStateIdle) == 0) ||
     89       (state.compare(shill::kStateOffline) == 0) ||
     90       (state.compare(shill::kStateDisconnect) == 0)) {
     91     return Network::State::kOffline;
     92   }
     93   LOG(WARNING) << "Unknown state found: '" << state << "'";
     94   return Network::State::kOffline;
     95 }
     96 
     97 }  // namespace
     98 
     99 ShillClient::ShillClient(const scoped_refptr<dbus::Bus>& bus,
    100                          const set<string>& device_whitelist,
    101                          bool disable_xmpp)
    102     : bus_{bus},
    103       manager_proxy_{bus_},
    104       device_whitelist_{device_whitelist},
    105       disable_xmpp_{disable_xmpp},
    106       ap_manager_client_{new ApManagerClient(bus)} {
    107   manager_proxy_.RegisterPropertyChangedSignalHandler(
    108       base::Bind(&ShillClient::OnManagerPropertyChange,
    109                  weak_factory_.GetWeakPtr()),
    110       base::Bind(&ShillClient::OnManagerPropertyChangeRegistration,
    111                  weak_factory_.GetWeakPtr()));
    112   auto owner_changed_cb = base::Bind(&ShillClient::OnShillServiceOwnerChange,
    113                                      weak_factory_.GetWeakPtr());
    114   bus_->GetObjectProxy(shill::kFlimflamServiceName, ObjectPath{"/"})
    115       ->SetNameOwnerChangedCallback(owner_changed_cb);
    116 
    117   Init();
    118 }
    119 
    120 ShillClient::~ShillClient() {}
    121 
    122 void ShillClient::Init() {
    123   VLOG(2) << "ShillClient::Init();";
    124   CleanupConnectingService();
    125   devices_.clear();
    126   connectivity_state_ = Network::State::kOffline;
    127   VariantDictionary properties;
    128   if (!manager_proxy_.GetProperties(&properties, nullptr)) {
    129     LOG(ERROR) << "Unable to get properties from Manager, waiting for "
    130                   "Manager to come back online.";
    131     return;
    132   }
    133   auto it = properties.find(shill::kDevicesProperty);
    134   CHECK(it != properties.end()) << "shill should always publish a device list.";
    135   OnManagerPropertyChange(shill::kDevicesProperty, it->second);
    136 }
    137 
    138 void ShillClient::Connect(const string& ssid,
    139                           const string& passphrase,
    140                           const weave::DoneCallback& callback) {
    141   LOG(INFO) << "Connecting to WiFi network: " << ssid;
    142   if (connecting_service_) {
    143     weave::ErrorPtr error;
    144     weave::Error::AddTo(&error, FROM_HERE, "busy",
    145                         "Already connecting to WiFi network");
    146     base::MessageLoop::current()->PostTask(
    147         FROM_HERE, base::Bind(callback, base::Passed(&error)));
    148     return;
    149   }
    150   CleanupConnectingService();
    151   VariantDictionary service_properties;
    152   service_properties[shill::kTypeProperty] = Any{string{shill::kTypeWifi}};
    153   service_properties[shill::kSSIDProperty] = Any{ssid};
    154   if (passphrase.empty()) {
    155     service_properties[shill::kSecurityProperty] = Any{shill::kSecurityNone};
    156   } else {
    157     service_properties[shill::kPassphraseProperty] = Any{passphrase};
    158     service_properties[shill::kSecurityProperty] = Any{shill::kSecurityPsk};
    159   }
    160   service_properties[shill::kSaveCredentialsProperty] = Any{true};
    161   service_properties[shill::kAutoConnectProperty] = Any{true};
    162   ObjectPath service_path;
    163   brillo::ErrorPtr brillo_error;
    164   if (!manager_proxy_.ConfigureService(service_properties, &service_path,
    165                                        &brillo_error) ||
    166       !manager_proxy_.RequestScan(shill::kTypeWifi, &brillo_error)) {
    167     weave::ErrorPtr weave_error;
    168     ConvertError(*brillo_error, &weave_error);
    169     base::MessageLoop::current()->PostTask(
    170         FROM_HERE, base::Bind(callback, base::Passed(&weave_error)));
    171     return;
    172   }
    173   connecting_service_.reset(new ServiceProxy{bus_, service_path});
    174   connecting_service_->Connect(nullptr);
    175   connect_done_callback_ = callback;
    176   connecting_service_->RegisterPropertyChangedSignalHandler(
    177       base::Bind(&ShillClient::OnServicePropertyChange,
    178                  weak_factory_.GetWeakPtr(), service_path),
    179       base::Bind(&ShillClient::OnServicePropertyChangeRegistration,
    180                  weak_factory_.GetWeakPtr(), service_path));
    181   base::MessageLoop::current()->PostDelayedTask(
    182       FROM_HERE, base::Bind(&ShillClient::ConnectToServiceError,
    183                             weak_factory_.GetWeakPtr(), connecting_service_),
    184       base::TimeDelta::FromMinutes(1));
    185 }
    186 
    187 void ShillClient::ConnectToServiceError(
    188     std::shared_ptr<org::chromium::flimflam::ServiceProxy> connecting_service) {
    189   if (connecting_service != connecting_service_ ||
    190       connect_done_callback_.is_null()) {
    191     return;
    192   }
    193   std::string error = have_called_connect_ ? connecting_service_error_
    194                                            : shill::kErrorOutOfRange;
    195   if (error.empty())
    196     error = shill::kErrorInternal;
    197   OnErrorChangeForConnectingService(error);
    198 }
    199 
    200 Network::State ShillClient::GetConnectionState() const {
    201   return connectivity_state_;
    202 }
    203 
    204 void ShillClient::StartAccessPoint(const std::string& ssid) {
    205   LOG(INFO) << "Starting Soft AP: " << ssid;
    206   ap_manager_client_->Start(ssid);
    207 }
    208 
    209 void ShillClient::StopAccessPoint() {
    210   LOG(INFO) << "Stopping Soft AP";
    211   ap_manager_client_->Stop();
    212 }
    213 
    214 void ShillClient::AddConnectionChangedCallback(
    215     const ConnectionChangedCallback& listener) {
    216   connectivity_listeners_.push_back(listener);
    217 }
    218 
    219 bool ShillClient::IsMonitoredDevice(DeviceProxy* device) {
    220   if (device_whitelist_.empty()) {
    221     return true;
    222   }
    223   VariantDictionary device_properties;
    224   if (!device->GetProperties(&device_properties, nullptr)) {
    225     LOG(ERROR) << "Devices without properties aren't whitelisted.";
    226     return false;
    227   }
    228   auto it = device_properties.find(shill::kInterfaceProperty);
    229   if (it == device_properties.end()) {
    230     LOG(ERROR) << "Failed to find interface property in device properties.";
    231     return false;
    232   }
    233   return ContainsKey(device_whitelist_, it->second.TryGet<string>());
    234 }
    235 
    236 void ShillClient::OnShillServiceOwnerChange(const string& old_owner,
    237                                             const string& new_owner) {
    238   VLOG(1) << "Shill service owner name changed to '" << new_owner << "'";
    239   if (new_owner.empty()) {
    240     CleanupConnectingService();
    241     devices_.clear();
    242     connectivity_state_ = Network::State::kOffline;
    243   } else {
    244     Init();  // New service owner means shill reset!
    245   }
    246 }
    247 
    248 void ShillClient::OnManagerPropertyChangeRegistration(const string& interface,
    249                                                       const string& signal_name,
    250                                                       bool success) {
    251   VLOG(3) << "Registered ManagerPropertyChange handler.";
    252   CHECK(success) << "privetd requires Manager signals.";
    253   VariantDictionary properties;
    254   if (!manager_proxy_.GetProperties(&properties, nullptr)) {
    255     LOG(ERROR) << "Unable to get properties from Manager, waiting for "
    256                   "Manager to come back online.";
    257     return;
    258   }
    259   auto it = properties.find(shill::kDevicesProperty);
    260   CHECK(it != properties.end()) << "Shill should always publish a device list.";
    261   OnManagerPropertyChange(shill::kDevicesProperty, it->second);
    262 }
    263 
    264 void ShillClient::OnManagerPropertyChange(const string& property_name,
    265                                           const Any& property_value) {
    266   if (property_name != shill::kDevicesProperty) {
    267     return;
    268   }
    269   bool update_connectivity = false;
    270   VLOG(3) << "Manager's device list has changed.";
    271   // We're going to remove every device we haven't seen in the update.
    272   set<ObjectPath> device_paths_to_remove;
    273   for (const auto& kv : devices_) {
    274     device_paths_to_remove.insert(kv.first);
    275   }
    276   for (const auto& device_path : property_value.TryGet<vector<ObjectPath>>()) {
    277     if (!device_path.IsValid()) {
    278       LOG(ERROR) << "Ignoring invalid device path in Manager's device list.";
    279       return;
    280     }
    281     auto it = devices_.find(device_path);
    282     if (it != devices_.end()) {
    283       // Found an existing proxy.  Since the whitelist never changes,
    284       // this still a valid device.
    285       device_paths_to_remove.erase(device_path);
    286       continue;
    287     }
    288     std::unique_ptr<DeviceProxy> device{new DeviceProxy{bus_, device_path}};
    289     if (!IsMonitoredDevice(device.get())) {
    290       continue;
    291     }
    292     VLOG(3) << "Creating device proxy at " << device_path.value();
    293     devices_[device_path].device = std::move(device);
    294     update_connectivity = true;
    295     devices_[device_path].device->RegisterPropertyChangedSignalHandler(
    296         base::Bind(&ShillClient::OnDevicePropertyChange,
    297                    weak_factory_.GetWeakPtr(), device_path),
    298         base::Bind(&ShillClient::OnDevicePropertyChangeRegistration,
    299                    weak_factory_.GetWeakPtr(), device_path));
    300   }
    301   // Clean up devices/services related to removed devices.
    302   for (const ObjectPath& device_path : device_paths_to_remove) {
    303     devices_.erase(device_path);
    304     update_connectivity = true;
    305   }
    306 
    307   if (update_connectivity)
    308     UpdateConnectivityState();
    309 }
    310 
    311 void ShillClient::OnDevicePropertyChangeRegistration(
    312     const ObjectPath& device_path,
    313     const string& interface,
    314     const string& signal_name,
    315     bool success) {
    316   VLOG(3) << "Registered DevicePropertyChange handler.";
    317   auto it = devices_.find(device_path);
    318   if (it == devices_.end()) {
    319     return;
    320   }
    321   CHECK(success) << "Failed to subscribe to Device property changes.";
    322   DeviceProxy* device = it->second.device.get();
    323   VariantDictionary properties;
    324   if (!device->GetProperties(&properties, nullptr)) {
    325     LOG(WARNING) << "Failed to get device properties?";
    326     return;
    327   }
    328   auto prop_it = properties.find(shill::kSelectedServiceProperty);
    329   if (prop_it == properties.end()) {
    330     LOG(WARNING) << "Failed to get device's selected service?";
    331     return;
    332   }
    333   OnDevicePropertyChange(device_path, shill::kSelectedServiceProperty,
    334                          prop_it->second);
    335 }
    336 
    337 void ShillClient::OnDevicePropertyChange(const ObjectPath& device_path,
    338                                          const string& property_name,
    339                                          const Any& property_value) {
    340   // We only care about selected services anyway.
    341   if (property_name != shill::kSelectedServiceProperty) {
    342     return;
    343   }
    344   // If the device isn't our list of whitelisted devices, ignore it.
    345   auto it = devices_.find(device_path);
    346   if (it == devices_.end()) {
    347     return;
    348   }
    349   DeviceState& device_state = it->second;
    350   ObjectPath service_path{property_value.TryGet<ObjectPath>()};
    351   if (!service_path.IsValid()) {
    352     LOG(ERROR) << "Device at " << device_path.value()
    353                << " selected invalid service path.";
    354     return;
    355   }
    356   VLOG(3) << "Device at " << it->first.value() << " has selected service at "
    357           << service_path.value();
    358   bool removed_old_service{false};
    359   if (device_state.selected_service) {
    360     if (device_state.selected_service->GetObjectPath() == service_path) {
    361       return;  // Spurious update?
    362     }
    363     device_state.selected_service.reset();
    364     device_state.service_state = Network::State::kOffline;
    365     removed_old_service = true;
    366   }
    367   const bool reuse_connecting_service =
    368       service_path.value() != "/" && connecting_service_ &&
    369       connecting_service_->GetObjectPath() == service_path;
    370   if (reuse_connecting_service) {
    371     device_state.selected_service = connecting_service_;
    372     // When we reuse the connecting service, we need to make sure that our
    373     // cached state is correct.  Normally, we do this by relying reading the
    374     // state when our signal handlers finish registering, but this may have
    375     // happened long in the past for the connecting service.
    376     string state;
    377     if (GetStateForService(connecting_service_.get(), &state)) {
    378       device_state.service_state = ShillServiceStateToNetworkState(state);
    379     } else {
    380       LOG(WARNING) << "Failed to read properties from existing service "
    381                       "on selection.";
    382     }
    383   } else if (service_path.value() != "/") {
    384     // The device has selected a new service we haven't see before.
    385     device_state.selected_service =
    386         std::make_shared<ServiceProxy>(bus_, service_path);
    387     device_state.selected_service->RegisterPropertyChangedSignalHandler(
    388         base::Bind(&ShillClient::OnServicePropertyChange,
    389                    weak_factory_.GetWeakPtr(), service_path),
    390         base::Bind(&ShillClient::OnServicePropertyChangeRegistration,
    391                    weak_factory_.GetWeakPtr(), service_path));
    392   }
    393 
    394   if (reuse_connecting_service || removed_old_service) {
    395     UpdateConnectivityState();
    396   }
    397 }
    398 
    399 void ShillClient::OnServicePropertyChangeRegistration(const ObjectPath& path,
    400                                                       const string& interface,
    401                                                       const string& signal_name,
    402                                                       bool success) {
    403   VLOG(3) << "OnServicePropertyChangeRegistration(" << path.value() << ");";
    404   ServiceProxy* service{nullptr};
    405   if (connecting_service_ && connecting_service_->GetObjectPath() == path) {
    406     // Note that the connecting service might also be a selected service.
    407     service = connecting_service_.get();
    408     if (!success)
    409       CleanupConnectingService();
    410   } else {
    411     for (const auto& kv : devices_) {
    412       if (kv.second.selected_service &&
    413           kv.second.selected_service->GetObjectPath() == path) {
    414         service = kv.second.selected_service.get();
    415         break;
    416       }
    417     }
    418   }
    419   if (service == nullptr || !success) {
    420     return;  // A failure or success for a proxy we no longer care about.
    421   }
    422   VariantDictionary properties;
    423   if (!service->GetProperties(&properties, nullptr)) {
    424     return;
    425   }
    426   // Give ourselves property changed signals for the initial property
    427   // values.
    428   for (auto name : {shill::kStateProperty, shill::kSignalStrengthProperty,
    429                     shill::kErrorProperty}) {
    430     auto it = properties.find(name);
    431     if (it != properties.end())
    432       OnServicePropertyChange(path, name, it->second);
    433   }
    434 }
    435 
    436 void ShillClient::OnServicePropertyChange(const ObjectPath& service_path,
    437                                           const string& property_name,
    438                                           const Any& property_value) {
    439   VLOG(3) << "ServicePropertyChange(" << service_path.value() << ", "
    440           << property_name << ", ...);";
    441 
    442   bool is_connecting_service =
    443       connecting_service_ &&
    444       connecting_service_->GetObjectPath() == service_path;
    445   if (property_name == shill::kStateProperty) {
    446     const string state{property_value.TryGet<string>()};
    447     if (state.empty()) {
    448       VLOG(3) << "Invalid service state update.";
    449       return;
    450     }
    451     VLOG(3) << "New service state=" << state;
    452     OnStateChangeForSelectedService(service_path, state);
    453     if (is_connecting_service)
    454       OnStateChangeForConnectingService(state);
    455   } else if (property_name == shill::kSignalStrengthProperty) {
    456     VLOG(3) << "Signal strength=" << property_value.TryGet<uint8_t>();
    457     if (is_connecting_service)
    458       OnStrengthChangeForConnectingService(property_value.TryGet<uint8_t>());
    459   } else if (property_name == shill::kErrorProperty) {
    460     VLOG(3) << "Error=" << property_value.TryGet<std::string>();
    461     if (is_connecting_service)
    462       connecting_service_error_ = property_value.TryGet<std::string>();
    463   }
    464 }
    465 
    466 void ShillClient::OnStateChangeForConnectingService(const string& state) {
    467   switch (ShillServiceStateToNetworkState(state)) {
    468     case Network::State::kOnline: {
    469       auto callback = connect_done_callback_;
    470       connect_done_callback_.Reset();
    471       CleanupConnectingService();
    472 
    473       if (!callback.is_null())
    474         callback.Run(nullptr);
    475       break;
    476     }
    477     case Network::State::kError: {
    478       ConnectToServiceError(connecting_service_);
    479       break;
    480     }
    481     case Network::State::kOffline:
    482     case Network::State::kConnecting:
    483       break;
    484   }
    485 }
    486 
    487 void ShillClient::OnErrorChangeForConnectingService(const std::string& error) {
    488   if (error.empty())
    489     return;
    490 
    491   auto callback = connect_done_callback_;
    492   CleanupConnectingService();
    493 
    494   weave::ErrorPtr weave_error;
    495   weave::Error::AddTo(&weave_error, FROM_HERE, error,
    496                       "Failed to connect to WiFi network");
    497 
    498   if (!callback.is_null())
    499     callback.Run(std::move(weave_error));
    500 }
    501 
    502 void ShillClient::OnStrengthChangeForConnectingService(
    503     uint8_t signal_strength) {
    504   if (signal_strength == 0 || have_called_connect_) {
    505     return;
    506   }
    507   VLOG(1) << "Connecting service has signal. Calling Connect().";
    508   have_called_connect_ = true;
    509   // Failures here indicate that we've already connected,
    510   // or are connecting, or some other very unexciting thing.
    511   // Ignore all that, and rely on state changes to detect
    512   // connectivity.
    513   connecting_service_->Connect(nullptr);
    514 }
    515 
    516 void ShillClient::OnStateChangeForSelectedService(
    517     const ObjectPath& service_path,
    518     const string& state) {
    519   // Find the device/service pair responsible for this update
    520   VLOG(3) << "State for potentially selected service " << service_path.value()
    521           << " have changed to " << state;
    522   for (auto& kv : devices_) {
    523     if (kv.second.selected_service &&
    524         kv.second.selected_service->GetObjectPath() == service_path) {
    525       VLOG(3) << "Updated cached connection state for selected service.";
    526       kv.second.service_state = ShillServiceStateToNetworkState(state);
    527       UpdateConnectivityState();
    528       return;
    529     }
    530   }
    531 }
    532 
    533 void ShillClient::UpdateConnectivityState() {
    534   // Update the connectivity state of the device by picking the
    535   // state of the currently most connected selected service.
    536   Network::State new_connectivity_state{Network::State::kOffline};
    537   for (const auto& kv : devices_) {
    538     if (kv.second.service_state > new_connectivity_state) {
    539       new_connectivity_state = kv.second.service_state;
    540     }
    541   }
    542   VLOG(1) << "Connectivity changed: " << EnumToString(connectivity_state_)
    543           << " -> " << EnumToString(new_connectivity_state);
    544   // Notify listeners even if state changed to the same value. Listeners may
    545   // want to handle this event.
    546   connectivity_state_ = new_connectivity_state;
    547   // We may call UpdateConnectivityState whenever we mutate a data structure
    548   // such that our connectivity status could change.  However, we don't want
    549   // to allow people to call into ShillClient while some other operation is
    550   // underway.  Therefore, call our callbacks later, when we're in a good
    551   // state.
    552   base::MessageLoop::current()->PostTask(
    553       FROM_HERE, base::Bind(&ShillClient::NotifyConnectivityListeners,
    554                             weak_factory_.GetWeakPtr(),
    555                             GetConnectionState() == Network::State::kOnline));
    556 }
    557 
    558 void ShillClient::NotifyConnectivityListeners(bool am_online) {
    559   VLOG(3) << "Notifying connectivity listeners that online=" << am_online;
    560   for (const auto& listener : connectivity_listeners_)
    561     listener.Run();
    562 }
    563 
    564 void ShillClient::CleanupConnectingService() {
    565   if (connecting_service_) {
    566     connecting_service_->ReleaseObjectProxy(base::Bind(&IgnoreDetachEvent));
    567     connecting_service_.reset();
    568   }
    569   connect_done_callback_.Reset();
    570   have_called_connect_ = false;
    571 }
    572 
    573 void ShillClient::OpenSslSocket(const std::string& host,
    574                                 uint16_t port,
    575                                 const OpenSslSocketCallback& callback) {
    576   if (disable_xmpp_)
    577     return;
    578   std::unique_ptr<weave::Stream> raw_stream{
    579       SocketStream::ConnectBlocking(host, port)};
    580   if (!raw_stream) {
    581     brillo::ErrorPtr error;
    582     brillo::errors::system::AddSystemError(&error, FROM_HERE, errno);
    583     weave::ErrorPtr weave_error;
    584     ConvertError(*error.get(), &weave_error);
    585     base::MessageLoop::current()->PostTask(
    586         FROM_HERE, base::Bind(callback, nullptr, base::Passed(&weave_error)));
    587     return;
    588   }
    589 
    590   SocketStream::TlsConnect(std::move(raw_stream), host, callback);
    591 }
    592 
    593 }  // namespace buffet
    594