blob: cf39c73c42e74342213921cd40670c958e2369e8 [file] [log] [blame]
Chris Sosa5bac9492014-03-24 11:18:54 -07001// Copyright 2014 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
Alex Vakulenko35e3bab2014-08-15 11:45:46 -07005#include <memory>
Chris Sosa5bac9492014-03-24 11:18:54 -07006#include <string>
Christopher Wiley737ee3a2014-05-08 14:43:58 -07007#include <sysexits.h>
Chris Sosa5bac9492014-03-24 11:18:54 -07008
Christopher Wiley2ffdc142015-03-03 12:57:54 -08009#include <base/cancelable_callback.h>
Chris Sosaea6456d2014-04-09 15:42:01 -070010#include <base/command_line.h>
Alex Vakulenkoceab1772015-01-20 10:50:04 -080011#include <base/json/json_reader.h>
Chris Sosa5bac9492014-03-24 11:18:54 -070012#include <base/logging.h>
Christopher Wiley737ee3a2014-05-08 14:43:58 -070013#include <base/memory/ref_counted.h>
Christopher Wiley2ffdc142015-03-03 12:57:54 -080014#include <base/memory/weak_ptr.h>
Alex Vakulenko94858962014-12-01 17:53:27 -080015#include <base/strings/stringprintf.h>
Alex Vakulenkof3d77e52014-04-15 11:36:32 -070016#include <base/values.h>
Alex Vakulenko9f826a62014-08-23 19:31:27 -070017#include <chromeos/any.h>
Alex Vakulenko94858962014-12-01 17:53:27 -080018#include <chromeos/daemons/dbus_daemon.h>
Alex Vakulenko59355dc2014-08-20 15:47:36 -070019#include <chromeos/data_encoding.h>
Alex Vakulenkoca7cc3b2014-08-27 08:04:22 -070020#include <chromeos/dbus/data_serialization.h>
Alex Vakulenko95877b52014-09-19 15:31:09 -070021#include <chromeos/dbus/dbus_method_invoker.h>
22#include <chromeos/errors/error.h>
Alex Vakulenkoacec6aa2015-04-20 11:00:54 -070023#include <chromeos/strings/string_utils.h>
Alex Vakulenkob9d72642014-09-22 16:49:45 -070024#include <chromeos/variant_dictionary.h>
Chris Sosa5bac9492014-03-24 11:18:54 -070025#include <dbus/bus.h>
Christopher Wiley4b5f04c2014-03-27 14:45:37 -070026#include <dbus/message.h>
Christopher Wiley737ee3a2014-05-08 14:43:58 -070027#include <dbus/object_proxy.h>
Christopher Wileyd71774f2014-05-07 09:58:45 -070028#include <dbus/object_manager.h>
Alex Vakulenkof3d77e52014-04-15 11:36:32 -070029#include <dbus/values_util.h>
Chris Sosa5bac9492014-03-24 11:18:54 -070030
Alex Vakulenko12e2c1a2014-11-21 08:57:57 -080031#include "buffet/dbus-proxies.h"
Chris Sosa5bac9492014-03-24 11:18:54 -070032
Alex Vakulenko94858962014-12-01 17:53:27 -080033using chromeos::Error;
Alex Vakulenko95877b52014-09-19 15:31:09 -070034using chromeos::ErrorPtr;
Alex Vakulenko63bdf082015-08-21 09:27:12 -070035using com::android::Weave::ManagerProxy;
Alex Vakulenko95877b52014-09-19 15:31:09 -070036
Chris Sosa5bac9492014-03-24 11:18:54 -070037namespace {
38
Christopher Wiley4b5f04c2014-03-27 14:45:37 -070039void usage() {
Alex Vakulenko94858962014-12-01 17:53:27 -080040 printf(R"(Possible commands:
41 - TestMethod <message>
Alex Vakulenko94858962014-12-01 17:53:27 -080042 - CheckDeviceRegistered
43 - GetDeviceInfo
44 - RegisterDevice param1=val1&param2=val2...
45 - AddCommand '{"name":"command_name","parameters":{}}'
Nathan Bullock83a5f132015-01-07 09:57:42 -050046 - UpdateState prop_name prop_value
Alex Vakulenkoceab1772015-01-20 10:50:04 -080047 - GetState
Alex Vakulenko94858962014-12-01 17:53:27 -080048 - PendingCommands
Alex Vakulenkoacec6aa2015-04-20 11:00:54 -070049 - SetCommandVisibility pkg1.cmd1[,pkg2.cm2,...] [all|cloud|local|none]
Alex Vakulenko94858962014-12-01 17:53:27 -080050)");
Christopher Wiley4b5f04c2014-03-27 14:45:37 -070051}
52
Alex Vakulenkoceab1772015-01-20 10:50:04 -080053// Helpers for JsonToAny().
54template<typename T>
55chromeos::Any GetJsonValue(const base::Value& json,
56 bool(base::Value::*fnc)(T*) const) {
57 T val;
58 CHECK((json.*fnc)(&val));
59 return val;
60}
61
62template<typename T>
63chromeos::Any GetJsonList(const base::ListValue& list); // Prototype.
64
65// Converts a JSON value into an Any so it can be sent over D-Bus using
66// UpdateState D-Bus method from Buffet.
67chromeos::Any JsonToAny(const base::Value& json) {
68 chromeos::Any prop_value;
69 switch (json.GetType()) {
70 case base::Value::TYPE_NULL:
71 prop_value = nullptr;
72 break;
73 case base::Value::TYPE_BOOLEAN:
74 prop_value = GetJsonValue<bool>(json, &base::Value::GetAsBoolean);
75 break;
76 case base::Value::TYPE_INTEGER:
77 prop_value = GetJsonValue<int>(json, &base::Value::GetAsInteger);
78 break;
79 case base::Value::TYPE_DOUBLE:
80 prop_value = GetJsonValue<double>(json, &base::Value::GetAsDouble);
81 break;
82 case base::Value::TYPE_STRING:
83 prop_value = GetJsonValue<std::string>(json, &base::Value::GetAsString);
84 break;
85 case base::Value::TYPE_BINARY:
86 LOG(FATAL) << "Binary values should not happen";
87 break;
88 case base::Value::TYPE_DICTIONARY: {
89 const base::DictionaryValue* dict = nullptr; // Still owned by |json|.
90 CHECK(json.GetAsDictionary(&dict));
91 chromeos::VariantDictionary var_dict;
92 base::DictionaryValue::Iterator it(*dict);
93 while (!it.IsAtEnd()) {
94 var_dict.emplace(it.key(), JsonToAny(it.value()));
95 it.Advance();
96 }
97 prop_value = var_dict;
98 break;
99 }
100 case base::Value::TYPE_LIST: {
101 const base::ListValue* list = nullptr; // Still owned by |json|.
102 CHECK(json.GetAsList(&list));
103 CHECK(!list->empty()) << "Unable to deduce the type of list elements.";
104 switch ((*list->begin())->GetType()) {
105 case base::Value::TYPE_BOOLEAN:
106 prop_value = GetJsonList<bool>(*list);
107 break;
108 case base::Value::TYPE_INTEGER:
109 prop_value = GetJsonList<int>(*list);
110 break;
111 case base::Value::TYPE_DOUBLE:
112 prop_value = GetJsonList<double>(*list);
113 break;
114 case base::Value::TYPE_STRING:
115 prop_value = GetJsonList<std::string>(*list);
116 break;
117 case base::Value::TYPE_DICTIONARY:
118 prop_value = GetJsonList<chromeos::VariantDictionary>(*list);
119 break;
120 default:
121 LOG(FATAL) << "Unsupported JSON value type for list element: "
122 << (*list->begin())->GetType();
123 }
124 break;
125 }
126 default:
127 LOG(FATAL) << "Unexpected JSON value type: " << json.GetType();
128 break;
129 }
130 return prop_value;
131}
132
133template<typename T>
134chromeos::Any GetJsonList(const base::ListValue& list) {
135 std::vector<T> val;
136 val.reserve(list.GetSize());
137 for (const base::Value* v : list)
138 val.push_back(JsonToAny(*v).Get<T>());
139 return val;
140}
141
Alex Vakulenko1e3a66b2015-05-22 15:48:53 -0700142class Daemon final : public chromeos::DBusDaemon {
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700143 public:
Alex Vakulenko94858962014-12-01 17:53:27 -0800144 Daemon() = default;
145
146 protected:
147 int OnInit() override {
148 int return_code = chromeos::DBusDaemon::OnInit();
149 if (return_code != EX_OK)
150 return return_code;
151
Alex Vakulenko63bdf082015-08-21 09:27:12 -0700152 object_manager_.reset(new com::android::Weave::ObjectManagerProxy{bus_});
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800153 return_code = ScheduleActions();
154 if (return_code == EX_USAGE) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800155 usage();
Alex Vakulenko94858962014-12-01 17:53:27 -0800156 }
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800157 return return_code;
Christopher Wiley4b5f04c2014-03-27 14:45:37 -0700158 }
159
Alex Vakulenko94858962014-12-01 17:53:27 -0800160 void OnShutdown(int* return_code) override {
161 if (*return_code == EX_OK)
162 *return_code = exit_code_;
163 }
Alex Vakulenko35e3bab2014-08-15 11:45:46 -0700164
Alex Vakulenko94858962014-12-01 17:53:27 -0800165 private:
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800166 int ScheduleActions() {
Alex Vakulenko18862e42015-04-02 14:31:10 -0700167 auto args = base::CommandLine::ForCurrentProcess()->GetArgs();
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800168
169 // Pop the command off of the args list.
170 std::string command = args.front();
171 args.erase(args.begin());
172 base::Callback<void(ManagerProxy*)> job;
173 if (command.compare("TestMethod") == 0) {
174 if (!args.empty() && !CheckArgs(command, args, 1))
175 return EX_USAGE;
176 std::string message;
177 if (!args.empty())
178 message = args.back();
179 job = base::Bind(&Daemon::CallTestMethod, weak_factory_.GetWeakPtr(),
180 message);
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800181 } else if (command.compare("CheckDeviceRegistered") == 0 ||
182 command.compare("cr") == 0) {
183 if (!CheckArgs(command, args, 0))
184 return EX_USAGE;
185 job = base::Bind(&Daemon::CallCheckDeviceRegistered,
186 weak_factory_.GetWeakPtr());
187 } else if (command.compare("GetDeviceInfo") == 0 ||
188 command.compare("di") == 0) {
189 if (!CheckArgs(command, args, 0))
190 return EX_USAGE;
191 job = base::Bind(&Daemon::CallGetDeviceInfo,
192 weak_factory_.GetWeakPtr());
193 } else if (command.compare("RegisterDevice") == 0 ||
194 command.compare("rd") == 0) {
195 if (!args.empty() && !CheckArgs(command, args, 1))
196 return EX_USAGE;
197 std::string dict;
198 if (!args.empty())
199 dict = args.back();
200 job = base::Bind(&Daemon::CallRegisterDevice,
201 weak_factory_.GetWeakPtr(), dict);
202 } else if (command.compare("UpdateState") == 0 ||
203 command.compare("us") == 0) {
204 if (!CheckArgs(command, args, 2))
205 return EX_USAGE;
206 job = base::Bind(&Daemon::CallUpdateState, weak_factory_.GetWeakPtr(),
207 args.front(), args.back());
208 } else if (command.compare("GetState") == 0 ||
209 command.compare("gs") == 0) {
210 if (!CheckArgs(command, args, 0))
211 return EX_USAGE;
212 job = base::Bind(&Daemon::CallGetState, weak_factory_.GetWeakPtr());
213 } else if (command.compare("AddCommand") == 0 ||
214 command.compare("ac") == 0) {
215 if (!CheckArgs(command, args, 1))
216 return EX_USAGE;
217 job = base::Bind(&Daemon::CallAddCommand, weak_factory_.GetWeakPtr(),
218 args.back());
219 } else if (command.compare("PendingCommands") == 0 ||
220 command.compare("pc") == 0) {
221 if (!CheckArgs(command, args, 0))
222 return EX_USAGE;
223 // CallGetPendingCommands relies on ObjectManager but it is being
224 // initialized asynchronously without a way to get a callback when
225 // it is ready to be used. So, just wait a bit before calling its
226 // methods.
227 base::MessageLoop::current()->PostDelayedTask(
228 FROM_HERE,
229 base::Bind(&Daemon::CallGetPendingCommands,
230 weak_factory_.GetWeakPtr()),
231 base::TimeDelta::FromMilliseconds(100));
232 } else {
233 fprintf(stderr, "Unknown command: '%s'\n", command.c_str());
234 return EX_USAGE;
235 }
236 if (!job.is_null())
237 object_manager_->SetManagerAddedCallback(job);
238 timeout_task_.Reset(
239 base::Bind(&Daemon::OnJobTimeout, weak_factory_.GetWeakPtr()));
240 base::MessageLoop::current()->PostDelayedTask(
241 FROM_HERE,
242 timeout_task_.callback(),
243 base::TimeDelta::FromSeconds(10));
244
245 return EX_OK;
246 }
247
248 void OnJobComplete() {
249 timeout_task_.Cancel();
250 Quit();
251 }
252
253 void OnJobTimeout() {
254 fprintf(stderr, "Timed out before completing request.");
255 Quit();
256 }
257
Alex Vakulenko94858962014-12-01 17:53:27 -0800258 void ReportError(Error* error) {
259 fprintf(stderr, "Failed to receive a response: %s\n",
260 error->GetMessage().c_str());
261 exit_code_ = EX_UNAVAILABLE;
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800262 OnJobComplete();
Alex Vakulenko94858962014-12-01 17:53:27 -0800263 }
264
265 bool CheckArgs(const std::string& command,
266 const std::vector<std::string>& args,
267 size_t expected_arg_count) {
268 if (args.size() == expected_arg_count)
269 return true;
270 fprintf(stderr, "Invalid number of arguments for command '%s'\n",
271 command.c_str());
Alex Vakulenko94858962014-12-01 17:53:27 -0800272 return false;
273 }
274
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800275 void CallTestMethod(const std::string& message, ManagerProxy* manager_proxy) {
Alex Vakulenko95877b52014-09-19 15:31:09 -0700276 ErrorPtr error;
Alex Vakulenko95877b52014-09-19 15:31:09 -0700277 std::string response_message;
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800278 if (!manager_proxy->TestMethod(message, &response_message, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800279 return ReportError(error.get());
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700280 }
Alex Vakulenko94858962014-12-01 17:53:27 -0800281 printf("Received a response: %s\n", response_message.c_str());
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800282 OnJobComplete();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700283 }
284
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800285 void CallCheckDeviceRegistered(ManagerProxy* manager_proxy) {
Alex Vakulenko95877b52014-09-19 15:31:09 -0700286 ErrorPtr error;
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700287 std::string device_id;
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800288 if (!manager_proxy->CheckDeviceRegistered(&device_id, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800289 return ReportError(error.get());
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700290 }
291
Alex Vakulenko94858962014-12-01 17:53:27 -0800292 printf("Device ID: %s\n",
293 device_id.empty() ? "<unregistered>" : device_id.c_str());
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800294 OnJobComplete();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700295 }
296
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800297 void CallGetDeviceInfo(ManagerProxy* manager_proxy) {
Alex Vakulenko95877b52014-09-19 15:31:09 -0700298 ErrorPtr error;
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700299 std::string device_info;
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800300 if (!manager_proxy->GetDeviceInfo(&device_info, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800301 return ReportError(error.get());
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700302 }
303
Nathan Bullockf5b91bf2015-04-01 15:32:58 -0400304 printf("%s\n", device_info.c_str());
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800305 OnJobComplete();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700306 }
307
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800308 void CallRegisterDevice(const std::string& args,
309 ManagerProxy* manager_proxy) {
Vitaly Bukacbadabe2015-05-14 23:33:32 -0700310 std::string ticket_id;
Alex Vakulenkof3d77e52014-04-15 11:36:32 -0700311 if (!args.empty()) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800312 auto key_values = chromeos::data_encoding::WebParamsDecode(args);
Alex Vakulenko4ac47992014-06-13 16:10:17 -0700313 for (const auto& pair : key_values) {
Vitaly Bukacbadabe2015-05-14 23:33:32 -0700314 if (pair.first == "ticket_id")
315 ticket_id = pair.second;
Alex Vakulenkof3d77e52014-04-15 11:36:32 -0700316 }
317 }
318
Alex Vakulenko95877b52014-09-19 15:31:09 -0700319 ErrorPtr error;
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700320 std::string device_id;
Vitaly Bukacbadabe2015-05-14 23:33:32 -0700321 if (!manager_proxy->RegisterDevice(ticket_id, &device_id, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800322 return ReportError(error.get());
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700323 }
324
Alex Vakulenko94858962014-12-01 17:53:27 -0800325 printf("Device registered: %s\n", device_id.c_str());
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800326 OnJobComplete();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700327 }
328
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800329 void CallUpdateState(const std::string& prop,
330 const std::string& value,
331 ManagerProxy* manager_proxy) {
Alex Vakulenko95877b52014-09-19 15:31:09 -0700332 ErrorPtr error;
Alex Vakulenkoceab1772015-01-20 10:50:04 -0800333 std::string error_message;
Alex Vakulenkoab4e4ff2015-06-15 12:53:22 -0700334 std::unique_ptr<base::Value> json(
335 base::JSONReader::ReadAndReturnError(value, base::JSON_PARSE_RFC,
336 nullptr, &error_message)
337 .release());
Alex Vakulenkoceab1772015-01-20 10:50:04 -0800338 if (!json) {
339 Error::AddTo(&error, FROM_HERE, chromeos::errors::json::kDomain,
340 chromeos::errors::json::kParseError, error_message);
341 return ReportError(error.get());
342 }
343
344 chromeos::VariantDictionary property_set{{prop, JsonToAny(*json)}};
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800345 if (!manager_proxy->UpdateState(property_set, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800346 return ReportError(error.get());
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700347 }
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800348 OnJobComplete();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700349 }
350
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800351 void CallGetState(ManagerProxy* manager_proxy) {
Alex Vakulenkoceab1772015-01-20 10:50:04 -0800352 std::string json;
353 ErrorPtr error;
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800354 if (!manager_proxy->GetState(&json, &error)) {
Alex Vakulenkoceab1772015-01-20 10:50:04 -0800355 return ReportError(error.get());
356 }
Nathan Bullockf5b91bf2015-04-01 15:32:58 -0400357 printf("%s\n", json.c_str());
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800358 OnJobComplete();
Alex Vakulenkoceab1772015-01-20 10:50:04 -0800359 }
360
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800361 void CallAddCommand(const std::string& command, ManagerProxy* manager_proxy) {
Alex Vakulenko95877b52014-09-19 15:31:09 -0700362 ErrorPtr error;
Vitaly Buka59af7ac2015-03-24 12:42:24 -0700363 std::string id;
Vitaly Bukac03ec2b2015-05-31 23:32:46 -0700364 if (!manager_proxy->AddCommand(command, "owner", &id, &error)) {
Alex Vakulenko94858962014-12-01 17:53:27 -0800365 return ReportError(error.get());
Alex Vakulenko4f7778e2014-09-11 16:57:24 -0700366 }
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800367 OnJobComplete();
Alex Vakulenko4f7778e2014-09-11 16:57:24 -0700368 }
369
Alex Vakulenko94858962014-12-01 17:53:27 -0800370 void CallGetPendingCommands() {
371 printf("Pending commands:\n");
372 for (auto* cmd : object_manager_->GetCommandInstances()) {
Vitaly Bukae25f6fa2015-04-29 12:16:58 -0700373 printf("%10s - '%s' (id:%s)\n", cmd->status().c_str(),
374 cmd->name().c_str(), cmd->id().c_str());
Christopher Wileyd71774f2014-05-07 09:58:45 -0700375 }
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800376 OnJobComplete();
Christopher Wileyd71774f2014-05-07 09:58:45 -0700377 }
378
Alex Vakulenko63bdf082015-08-21 09:27:12 -0700379 std::unique_ptr<com::android::Weave::ObjectManagerProxy> object_manager_;
Christopher Wiley2f772932015-02-15 15:42:04 -0800380 int exit_code_{EX_OK};
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800381 base::CancelableCallback<void()> timeout_task_;
Alex Vakulenko94858962014-12-01 17:53:27 -0800382
Christopher Wiley2ffdc142015-03-03 12:57:54 -0800383 base::WeakPtrFactory<Daemon> weak_factory_{this};
Alex Vakulenko94858962014-12-01 17:53:27 -0800384 DISALLOW_COPY_AND_ASSIGN(Daemon);
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700385};
386
Alex Vakulenko94858962014-12-01 17:53:27 -0800387} // anonymous namespace
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700388
389int main(int argc, char** argv) {
Alex Vakulenko18862e42015-04-02 14:31:10 -0700390 base::CommandLine::Init(argc, argv);
391 base::CommandLine* cl = base::CommandLine::ForCurrentProcess();
392 base::CommandLine::StringVector args = cl->GetArgs();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700393 if (args.empty()) {
Christopher Wiley4b5f04c2014-03-27 14:45:37 -0700394 usage();
Chris Sosaea6456d2014-04-09 15:42:01 -0700395 return EX_USAGE;
Christopher Wiley4b5f04c2014-03-27 14:45:37 -0700396 }
397
Alex Vakulenko94858962014-12-01 17:53:27 -0800398 Daemon daemon;
399 return daemon.Run();
Christopher Wiley737ee3a2014-05-08 14:43:58 -0700400}