blob: 38eebe28aba1393acb593d647f597d8276f5c616 [file] [log] [blame]
Darin Petkovb72b62e2012-05-15 16:55:36 +02001// Copyright (c) 2012 The Chromium OS 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 "shill/wimax_provider.h"
6
Darin Petkove4b27022012-05-16 13:28:50 +02007#include <algorithm>
Darin Petkovfc00fd42012-05-30 11:30:06 +02008#include <set>
Darin Petkove4b27022012-05-16 13:28:50 +02009
Darin Petkovb72b62e2012-05-15 16:55:36 +020010#include <base/bind.h>
Darin Petkove4b27022012-05-16 13:28:50 +020011#include <base/string_util.h>
Ben Chanb879d142012-05-15 11:10:32 -070012#include <chromeos/dbus/service_constants.h>
Darin Petkovb72b62e2012-05-15 16:55:36 +020013
Darin Petkovb72b62e2012-05-15 16:55:36 +020014#include "shill/error.h"
Darin Petkovd1cd7972012-05-22 15:26:15 +020015#include "shill/key_value_store.h"
Darin Petkove4b27022012-05-16 13:28:50 +020016#include "shill/manager.h"
Darin Petkovc63dcf02012-05-24 11:51:43 +020017#include "shill/profile.h"
Darin Petkovb72b62e2012-05-15 16:55:36 +020018#include "shill/proxy_factory.h"
19#include "shill/scope_logger.h"
Darin Petkovc63dcf02012-05-24 11:51:43 +020020#include "shill/store_interface.h"
Darin Petkove4b27022012-05-16 13:28:50 +020021#include "shill/wimax.h"
Darin Petkovb72b62e2012-05-15 16:55:36 +020022#include "shill/wimax_manager_proxy_interface.h"
Darin Petkovd1cd7972012-05-22 15:26:15 +020023#include "shill/wimax_service.h"
Darin Petkovb72b62e2012-05-15 16:55:36 +020024
25using base::Bind;
26using base::Unretained;
Darin Petkove4b27022012-05-16 13:28:50 +020027using std::find;
28using std::map;
Darin Petkovc63dcf02012-05-24 11:51:43 +020029using std::set;
Darin Petkovb72b62e2012-05-15 16:55:36 +020030using std::string;
Darin Petkovb72b62e2012-05-15 16:55:36 +020031
32namespace shill {
33
Darin Petkovb72b62e2012-05-15 16:55:36 +020034WiMaxProvider::WiMaxProvider(ControlInterface *control,
35 EventDispatcher *dispatcher,
36 Metrics *metrics,
37 Manager *manager)
38 : control_(control),
39 dispatcher_(dispatcher),
40 metrics_(metrics),
41 manager_(manager),
42 proxy_factory_(ProxyFactory::GetInstance()) {}
43
Darin Petkovc63dcf02012-05-24 11:51:43 +020044WiMaxProvider::~WiMaxProvider() {}
Darin Petkovb72b62e2012-05-15 16:55:36 +020045
46void WiMaxProvider::Start() {
47 SLOG(WiMax, 2) << __func__;
Darin Petkovb501ad22012-07-03 12:50:52 +020048 if (!on_wimax_manager_appear_.IsCancelled()) {
Darin Petkovb72b62e2012-05-15 16:55:36 +020049 return;
50 }
Darin Petkovb501ad22012-07-03 12:50:52 +020051 // Registers a watcher for the WiMaxManager service. This provider will
52 // connect to it if/when the OnWiMaxManagerAppear callback is invoked.
53 on_wimax_manager_appear_.Reset(
54 Bind(&WiMaxProvider::OnWiMaxManagerAppear, Unretained(this)));
55 on_wimax_manager_vanish_.Reset(
56 Bind(&WiMaxProvider::DisconnectFromWiMaxManager, Unretained(this)));
57 manager_->dbus_manager()->WatchName(
58 wimax_manager::kWiMaxManagerServiceName,
59 on_wimax_manager_appear_.callback(),
60 on_wimax_manager_vanish_.callback());
Darin Petkovb72b62e2012-05-15 16:55:36 +020061}
62
63void WiMaxProvider::Stop() {
64 SLOG(WiMax, 2) << __func__;
Darin Petkovb501ad22012-07-03 12:50:52 +020065 // TODO(petkov): Deregister the watcher from DBusManager to avoid potential
66 // memory leaks (crosbug.com/32226).
67 on_wimax_manager_appear_.Cancel();
68 on_wimax_manager_vanish_.Cancel();
69 DisconnectFromWiMaxManager();
Darin Petkovc63dcf02012-05-24 11:51:43 +020070 DestroyAllServices();
Darin Petkovb72b62e2012-05-15 16:55:36 +020071}
72
Darin Petkovb501ad22012-07-03 12:50:52 +020073void WiMaxProvider::ConnectToWiMaxManager() {
74 DCHECK(!wimax_manager_proxy_.get());
75 LOG(INFO) << "Connecting to WiMaxManager.";
76 wimax_manager_proxy_.reset(proxy_factory_->CreateWiMaxManagerProxy());
77 wimax_manager_proxy_->set_devices_changed_callback(
78 Bind(&WiMaxProvider::OnDevicesChanged, Unretained(this)));
79 Error error;
80 OnDevicesChanged(wimax_manager_proxy_->Devices(&error));
81}
82
83void WiMaxProvider::DisconnectFromWiMaxManager() {
84 SLOG(WiMax, 2) << __func__;
85 if (!wimax_manager_proxy_.get()) {
86 return;
87 }
88 LOG(INFO) << "Disconnecting from WiMaxManager.";
89 wimax_manager_proxy_.reset();
90 OnDevicesChanged(RpcIdentifiers());
91}
92
93void WiMaxProvider::OnWiMaxManagerAppear(const string &owner) {
94 SLOG(WiMax, 2) << __func__ << "(" << owner << ")";
95 DisconnectFromWiMaxManager();
96 ConnectToWiMaxManager();
97}
98
Darin Petkove4b27022012-05-16 13:28:50 +020099void WiMaxProvider::OnDeviceInfoAvailable(const string &link_name) {
100 SLOG(WiMax, 2) << __func__ << "(" << link_name << ")";
Darin Petkovfc00fd42012-05-30 11:30:06 +0200101 map<string, RpcIdentifier>::const_iterator find_it =
Darin Petkovc1e52732012-05-25 15:23:45 +0200102 pending_devices_.find(link_name);
Darin Petkove4b27022012-05-16 13:28:50 +0200103 if (find_it != pending_devices_.end()) {
104 RpcIdentifier path = find_it->second;
105 CreateDevice(link_name, path);
106 }
107}
108
Darin Petkovc63dcf02012-05-24 11:51:43 +0200109void WiMaxProvider::OnNetworksChanged() {
110 SLOG(WiMax, 2) << __func__;
Darin Petkovfc00fd42012-05-30 11:30:06 +0200111 // Collects a set of live networks from all devices.
112 set<RpcIdentifier> live_networks;
113 for (map<string, WiMaxRefPtr>::const_iterator it = devices_.begin();
Darin Petkovc63dcf02012-05-24 11:51:43 +0200114 it != devices_.end(); ++it) {
115 const set<RpcIdentifier> &networks = it->second->networks();
Darin Petkovfc00fd42012-05-30 11:30:06 +0200116 live_networks.insert(networks.begin(), networks.end());
Darin Petkovc63dcf02012-05-24 11:51:43 +0200117 }
Darin Petkovfc00fd42012-05-30 11:30:06 +0200118 // Removes dead networks from |networks_|.
119 for (map<RpcIdentifier, NetworkInfo>::iterator it = networks_.begin();
120 it != networks_.end(); ) {
Darin Petkovb96a4512012-06-04 11:02:49 +0200121 const RpcIdentifier &path = it->first;
122 if (ContainsKey(live_networks, path)) {
Darin Petkovfc00fd42012-05-30 11:30:06 +0200123 ++it;
124 } else {
Darin Petkovb96a4512012-06-04 11:02:49 +0200125 LOG(INFO) << "WiMAX network disappeared: " << path;
Darin Petkovfc00fd42012-05-30 11:30:06 +0200126 networks_.erase(it++);
127 }
128 }
129 // Retrieves network info into |networks_| for the live networks.
130 for (set<RpcIdentifier>::const_iterator it = live_networks.begin();
131 it != live_networks.end(); ++it) {
132 RetrieveNetworkInfo(*it);
133 }
134 // Stops dead and starts live services based on the current |networks_|.
Darin Petkovc63dcf02012-05-24 11:51:43 +0200135 StopDeadServices();
136 StartLiveServices();
137}
138
Darin Petkovc1e52732012-05-25 15:23:45 +0200139bool WiMaxProvider::OnServiceUnloaded(const WiMaxServiceRefPtr &service) {
140 SLOG(WiMax, 2) << __func__ << "(" << service->GetStorageIdentifier() << ")";
141 if (service->is_default()) {
142 return false;
143 }
144 // Removes the service from the managed service set. The service will be
145 // deregistered from Manager when we release ownership by returning true.
146 services_.erase(service->GetStorageIdentifier());
147 return true;
148}
149
Darin Petkovd1cd7972012-05-22 15:26:15 +0200150WiMaxServiceRefPtr WiMaxProvider::GetService(const KeyValueStore &args,
151 Error *error) {
152 SLOG(WiMax, 2) << __func__;
153 CHECK_EQ(args.GetString(flimflam::kTypeProperty), flimflam::kTypeWimax);
Darin Petkovd1cd7972012-05-22 15:26:15 +0200154 WiMaxNetworkId id = args.LookupString(WiMaxService::kNetworkIdProperty, "");
155 if (id.empty()) {
156 Error::PopulateAndLog(
157 error, Error::kInvalidArguments, "Missing WiMAX network id.");
158 return NULL;
159 }
160 string name = args.LookupString(flimflam::kNameProperty, "");
161 if (name.empty()) {
162 Error::PopulateAndLog(
163 error, Error::kInvalidArguments, "Missing WiMAX service name.");
164 return NULL;
165 }
Darin Petkovc63dcf02012-05-24 11:51:43 +0200166 WiMaxServiceRefPtr service = GetUniqueService(id, name);
Darin Petkovd1cd7972012-05-22 15:26:15 +0200167 CHECK(service);
Darin Petkovfc00fd42012-05-30 11:30:06 +0200168 // Configures the service using the rest of the passed-in arguments.
Darin Petkovd1cd7972012-05-22 15:26:15 +0200169 service->Configure(args, error);
Darin Petkovfc00fd42012-05-30 11:30:06 +0200170 // Starts the service if there's a matching live network.
Darin Petkovc63dcf02012-05-24 11:51:43 +0200171 StartLiveServices();
Darin Petkovd1cd7972012-05-22 15:26:15 +0200172 return service;
173}
174
Darin Petkovc63dcf02012-05-24 11:51:43 +0200175void WiMaxProvider::CreateServicesFromProfile(const ProfileRefPtr &profile) {
176 SLOG(WiMax, 2) << __func__;
177 bool created = false;
178 const StoreInterface *storage = profile->GetConstStorage();
179 set<string> groups = storage->GetGroupsWithKey(Service::kStorageType);
Darin Petkovfc00fd42012-05-30 11:30:06 +0200180 for (set<string>::const_iterator it = groups.begin();
181 it != groups.end(); ++it) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200182 const string &storage_id = *it;
183 string type;
184 if (!storage->GetString(storage_id, Service::kStorageType, &type) ||
185 type != Technology::NameFromIdentifier(Technology::kWiMax)) {
186 continue;
187 }
188 if (FindService(storage_id)) {
189 continue;
190 }
191 WiMaxNetworkId id;
192 if (!storage->GetString(storage_id, WiMaxService::kStorageNetworkId, &id) ||
193 id.empty()) {
194 LOG(ERROR) << "Unable to load network id: " << storage_id;
195 continue;
196 }
197 string name;
198 if (!storage->GetString(storage_id, Service::kStorageName, &name) ||
199 name.empty()) {
200 LOG(ERROR) << "Unable to load service name: " << storage_id;
201 continue;
202 }
203 WiMaxServiceRefPtr service = GetUniqueService(id, name);
204 CHECK(service);
205 if (!profile->ConfigureService(service)) {
206 LOG(ERROR) << "Could not configure service: " << storage_id;
207 }
208 created = true;
209 }
210 if (created) {
211 StartLiveServices();
212 }
213}
214
215WiMaxRefPtr WiMaxProvider::SelectCarrier(const WiMaxServiceRefPtr &service) {
216 SLOG(WiMax, 2) << __func__ << "(" << service->GetStorageIdentifier() << ")";
217 if (devices_.empty()) {
218 LOG(ERROR) << "No WiMAX devices available.";
219 return NULL;
220 }
221 // TODO(petkov): For now, just return the first available device. We need to
222 // be smarter here and select a device that sees |service|'s network.
223 return devices_.begin()->second;
224}
225
226void WiMaxProvider::OnDevicesChanged(const RpcIdentifiers &devices) {
227 SLOG(WiMax, 2) << __func__;
228 DestroyDeadDevices(devices);
229 for (RpcIdentifiers::const_iterator it = devices.begin();
230 it != devices.end(); ++it) {
231 const RpcIdentifier &path = *it;
232 string link_name = GetLinkName(path);
233 if (!link_name.empty()) {
234 CreateDevice(link_name, path);
235 }
236 }
237}
238
Darin Petkove4b27022012-05-16 13:28:50 +0200239void WiMaxProvider::CreateDevice(const string &link_name,
240 const RpcIdentifier &path) {
241 SLOG(WiMax, 2) << __func__ << "(" << link_name << ", " << path << ")";
Darin Petkov0fcd3462012-05-17 11:25:11 +0200242 if (ContainsKey(devices_, link_name)) {
243 SLOG(WiMax, 2) << "Device already exists.";
244 CHECK_EQ(path, devices_[link_name]->path());
245 return;
246 }
Darin Petkove4b27022012-05-16 13:28:50 +0200247 pending_devices_.erase(link_name);
248 DeviceInfo *device_info = manager_->device_info();
249 if (device_info->IsDeviceBlackListed(link_name)) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200250 LOG(INFO) << "WiMAX device not created, interface blacklisted: "
Darin Petkove4b27022012-05-16 13:28:50 +0200251 << link_name;
252 return;
253 }
254 int index = device_info->GetIndex(link_name);
255 if (index < 0) {
256 SLOG(WiMax, 2) << link_name << " pending device info.";
257 // Adds the link to the pending device map, waiting for a notification from
258 // DeviceInfo that it's received information about the device from RTNL.
259 pending_devices_[link_name] = path;
260 return;
261 }
262 ByteString address_bytes;
263 if (!device_info->GetMACAddress(index, &address_bytes)) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200264 LOG(ERROR) << "Unable to create a WiMAX device with no MAC address: "
Darin Petkove4b27022012-05-16 13:28:50 +0200265 << link_name;
266 return;
267 }
268 string address = address_bytes.HexEncode();
269 WiMaxRefPtr device(new WiMax(control_, dispatcher_, metrics_, manager_,
270 link_name, address, index, path));
Darin Petkov0fcd3462012-05-17 11:25:11 +0200271 devices_[link_name] = device;
Darin Petkove4b27022012-05-16 13:28:50 +0200272 device_info->RegisterDevice(device);
Darin Petkovc63dcf02012-05-24 11:51:43 +0200273 LOG(INFO) << "Created WiMAX device: " << link_name << " @ " << path;
Darin Petkove4b27022012-05-16 13:28:50 +0200274}
275
276void WiMaxProvider::DestroyDeadDevices(const RpcIdentifiers &live_devices) {
277 SLOG(WiMax, 2) << __func__ << "(" << live_devices.size() << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200278 for (map<string, RpcIdentifier>::iterator it = pending_devices_.begin();
Darin Petkove4b27022012-05-16 13:28:50 +0200279 it != pending_devices_.end(); ) {
280 if (find(live_devices.begin(), live_devices.end(), it->second) ==
281 live_devices.end()) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200282 LOG(INFO) << "Forgetting pending device: " << it->second;
Darin Petkove4b27022012-05-16 13:28:50 +0200283 pending_devices_.erase(it++);
284 } else {
285 ++it;
286 }
287 }
Darin Petkov0fcd3462012-05-17 11:25:11 +0200288 for (map<string, WiMaxRefPtr>::iterator it = devices_.begin();
Darin Petkove4b27022012-05-16 13:28:50 +0200289 it != devices_.end(); ) {
Darin Petkov0fcd3462012-05-17 11:25:11 +0200290 if (find(live_devices.begin(), live_devices.end(), it->second->path()) ==
Darin Petkove4b27022012-05-16 13:28:50 +0200291 live_devices.end()) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200292 LOG(INFO) << "Destroying device: " << it->first;
Darin Petkovb96a4512012-06-04 11:02:49 +0200293 const WiMaxRefPtr &device = it->second;
294 device->OnDeviceVanished();
295 manager_->device_info()->DeregisterDevice(device);
Darin Petkov0fcd3462012-05-17 11:25:11 +0200296 devices_.erase(it++);
Darin Petkove4b27022012-05-16 13:28:50 +0200297 } else {
298 ++it;
299 }
300 }
301}
302
303string WiMaxProvider::GetLinkName(const RpcIdentifier &path) {
Darin Petkov9893d9c2012-05-17 15:27:31 -0700304 if (StartsWithASCII(path, wimax_manager::kDeviceObjectPathPrefix, true)) {
305 return path.substr(strlen(wimax_manager::kDeviceObjectPathPrefix));
Darin Petkove4b27022012-05-16 13:28:50 +0200306 }
307 LOG(ERROR) << "Unable to determine link name from RPC path: " << path;
308 return string();
Darin Petkovb72b62e2012-05-15 16:55:36 +0200309}
310
Darin Petkovfc00fd42012-05-30 11:30:06 +0200311void WiMaxProvider::RetrieveNetworkInfo(const RpcIdentifier &path) {
312 if (ContainsKey(networks_, path)) {
313 // Nothing to do, the network info is already available.
314 return;
315 }
Darin Petkovb96a4512012-06-04 11:02:49 +0200316 LOG(INFO) << "WiMAX network appeared: " << path;
Darin Petkovfc00fd42012-05-30 11:30:06 +0200317 scoped_ptr<WiMaxNetworkProxyInterface> proxy(
318 proxy_factory_->CreateWiMaxNetworkProxy(path));
319 Error error;
320 NetworkInfo info;
321 info.name = proxy->Name(&error);
322 if (error.IsFailure()) {
323 return;
324 }
325 uint32 identifier = proxy->Identifier(&error);
326 if (error.IsFailure()) {
327 return;
328 }
329 info.id = WiMaxService::ConvertIdentifierToNetworkId(identifier);
330 networks_[path] = info;
331}
332
Darin Petkovc63dcf02012-05-24 11:51:43 +0200333WiMaxServiceRefPtr WiMaxProvider::FindService(const string &storage_id) {
334 SLOG(WiMax, 2) << __func__ << "(" << storage_id << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200335 map<string, WiMaxServiceRefPtr>::const_iterator find_it =
336 services_.find(storage_id);
337 if (find_it == services_.end()) {
338 return NULL;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200339 }
Darin Petkovc1e52732012-05-25 15:23:45 +0200340 const WiMaxServiceRefPtr &service = find_it->second;
341 LOG_IF(ERROR, storage_id != service->GetStorageIdentifier());
342 return service;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200343}
344
345WiMaxServiceRefPtr WiMaxProvider::GetUniqueService(const WiMaxNetworkId &id,
346 const string &name) {
347 SLOG(WiMax, 2) << __func__ << "(" << id << ", " << name << ")";
348 string storage_id = WiMaxService::CreateStorageIdentifier(id, name);
349 WiMaxServiceRefPtr service = FindService(storage_id);
350 if (service) {
351 SLOG(WiMax, 2) << "Service already exists.";
352 return service;
353 }
354 service = new WiMaxService(control_, dispatcher_, metrics_, manager_);
355 service->set_network_id(id);
356 service->set_friendly_name(name);
357 service->InitStorageIdentifier();
Darin Petkovc1e52732012-05-25 15:23:45 +0200358 services_[service->GetStorageIdentifier()] = service;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200359 manager_->RegisterService(service);
360 LOG(INFO) << "Registered WiMAX service: " << service->GetStorageIdentifier();
361 return service;
362}
363
Darin Petkovc63dcf02012-05-24 11:51:43 +0200364void WiMaxProvider::StartLiveServices() {
Darin Petkovfc00fd42012-05-30 11:30:06 +0200365 SLOG(WiMax, 2) << __func__ << "(" << networks_.size() << ")";
366 for (map<RpcIdentifier, NetworkInfo>::const_iterator nit = networks_.begin();
367 nit != networks_.end(); ++nit) {
368 const RpcIdentifier &path = nit->first;
369 const NetworkInfo &info = nit->second;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200370
Darin Petkovfc00fd42012-05-30 11:30:06 +0200371 // Creates the default service for the network, if not already created.
372 GetUniqueService(info.id, info.name)->set_is_default(true);
373
374 // Starts services for this live network.
375 for (map<string, WiMaxServiceRefPtr>::const_iterator it = services_.begin();
376 it != services_.end(); ++it) {
377 const WiMaxServiceRefPtr &service = it->second;
378 if (service->network_id() != info.id || service->IsStarted()) {
379 continue;
380 }
381 if (!service->Start(proxy_factory_->CreateWiMaxNetworkProxy(path))) {
382 LOG(ERROR) << "Unable to start service: "
383 << service->GetStorageIdentifier();
384 }
Darin Petkovc63dcf02012-05-24 11:51:43 +0200385 }
386 }
387}
388
389void WiMaxProvider::StopDeadServices() {
390 SLOG(WiMax, 2) << __func__ << "(" << networks_.size() << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200391 for (map<string, WiMaxServiceRefPtr>::iterator it = services_.begin();
392 it != services_.end(); ) {
Darin Petkovc33eba82012-06-28 10:51:16 +0200393 // Keeps a local reference until we're done with this service.
394 WiMaxServiceRefPtr service = it->second;
Darin Petkovc1e52732012-05-25 15:23:45 +0200395 if (service->IsStarted() &&
396 !ContainsKey(networks_, service->GetNetworkObjectPath())) {
397 service->Stop();
398 // Default services are created and registered when a network becomes
399 // live. They need to be deregistered and destroyed when the network
400 // disappears.
401 if (service->is_default()) {
Darin Petkovc33eba82012-06-28 10:51:16 +0200402 // Removes |service| from the managed service set before deregistering
403 // it from Manager to ensure correct iterator increment (consider
404 // Manager::DeregisterService -> WiMaxService::Unload ->
405 // WiMaxProvider::OnServiceUnloaded -> services_.erase).
Darin Petkovc1e52732012-05-25 15:23:45 +0200406 services_.erase(it++);
Darin Petkovc33eba82012-06-28 10:51:16 +0200407 manager_->DeregisterService(service);
Darin Petkovc1e52732012-05-25 15:23:45 +0200408 continue;
409 }
Darin Petkovc63dcf02012-05-24 11:51:43 +0200410 }
Darin Petkovc1e52732012-05-25 15:23:45 +0200411 ++it;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200412 }
413}
414
415void WiMaxProvider::DestroyAllServices() {
416 SLOG(WiMax, 2) << __func__;
Darin Petkovc33eba82012-06-28 10:51:16 +0200417 while (!services_.empty()) {
418 // Keeps a local reference until we're done with this service.
419 WiMaxServiceRefPtr service = services_.begin()->second;
420 services_.erase(services_.begin());
421 // Stops the service so that it can notify its carrier device, if any.
Darin Petkovc63dcf02012-05-24 11:51:43 +0200422 service->Stop();
423 manager_->DeregisterService(service);
424 LOG(INFO) << "Deregistered WiMAX service: "
425 << service->GetStorageIdentifier();
Darin Petkovc63dcf02012-05-24 11:51:43 +0200426 }
427}
428
Darin Petkovb72b62e2012-05-15 16:55:36 +0200429} // namespace shill