blob: a6f45bf5e127d78db7d9421aec1e5ee0c86c2664 [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"
Christopher Wileyb691efd2012-08-09 13:51:51 -070016#include "shill/logging.h"
Darin Petkove4b27022012-05-16 13:28:50 +020017#include "shill/manager.h"
Darin Petkovc63dcf02012-05-24 11:51:43 +020018#include "shill/profile.h"
Darin Petkovb72b62e2012-05-15 16:55:36 +020019#include "shill/proxy_factory.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 // Starts the service if there's a matching live network.
Darin Petkovc63dcf02012-05-24 11:51:43 +0200169 StartLiveServices();
Darin Petkovd1cd7972012-05-22 15:26:15 +0200170 return service;
171}
172
Darin Petkovc63dcf02012-05-24 11:51:43 +0200173void WiMaxProvider::CreateServicesFromProfile(const ProfileRefPtr &profile) {
174 SLOG(WiMax, 2) << __func__;
175 bool created = false;
176 const StoreInterface *storage = profile->GetConstStorage();
177 set<string> groups = storage->GetGroupsWithKey(Service::kStorageType);
Darin Petkovfc00fd42012-05-30 11:30:06 +0200178 for (set<string>::const_iterator it = groups.begin();
179 it != groups.end(); ++it) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200180 const string &storage_id = *it;
181 string type;
182 if (!storage->GetString(storage_id, Service::kStorageType, &type) ||
183 type != Technology::NameFromIdentifier(Technology::kWiMax)) {
184 continue;
185 }
186 if (FindService(storage_id)) {
187 continue;
188 }
189 WiMaxNetworkId id;
190 if (!storage->GetString(storage_id, WiMaxService::kStorageNetworkId, &id) ||
191 id.empty()) {
192 LOG(ERROR) << "Unable to load network id: " << storage_id;
193 continue;
194 }
195 string name;
196 if (!storage->GetString(storage_id, Service::kStorageName, &name) ||
197 name.empty()) {
198 LOG(ERROR) << "Unable to load service name: " << storage_id;
199 continue;
200 }
201 WiMaxServiceRefPtr service = GetUniqueService(id, name);
202 CHECK(service);
203 if (!profile->ConfigureService(service)) {
204 LOG(ERROR) << "Could not configure service: " << storage_id;
205 }
206 created = true;
207 }
208 if (created) {
209 StartLiveServices();
210 }
211}
212
Darin Petkov6b9b2e12012-07-10 15:51:42 +0200213WiMaxRefPtr WiMaxProvider::SelectCarrier(
214 const WiMaxServiceConstRefPtr &service) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200215 SLOG(WiMax, 2) << __func__ << "(" << service->GetStorageIdentifier() << ")";
216 if (devices_.empty()) {
217 LOG(ERROR) << "No WiMAX devices available.";
218 return NULL;
219 }
220 // TODO(petkov): For now, just return the first available device. We need to
221 // be smarter here and select a device that sees |service|'s network.
222 return devices_.begin()->second;
223}
224
225void WiMaxProvider::OnDevicesChanged(const RpcIdentifiers &devices) {
226 SLOG(WiMax, 2) << __func__;
227 DestroyDeadDevices(devices);
228 for (RpcIdentifiers::const_iterator it = devices.begin();
229 it != devices.end(); ++it) {
230 const RpcIdentifier &path = *it;
231 string link_name = GetLinkName(path);
232 if (!link_name.empty()) {
233 CreateDevice(link_name, path);
234 }
235 }
236}
237
Darin Petkove4b27022012-05-16 13:28:50 +0200238void WiMaxProvider::CreateDevice(const string &link_name,
239 const RpcIdentifier &path) {
240 SLOG(WiMax, 2) << __func__ << "(" << link_name << ", " << path << ")";
Darin Petkov0fcd3462012-05-17 11:25:11 +0200241 if (ContainsKey(devices_, link_name)) {
242 SLOG(WiMax, 2) << "Device already exists.";
243 CHECK_EQ(path, devices_[link_name]->path());
244 return;
245 }
Darin Petkove4b27022012-05-16 13:28:50 +0200246 pending_devices_.erase(link_name);
247 DeviceInfo *device_info = manager_->device_info();
248 if (device_info->IsDeviceBlackListed(link_name)) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200249 LOG(INFO) << "WiMAX device not created, interface blacklisted: "
Darin Petkove4b27022012-05-16 13:28:50 +0200250 << link_name;
251 return;
252 }
253 int index = device_info->GetIndex(link_name);
254 if (index < 0) {
255 SLOG(WiMax, 2) << link_name << " pending device info.";
256 // Adds the link to the pending device map, waiting for a notification from
257 // DeviceInfo that it's received information about the device from RTNL.
258 pending_devices_[link_name] = path;
259 return;
260 }
261 ByteString address_bytes;
262 if (!device_info->GetMACAddress(index, &address_bytes)) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200263 LOG(ERROR) << "Unable to create a WiMAX device with no MAC address: "
Darin Petkove4b27022012-05-16 13:28:50 +0200264 << link_name;
265 return;
266 }
267 string address = address_bytes.HexEncode();
268 WiMaxRefPtr device(new WiMax(control_, dispatcher_, metrics_, manager_,
269 link_name, address, index, path));
Darin Petkov0fcd3462012-05-17 11:25:11 +0200270 devices_[link_name] = device;
Darin Petkove4b27022012-05-16 13:28:50 +0200271 device_info->RegisterDevice(device);
Darin Petkovc63dcf02012-05-24 11:51:43 +0200272 LOG(INFO) << "Created WiMAX device: " << link_name << " @ " << path;
Darin Petkove4b27022012-05-16 13:28:50 +0200273}
274
275void WiMaxProvider::DestroyDeadDevices(const RpcIdentifiers &live_devices) {
276 SLOG(WiMax, 2) << __func__ << "(" << live_devices.size() << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200277 for (map<string, RpcIdentifier>::iterator it = pending_devices_.begin();
Darin Petkove4b27022012-05-16 13:28:50 +0200278 it != pending_devices_.end(); ) {
279 if (find(live_devices.begin(), live_devices.end(), it->second) ==
280 live_devices.end()) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200281 LOG(INFO) << "Forgetting pending device: " << it->second;
Darin Petkove4b27022012-05-16 13:28:50 +0200282 pending_devices_.erase(it++);
283 } else {
284 ++it;
285 }
286 }
Darin Petkov0fcd3462012-05-17 11:25:11 +0200287 for (map<string, WiMaxRefPtr>::iterator it = devices_.begin();
Darin Petkove4b27022012-05-16 13:28:50 +0200288 it != devices_.end(); ) {
Darin Petkov0fcd3462012-05-17 11:25:11 +0200289 if (find(live_devices.begin(), live_devices.end(), it->second->path()) ==
Darin Petkove4b27022012-05-16 13:28:50 +0200290 live_devices.end()) {
Darin Petkovc63dcf02012-05-24 11:51:43 +0200291 LOG(INFO) << "Destroying device: " << it->first;
Darin Petkovb96a4512012-06-04 11:02:49 +0200292 const WiMaxRefPtr &device = it->second;
293 device->OnDeviceVanished();
294 manager_->device_info()->DeregisterDevice(device);
Darin Petkov0fcd3462012-05-17 11:25:11 +0200295 devices_.erase(it++);
Darin Petkove4b27022012-05-16 13:28:50 +0200296 } else {
297 ++it;
298 }
299 }
300}
301
302string WiMaxProvider::GetLinkName(const RpcIdentifier &path) {
Darin Petkov9893d9c2012-05-17 15:27:31 -0700303 if (StartsWithASCII(path, wimax_manager::kDeviceObjectPathPrefix, true)) {
304 return path.substr(strlen(wimax_manager::kDeviceObjectPathPrefix));
Darin Petkove4b27022012-05-16 13:28:50 +0200305 }
306 LOG(ERROR) << "Unable to determine link name from RPC path: " << path;
307 return string();
Darin Petkovb72b62e2012-05-15 16:55:36 +0200308}
309
Darin Petkovfc00fd42012-05-30 11:30:06 +0200310void WiMaxProvider::RetrieveNetworkInfo(const RpcIdentifier &path) {
311 if (ContainsKey(networks_, path)) {
312 // Nothing to do, the network info is already available.
313 return;
314 }
Darin Petkovb96a4512012-06-04 11:02:49 +0200315 LOG(INFO) << "WiMAX network appeared: " << path;
Darin Petkovfc00fd42012-05-30 11:30:06 +0200316 scoped_ptr<WiMaxNetworkProxyInterface> proxy(
317 proxy_factory_->CreateWiMaxNetworkProxy(path));
318 Error error;
319 NetworkInfo info;
320 info.name = proxy->Name(&error);
321 if (error.IsFailure()) {
322 return;
323 }
324 uint32 identifier = proxy->Identifier(&error);
325 if (error.IsFailure()) {
326 return;
327 }
328 info.id = WiMaxService::ConvertIdentifierToNetworkId(identifier);
329 networks_[path] = info;
330}
331
Darin Petkovc63dcf02012-05-24 11:51:43 +0200332WiMaxServiceRefPtr WiMaxProvider::FindService(const string &storage_id) {
333 SLOG(WiMax, 2) << __func__ << "(" << storage_id << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200334 map<string, WiMaxServiceRefPtr>::const_iterator find_it =
335 services_.find(storage_id);
336 if (find_it == services_.end()) {
337 return NULL;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200338 }
Darin Petkovc1e52732012-05-25 15:23:45 +0200339 const WiMaxServiceRefPtr &service = find_it->second;
340 LOG_IF(ERROR, storage_id != service->GetStorageIdentifier());
341 return service;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200342}
343
344WiMaxServiceRefPtr WiMaxProvider::GetUniqueService(const WiMaxNetworkId &id,
345 const string &name) {
346 SLOG(WiMax, 2) << __func__ << "(" << id << ", " << name << ")";
347 string storage_id = WiMaxService::CreateStorageIdentifier(id, name);
348 WiMaxServiceRefPtr service = FindService(storage_id);
349 if (service) {
350 SLOG(WiMax, 2) << "Service already exists.";
351 return service;
352 }
353 service = new WiMaxService(control_, dispatcher_, metrics_, manager_);
354 service->set_network_id(id);
355 service->set_friendly_name(name);
356 service->InitStorageIdentifier();
Darin Petkovc1e52732012-05-25 15:23:45 +0200357 services_[service->GetStorageIdentifier()] = service;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200358 manager_->RegisterService(service);
359 LOG(INFO) << "Registered WiMAX service: " << service->GetStorageIdentifier();
360 return service;
361}
362
Darin Petkovc63dcf02012-05-24 11:51:43 +0200363void WiMaxProvider::StartLiveServices() {
Darin Petkovfc00fd42012-05-30 11:30:06 +0200364 SLOG(WiMax, 2) << __func__ << "(" << networks_.size() << ")";
365 for (map<RpcIdentifier, NetworkInfo>::const_iterator nit = networks_.begin();
366 nit != networks_.end(); ++nit) {
367 const RpcIdentifier &path = nit->first;
368 const NetworkInfo &info = nit->second;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200369
Darin Petkovfc00fd42012-05-30 11:30:06 +0200370 // Creates the default service for the network, if not already created.
371 GetUniqueService(info.id, info.name)->set_is_default(true);
372
373 // Starts services for this live network.
374 for (map<string, WiMaxServiceRefPtr>::const_iterator it = services_.begin();
375 it != services_.end(); ++it) {
376 const WiMaxServiceRefPtr &service = it->second;
377 if (service->network_id() != info.id || service->IsStarted()) {
378 continue;
379 }
380 if (!service->Start(proxy_factory_->CreateWiMaxNetworkProxy(path))) {
381 LOG(ERROR) << "Unable to start service: "
382 << service->GetStorageIdentifier();
383 }
Darin Petkovc63dcf02012-05-24 11:51:43 +0200384 }
385 }
386}
387
388void WiMaxProvider::StopDeadServices() {
389 SLOG(WiMax, 2) << __func__ << "(" << networks_.size() << ")";
Darin Petkovc1e52732012-05-25 15:23:45 +0200390 for (map<string, WiMaxServiceRefPtr>::iterator it = services_.begin();
391 it != services_.end(); ) {
Darin Petkovc33eba82012-06-28 10:51:16 +0200392 // Keeps a local reference until we're done with this service.
393 WiMaxServiceRefPtr service = it->second;
Darin Petkovc1e52732012-05-25 15:23:45 +0200394 if (service->IsStarted() &&
395 !ContainsKey(networks_, service->GetNetworkObjectPath())) {
396 service->Stop();
397 // Default services are created and registered when a network becomes
398 // live. They need to be deregistered and destroyed when the network
399 // disappears.
400 if (service->is_default()) {
Darin Petkovc33eba82012-06-28 10:51:16 +0200401 // Removes |service| from the managed service set before deregistering
402 // it from Manager to ensure correct iterator increment (consider
403 // Manager::DeregisterService -> WiMaxService::Unload ->
404 // WiMaxProvider::OnServiceUnloaded -> services_.erase).
Darin Petkovc1e52732012-05-25 15:23:45 +0200405 services_.erase(it++);
Darin Petkovc33eba82012-06-28 10:51:16 +0200406 manager_->DeregisterService(service);
Darin Petkovc1e52732012-05-25 15:23:45 +0200407 continue;
408 }
Darin Petkovc63dcf02012-05-24 11:51:43 +0200409 }
Darin Petkovc1e52732012-05-25 15:23:45 +0200410 ++it;
Darin Petkovc63dcf02012-05-24 11:51:43 +0200411 }
412}
413
414void WiMaxProvider::DestroyAllServices() {
415 SLOG(WiMax, 2) << __func__;
Darin Petkovc33eba82012-06-28 10:51:16 +0200416 while (!services_.empty()) {
417 // Keeps a local reference until we're done with this service.
418 WiMaxServiceRefPtr service = services_.begin()->second;
419 services_.erase(services_.begin());
420 // Stops the service so that it can notify its carrier device, if any.
Darin Petkovc63dcf02012-05-24 11:51:43 +0200421 service->Stop();
422 manager_->DeregisterService(service);
423 LOG(INFO) << "Deregistered WiMAX service: "
424 << service->GetStorageIdentifier();
Darin Petkovc63dcf02012-05-24 11:51:43 +0200425 }
426}
427
Darin Petkovb72b62e2012-05-15 16:55:36 +0200428} // namespace shill