blob: f08a7a3ea2886c1b4e42c934695df9df5d67d111 [file] [log] [blame]
Shuo Chenccfbe242014-04-29 23:57:51 +08001/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
2/**
Alexander Afanasyev42290b22017-03-09 12:58:29 -08003 * Copyright (c) 2014-2017, Regents of the University of California.
Shuo Chenccfbe242014-04-29 23:57:51 +08004 *
5 * This file is part of NDN repo-ng (Next generation of NDN repository).
6 * See AUTHORS.md for complete list of repo-ng authors and contributors.
7 *
8 * repo-ng is free software: you can redistribute it and/or modify it under the terms
9 * of the GNU General Public License as published by the Free Software Foundation,
10 * either version 3 of the License, or (at your option) any later version.
11 *
12 * repo-ng is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 * PURPOSE. See the GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along with
Alexander Afanasyev42290b22017-03-09 12:58:29 -080017 * repo-ng, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
Shuo Chenccfbe242014-04-29 23:57:51 +080018 */
19
Alexander Afanasyev39d98072014-05-04 12:46:29 -070020#include "../src/repo-command-parameter.hpp"
21#include "../src/repo-command-response.hpp"
Shuo Chenccfbe242014-04-29 23:57:51 +080022
23#include <ndn-cxx/face.hpp>
24#include <ndn-cxx/security/key-chain.hpp>
Junxiao Shi959c5b92016-08-23 01:05:27 +000025#include <ndn-cxx/security/signing-helpers.hpp>
Wentao Shang91fb4f22014-05-20 10:55:22 -070026#include <ndn-cxx/util/scheduler.hpp>
Shuo Chenccfbe242014-04-29 23:57:51 +080027#include <fstream>
28#include <string>
29#include <stdlib.h>
Wentao Shang91fb4f22014-05-20 10:55:22 -070030#include <stdint.h>
Shuo Chenccfbe242014-04-29 23:57:51 +080031#include <boost/filesystem.hpp>
Wentao Shang91fb4f22014-05-20 10:55:22 -070032#include <boost/lexical_cast.hpp>
33#include <boost/asio.hpp>
Shuo Chenccfbe242014-04-29 23:57:51 +080034#include <boost/iostreams/operations.hpp>
35#include <boost/iostreams/read.hpp>
36
37namespace repo {
38
39using namespace ndn::time;
40
Wentao Shanga8f3c402014-10-30 14:03:27 -070041using std::shared_ptr;
42using std::make_shared;
43using std::bind;
44using std::placeholders::_1;
45using std::placeholders::_2;
46
Shuo Chenccfbe242014-04-29 23:57:51 +080047static const uint64_t DEFAULT_BLOCK_SIZE = 1000;
48static const uint64_t DEFAULT_INTEREST_LIFETIME = 4000;
49static const uint64_t DEFAULT_FRESHNESS_PERIOD = 10000;
50static const uint64_t DEFAULT_CHECK_PERIOD = 1000;
Weiqi Shi5822e342014-08-21 20:05:30 -070051static const size_t PRE_SIGN_DATA_COUNT = 11;
Shuo Chenccfbe242014-04-29 23:57:51 +080052
Alexander Afanasyev42290b22017-03-09 12:58:29 -080053class NdnPutFile : boost::noncopyable
Shuo Chenccfbe242014-04-29 23:57:51 +080054{
55public:
56 class Error : public std::runtime_error
57 {
58 public:
59 explicit
60 Error(const std::string& what)
61 : std::runtime_error(what)
62 {
63 }
64 };
65
66 NdnPutFile()
67 : isUnversioned(false)
68 , isSingle(false)
69 , useDigestSha256(false)
70 , freshnessPeriod(DEFAULT_FRESHNESS_PERIOD)
71 , interestLifetime(DEFAULT_INTEREST_LIFETIME)
72 , hasTimeout(false)
73 , timeout(0)
74 , insertStream(0)
75 , isVerbose(false)
Shuo Chenccfbe242014-04-29 23:57:51 +080076 , m_scheduler(m_face.getIoService())
77 , m_timestampVersion(toUnixTimestamp(system_clock::now()).count())
78 , m_processId(0)
79 , m_checkPeriod(DEFAULT_CHECK_PERIOD)
80 , m_currentSegmentNo(0)
81 , m_isFinished(false)
82 {
83 }
84
85 void
86 run();
87
88private:
89 void
90 prepareNextData(uint64_t referenceSegmentNo);
91
92 void
93 startInsertCommand();
94
95 void
Alexander Afanasyev42290b22017-03-09 12:58:29 -080096 onInsertCommandResponse(const ndn::Interest& interest, const ndn::Data& data);
Shuo Chenccfbe242014-04-29 23:57:51 +080097
98 void
99 onInsertCommandTimeout(const ndn::Interest& interest);
100
101 void
102 onInterest(const ndn::Name& prefix, const ndn::Interest& interest);
103
104 void
105 onSingleInterest(const ndn::Name& prefix, const ndn::Interest& interest);
106
107 void
Wentao Shang91fb4f22014-05-20 10:55:22 -0700108 onRegisterSuccess(const ndn::Name& prefix);
109
110 void
Shuo Chenccfbe242014-04-29 23:57:51 +0800111 onRegisterFailed(const ndn::Name& prefix, const std::string& reason);
112
113 void
114 stopProcess();
115
116 void
117 signData(ndn::Data& data);
118
119 void
120 startCheckCommand();
121
122 void
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800123 onCheckCommandResponse(const ndn::Interest& interest, const ndn::Data& data);
Shuo Chenccfbe242014-04-29 23:57:51 +0800124
125 void
126 onCheckCommandTimeout(const ndn::Interest& interest);
127
128 ndn::Interest
129 generateCommandInterest(const ndn::Name& commandPrefix, const std::string& command,
130 const RepoCommandParameter& commandParameter);
131
132public:
133 bool isUnversioned;
134 bool isSingle;
135 bool useDigestSha256;
136 std::string identityForData;
137 std::string identityForCommand;
138 milliseconds freshnessPeriod;
139 milliseconds interestLifetime;
140 bool hasTimeout;
141 milliseconds timeout;
142 ndn::Name repoPrefix;
143 ndn::Name ndnName;
144 std::istream* insertStream;
145 bool isVerbose;
146
147private:
148 ndn::Face m_face;
149 ndn::Scheduler m_scheduler;
150 ndn::KeyChain m_keyChain;
Shuo Chenccfbe242014-04-29 23:57:51 +0800151 uint64_t m_timestampVersion;
152 uint64_t m_processId;
153 milliseconds m_checkPeriod;
154 size_t m_currentSegmentNo;
155 bool m_isFinished;
156 ndn::Name m_dataPrefix;
157
Wentao Shanga8f3c402014-10-30 14:03:27 -0700158 typedef std::map<uint64_t, shared_ptr<ndn::Data> > DataContainer;
Shuo Chenccfbe242014-04-29 23:57:51 +0800159 DataContainer m_data;
160};
161
162void
163NdnPutFile::prepareNextData(uint64_t referenceSegmentNo)
164{
165 // make sure m_data has [referenceSegmentNo, referenceSegmentNo + PRE_SIGN_DATA_COUNT] Data
166 if (m_isFinished)
167 return;
168
169 size_t nDataToPrepare = PRE_SIGN_DATA_COUNT;
170
171 if (!m_data.empty()) {
172 uint64_t maxSegmentNo = m_data.rbegin()->first;
173
174 if (maxSegmentNo - referenceSegmentNo >= nDataToPrepare) {
175 // nothing to prepare
176 return;
177 }
178
179 nDataToPrepare -= maxSegmentNo - referenceSegmentNo;
180 }
181
182 for (size_t i = 0; i < nDataToPrepare && !m_isFinished; ++i) {
183 uint8_t buffer[DEFAULT_BLOCK_SIZE];
184
185 std::streamsize readSize =
186 boost::iostreams::read(*insertStream, reinterpret_cast<char*>(buffer), DEFAULT_BLOCK_SIZE);
187
188 if (readSize <= 0) {
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800189 BOOST_THROW_EXCEPTION(Error("Error reading from the input stream"));
Shuo Chenccfbe242014-04-29 23:57:51 +0800190 }
191
Wentao Shanga8f3c402014-10-30 14:03:27 -0700192 shared_ptr<ndn::Data> data =
193 make_shared<ndn::Data>(Name(m_dataPrefix)
Shuo Chenccfbe242014-04-29 23:57:51 +0800194 .appendSegment(m_currentSegmentNo));
195
196 if (insertStream->peek() == std::istream::traits_type::eof()) {
Weiqi Shi5822e342014-08-21 20:05:30 -0700197 data->setFinalBlockId(ndn::name::Component::fromSegment(m_currentSegmentNo));
Shuo Chenccfbe242014-04-29 23:57:51 +0800198 m_isFinished = true;
199 }
200
201 data->setContent(buffer, readSize);
202 data->setFreshnessPeriod(freshnessPeriod);
203 signData(*data);
204
205 m_data.insert(std::make_pair(m_currentSegmentNo, data));
206
207 ++m_currentSegmentNo;
208 }
209}
210
211void
212NdnPutFile::run()
213{
214 m_dataPrefix = ndnName;
215 if (!isUnversioned)
216 m_dataPrefix.appendVersion(m_timestampVersion);
217
218 if (isVerbose)
219 std::cerr << "setInterestFilter for " << m_dataPrefix << std::endl;
220 m_face.setInterestFilter(m_dataPrefix,
221 isSingle ?
Wentao Shanga8f3c402014-10-30 14:03:27 -0700222 bind(&NdnPutFile::onSingleInterest, this, _1, _2)
223 :
224 bind(&NdnPutFile::onInterest, this, _1, _2),
225 bind(&NdnPutFile::onRegisterSuccess, this, _1),
226 bind(&NdnPutFile::onRegisterFailed, this, _1, _2));
Shuo Chenccfbe242014-04-29 23:57:51 +0800227
Weiqi Shi5822e342014-08-21 20:05:30 -0700228
Shuo Chenccfbe242014-04-29 23:57:51 +0800229 if (hasTimeout)
Wentao Shanga8f3c402014-10-30 14:03:27 -0700230 m_scheduler.scheduleEvent(timeout, bind(&NdnPutFile::stopProcess, this));
Shuo Chenccfbe242014-04-29 23:57:51 +0800231
232 m_face.processEvents();
233}
234
235void
Wentao Shang91fb4f22014-05-20 10:55:22 -0700236NdnPutFile::onRegisterSuccess(const Name& prefix)
237{
238 startInsertCommand();
239}
240
241void
Shuo Chenccfbe242014-04-29 23:57:51 +0800242NdnPutFile::startInsertCommand()
243{
244 RepoCommandParameter parameters;
245 parameters.setName(m_dataPrefix);
246 if (!isSingle) {
247 parameters.setStartBlockId(0);
248 }
249
250 ndn::Interest commandInterest = generateCommandInterest(repoPrefix, "insert", parameters);
251 m_face.expressInterest(commandInterest,
Wentao Shanga8f3c402014-10-30 14:03:27 -0700252 bind(&NdnPutFile::onInsertCommandResponse, this, _1, _2),
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800253 bind(&NdnPutFile::onInsertCommandTimeout, this, _1), // Nack
Wentao Shanga8f3c402014-10-30 14:03:27 -0700254 bind(&NdnPutFile::onInsertCommandTimeout, this, _1));
Shuo Chenccfbe242014-04-29 23:57:51 +0800255}
256
257void
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800258NdnPutFile::onInsertCommandResponse(const ndn::Interest& interest, const ndn::Data& data)
Shuo Chenccfbe242014-04-29 23:57:51 +0800259{
260 RepoCommandResponse response(data.getContent().blockFromValue());
261 int statusCode = response.getStatusCode();
262 if (statusCode >= 400) {
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800263 BOOST_THROW_EXCEPTION(Error("insert command failed with code " +
264 boost::lexical_cast<std::string>(statusCode)));
Shuo Chenccfbe242014-04-29 23:57:51 +0800265 }
266 m_processId = response.getProcessId();
267
268 m_scheduler.scheduleEvent(m_checkPeriod,
Wentao Shanga8f3c402014-10-30 14:03:27 -0700269 bind(&NdnPutFile::startCheckCommand, this));
Shuo Chenccfbe242014-04-29 23:57:51 +0800270}
271
272void
273NdnPutFile::onInsertCommandTimeout(const ndn::Interest& interest)
274{
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800275 BOOST_THROW_EXCEPTION(Error("command response timeout"));
Shuo Chenccfbe242014-04-29 23:57:51 +0800276}
277
278void
279NdnPutFile::onInterest(const ndn::Name& prefix, const ndn::Interest& interest)
280{
281 if (interest.getName().size() != prefix.size() + 1) {
282 if (isVerbose) {
283 std::cerr << "Error processing incoming interest " << interest << ": "
284 << "Unrecognized Interest" << std::endl;
285 }
286 return;
287 }
288
289 uint64_t segmentNo;
290 try {
291 ndn::Name::Component segmentComponent = interest.getName().get(prefix.size());
292 segmentNo = segmentComponent.toSegment();
293 }
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800294 catch (const tlv::Error& e) {
Shuo Chenccfbe242014-04-29 23:57:51 +0800295 if (isVerbose) {
296 std::cerr << "Error processing incoming interest " << interest << ": "
297 << e.what() << std::endl;
298 }
299 return;
300 }
301
302 prepareNextData(segmentNo);
303
304 DataContainer::iterator item = m_data.find(segmentNo);
305 if (item == m_data.end()) {
306 if (isVerbose) {
307 std::cerr << "Requested segment [" << segmentNo << "] does not exist" << std::endl;
308 }
309 return;
310 }
311
Weiqi Shi5822e342014-08-21 20:05:30 -0700312 if (m_isFinished) {
313 uint64_t final = m_currentSegmentNo - 1;
314 item->second->setFinalBlockId(ndn::name::Component::fromSegment(final));
315
316 }
Shuo Chenccfbe242014-04-29 23:57:51 +0800317 m_face.put(*item->second);
318}
319
320void
321NdnPutFile::onSingleInterest(const ndn::Name& prefix, const ndn::Interest& interest)
322{
323 BOOST_ASSERT(prefix == m_dataPrefix);
324
325 if (prefix != interest.getName()) {
326 if (isVerbose) {
327 std::cerr << "Received unexpected interest " << interest << std::endl;
328 }
329 return;
330 }
331
332 uint8_t buffer[DEFAULT_BLOCK_SIZE];
333 std::streamsize readSize =
334 boost::iostreams::read(*insertStream, reinterpret_cast<char*>(buffer), DEFAULT_BLOCK_SIZE);
335
336 if (readSize <= 0) {
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800337 BOOST_THROW_EXCEPTION(Error("Error reading from the input stream"));
Shuo Chenccfbe242014-04-29 23:57:51 +0800338 }
339
340 if (insertStream->peek() != std::istream::traits_type::eof()) {
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800341 BOOST_THROW_EXCEPTION(Error("Input data does not fit into one Data packet"));
Shuo Chenccfbe242014-04-29 23:57:51 +0800342 }
343
Wentao Shanga8f3c402014-10-30 14:03:27 -0700344 shared_ptr<ndn::Data> data = make_shared<ndn::Data>(m_dataPrefix);
Weiqi Shi5822e342014-08-21 20:05:30 -0700345 data->setContent(buffer, readSize);
346 data->setFreshnessPeriod(freshnessPeriod);
347 signData(*data);
348 m_face.put(*data);
Shuo Chenccfbe242014-04-29 23:57:51 +0800349
350 m_isFinished = true;
351}
352
353void
354NdnPutFile::onRegisterFailed(const ndn::Name& prefix, const std::string& reason)
355{
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800356 BOOST_THROW_EXCEPTION(Error("onRegisterFailed: " + reason));
Shuo Chenccfbe242014-04-29 23:57:51 +0800357}
358
359void
360NdnPutFile::stopProcess()
361{
362 m_face.getIoService().stop();
363}
364
365void
366NdnPutFile::signData(ndn::Data& data)
367{
368 if (useDigestSha256) {
369 m_keyChain.signWithSha256(data);
370 }
371 else {
372 if (identityForData.empty())
373 m_keyChain.sign(data);
374 else {
375 ndn::Name keyName = m_keyChain.getDefaultKeyNameForIdentity(ndn::Name(identityForData));
376 ndn::Name certName = m_keyChain.getDefaultCertificateNameForKey(keyName);
377 m_keyChain.sign(data, certName);
378 }
379 }
380}
381
382void
383NdnPutFile::startCheckCommand()
384{
385 ndn::Interest checkInterest = generateCommandInterest(repoPrefix, "insert check",
386 RepoCommandParameter()
387 .setProcessId(m_processId));
388 m_face.expressInterest(checkInterest,
Wentao Shanga8f3c402014-10-30 14:03:27 -0700389 bind(&NdnPutFile::onCheckCommandResponse, this, _1, _2),
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800390 bind(&NdnPutFile::onCheckCommandTimeout, this, _1), // Nack
Wentao Shanga8f3c402014-10-30 14:03:27 -0700391 bind(&NdnPutFile::onCheckCommandTimeout, this, _1));
Shuo Chenccfbe242014-04-29 23:57:51 +0800392}
393
394void
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800395NdnPutFile::onCheckCommandResponse(const ndn::Interest& interest, const ndn::Data& data)
Shuo Chenccfbe242014-04-29 23:57:51 +0800396{
397 RepoCommandResponse response(data.getContent().blockFromValue());
398 int statusCode = response.getStatusCode();
399 if (statusCode >= 400) {
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800400 BOOST_THROW_EXCEPTION(Error("Insert check command failed with code: " +
401 boost::lexical_cast<std::string>(statusCode)));
Shuo Chenccfbe242014-04-29 23:57:51 +0800402 }
403
404 if (m_isFinished) {
405 uint64_t insertCount = response.getInsertNum();
406
407 if (isSingle) {
408 if (insertCount == 1) {
409 m_face.getIoService().stop();
410 return;
411 }
412 }
413 // Technically, the check should not infer, but directly has signal from repo that
414 // write operation has been finished
415
416 if (insertCount == m_currentSegmentNo) {
417 m_face.getIoService().stop();
418 return;
419 }
420 }
421
422 m_scheduler.scheduleEvent(m_checkPeriod,
Wentao Shanga8f3c402014-10-30 14:03:27 -0700423 bind(&NdnPutFile::startCheckCommand, this));
Shuo Chenccfbe242014-04-29 23:57:51 +0800424}
425
426void
427NdnPutFile::onCheckCommandTimeout(const ndn::Interest& interest)
428{
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800429 BOOST_THROW_EXCEPTION(Error("check response timeout"));
Shuo Chenccfbe242014-04-29 23:57:51 +0800430}
431
432ndn::Interest
433NdnPutFile::generateCommandInterest(const ndn::Name& commandPrefix, const std::string& command,
434 const RepoCommandParameter& commandParameter)
435{
436 ndn::Interest interest(ndn::Name(commandPrefix)
437 .append(command)
438 .append(commandParameter.wireEncode()));
439 interest.setInterestLifetime(interestLifetime);
440
441 if (identityForCommand.empty())
Junxiao Shi959c5b92016-08-23 01:05:27 +0000442 m_keyChain.sign(interest);
Shuo Chenccfbe242014-04-29 23:57:51 +0800443 else {
Junxiao Shi959c5b92016-08-23 01:05:27 +0000444 m_keyChain.sign(interest, ndn::signingByIdentity(identityForCommand));
Shuo Chenccfbe242014-04-29 23:57:51 +0800445 }
446
447 return interest;
448}
449
450static void
451usage()
452{
453 fprintf(stderr,
454 "ndnputfile [-u] [-s] [-D] [-d] [-i identity] [-I identity]"
455 " [-x freshness] [-l lifetime] [-w timeout] repo-prefix ndn-name filename\n"
456 "\n"
457 " Write a file into a repo.\n"
458 " -u: unversioned: do not add a version component\n"
459 " -s: single: do not add version or segment component, implies -u\n"
460 " -D: use DigestSha256 signing method instead of SignatureSha256WithRsa\n"
461 " -i: specify identity used for signing Data\n"
462 " -I: specify identity used for signing commands\n"
463 " -x: FreshnessPeriod in milliseconds\n"
464 " -l: InterestLifetime in milliseconds for each command\n"
465 " -w: timeout in milliseconds for whole process (default unlimited)\n"
466 " -v: be verbose\n"
467 " repo-prefix: repo command prefix\n"
468 " ndn-name: NDN Name prefix for written Data\n"
469 " filename: local file name; \"-\" reads from stdin\n"
470 );
471 exit(1);
472}
473
474int
475main(int argc, char** argv)
476{
477 NdnPutFile ndnPutFile;
478 int opt;
479 while ((opt = getopt(argc, argv, "usDi:I:x:l:w:vh")) != -1) {
480 switch (opt) {
481 case 'u':
482 ndnPutFile.isUnversioned = true;
483 break;
484 case 's':
485 ndnPutFile.isSingle = true;
486 break;
487 case 'D':
488 ndnPutFile.useDigestSha256 = true;
489 break;
490 case 'i':
491 ndnPutFile.identityForData = std::string(optarg);
492 break;
493 case 'I':
494 ndnPutFile.identityForCommand = std::string(optarg);
495 break;
496 case 'x':
497 try {
498 ndnPutFile.freshnessPeriod = milliseconds(boost::lexical_cast<uint64_t>(optarg));
499 }
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800500 catch (const boost::bad_lexical_cast&) {
Shuo Chenccfbe242014-04-29 23:57:51 +0800501 std::cerr << "-x option should be an integer.";
502 return 1;
503 }
504 break;
505 case 'l':
506 try {
507 ndnPutFile.interestLifetime = milliseconds(boost::lexical_cast<uint64_t>(optarg));
508 }
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800509 catch (const boost::bad_lexical_cast&) {
Shuo Chenccfbe242014-04-29 23:57:51 +0800510 std::cerr << "-l option should be an integer.";
511 return 1;
512 }
513 break;
514 case 'w':
515 ndnPutFile.hasTimeout = true;
516 try {
517 ndnPutFile.timeout = milliseconds(boost::lexical_cast<uint64_t>(optarg));
518 }
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800519 catch (const boost::bad_lexical_cast&) {
Shuo Chenccfbe242014-04-29 23:57:51 +0800520 std::cerr << "-w option should be an integer.";
521 return 1;
522 }
523 break;
524 case 'v':
525 ndnPutFile.isVerbose = true;
526 break;
527 case 'h':
528 usage();
529 break;
530 default:
531 break;
532 }
533 }
534
535 argc -= optind;
536 argv += optind;
537
538 if (argc != 3)
539 usage();
540
541 ndnPutFile.repoPrefix = Name(argv[0]);
542 ndnPutFile.ndnName = Name(argv[1]);
543 if (strcmp(argv[2], "-") == 0) {
544
545 ndnPutFile.insertStream = &std::cin;
546 ndnPutFile.run();
547 }
548 else {
549 std::ifstream inputFileStream(argv[2], std::ios::in | std::ios::binary);
550 if (!inputFileStream.is_open()) {
551 std::cerr << "ERROR: cannot open " << argv[2] << std::endl;
552 return 1;
553 }
554
555 ndnPutFile.insertStream = &inputFileStream;
556 ndnPutFile.run();
557 }
558
559 // ndnPutFile MUST NOT be used anymore because .insertStream is a dangling pointer
560
561 return 0;
562}
563
564} // namespace repo
565
566int
567main(int argc, char** argv)
568{
569 try {
570 return repo::main(argc, argv);
571 }
Alexander Afanasyev42290b22017-03-09 12:58:29 -0800572 catch (const std::exception& e) {
Shuo Chenccfbe242014-04-29 23:57:51 +0800573 std::cerr << "ERROR: " << e.what() << std::endl;
574 return 2;
575 }
576}