Cleanup: replace some list with vector.
[dcpomatic.git] / src / lib / kdm_cli.cc
1 /*
2     Copyright (C) 2013-2022 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 /** @file  src/tools/dcpomatic_kdm_cli.cc
23  *  @brief Command-line program to generate KDMs.
24  */
25
26
27 #include "cinema.h"
28 #include "config.h"
29 #include "dkdm_wrapper.h"
30 #include "emailer.h"
31 #include "exceptions.h"
32 #include "film.h"
33 #include "kdm_with_metadata.h"
34 #include "screen.h"
35 #include <dcp/certificate.h>
36 #include <dcp/decrypted_kdm.h>
37 #include <dcp/encrypted_kdm.h>
38 #include <getopt.h>
39
40
41 using std::dynamic_pointer_cast;
42 using std::list;
43 using std::make_shared;
44 using std::runtime_error;
45 using std::shared_ptr;
46 using std::string;
47 using std::vector;
48 using boost::optional;
49 using boost::bind;
50 #if BOOST_VERSION >= 106100
51 using namespace boost::placeholders;
52 #endif
53 using namespace dcpomatic;
54
55
56 static void
57 help (std::function<void (string)> out)
58 {
59         out (String::compose("Syntax: %1 [OPTION] <FILM|CPL-ID|DKDM>", program_name));
60         out ("  -h, --help                               show this help");
61         out ("  -o, --output                             output file or directory");
62         out ("  -K, --filename-format                    filename format for KDMs");
63         out ("  -Z, --container-name-format              filename format for ZIP containers");
64         out ("  -f, --valid-from                         valid from time (in local time zone of the cinema) (e.g. \"2013-09-28 01:41:51\") or \"now\"");
65         out ("  -t, --valid-to                           valid to time (in local time zone of the cinema) (e.g. \"2014-09-28 01:41:51\")");
66         out ("  -d, --valid-duration                     valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")");
67         out ("  -F, --formulation                        modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]");
68         out ("  -p, --disable-forensic-marking-picture   disable forensic marking of pictures essences");
69         out ("  -a, --disable-forensic-marking-audio     disable forensic marking of audio essences (optionally above a given channel, e.g 12)");
70         out ("  -e, --email                              email KDMs to cinemas");
71         out ("  -z, --zip                                ZIP each cinema's KDMs into its own file");
72         out ("  -v, --verbose                            be verbose");
73         out ("  -c, --cinema                             cinema name (when using -C) or name/email (to filter cinemas)");
74         out ("  -S, --screen                             screen name (when using -C) or screen name (to filter screens when using -c)");
75         out ("  -C, --certificate                        file containing projector certificate");
76         out ("  -T, --trusted-device                     file containing a trusted device's certificate");
77         out ("      --list-cinemas                       list known cinemas from the DCP-o-matic settings");
78         out ("      --list-dkdm-cpls                     list CPLs for which DCP-o-matic has DKDMs");
79         out ("");
80         out ("CPL-ID must be the ID of a CPL that is mentioned in DCP-o-matic's DKDM list.");
81         out ("");
82         out ("For example:");
83         out ("");
84         out ("Create KDMs for my_great_movie to play in all of Fred's Cinema's screens for the next two weeks and zip them up.");
85         out ("(Fred's Cinema must have been set up in DCP-o-matic's KDM window)");
86         out ("");
87         out (String::compose("\t%1 -c \"Fred's Cinema\" -f now -d \"2 weeks\" -z my_great_movie", program_name));
88 }
89
90
91 class KDMCLIError : public std::runtime_error
92 {
93 public:
94         KDMCLIError (std::string message)
95                 : std::runtime_error (String::compose("%1: %2", program_name, message).c_str())
96         {}
97 };
98
99
100 static boost::posix_time::ptime
101 time_from_string (string t)
102 {
103         if (t == "now") {
104                 return boost::posix_time::second_clock::local_time ();
105         }
106
107         return boost::posix_time::time_from_string (t);
108 }
109
110
111 static boost::posix_time::time_duration
112 duration_from_string (string d)
113 {
114         int N;
115         char unit_buf[64] = "\0";
116         sscanf (d.c_str(), "%d %63s", &N, unit_buf);
117         string const unit (unit_buf);
118
119         if (N == 0) {
120                 throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
121         }
122
123         if (unit == "year" || unit == "years") {
124                 return boost::posix_time::time_duration (N * 24 * 365, 0, 0, 0);
125         } else if (unit == "week" || unit == "weeks") {
126                 return boost::posix_time::time_duration (N * 24 * 7, 0, 0, 0);
127         } else if (unit == "day" || unit == "days") {
128                 return boost::posix_time::time_duration (N * 24, 0, 0, 0);
129         } else if (unit == "hour" || unit == "hours") {
130                 return boost::posix_time::time_duration (N, 0, 0, 0);
131         }
132
133         throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
134 }
135
136
137 static bool
138 always_overwrite ()
139 {
140         return true;
141 }
142
143
144 static
145 void
146 write_files (
147         list<KDMWithMetadataPtr> kdms,
148         bool zip,
149         boost::filesystem::path output,
150         dcp::NameFormat container_name_format,
151         dcp::NameFormat filename_format,
152         bool verbose,
153         std::function<void (string)> out
154         )
155 {
156         if (zip) {
157                 int const N = write_zip_files (
158                         collect (kdms),
159                         output,
160                         container_name_format,
161                         filename_format,
162                         bind (&always_overwrite)
163                         );
164
165                 if (verbose) {
166                         out (String::compose("Wrote %1 ZIP files to %2", N, output));
167                 }
168         } else {
169                 int const N = write_files (
170                         kdms, output, filename_format,
171                         bind (&always_overwrite)
172                         );
173
174                 if (verbose) {
175                         out (String::compose("Wrote %1 KDM files to %2", N, output));
176                 }
177         }
178 }
179
180
181 static
182 shared_ptr<Cinema>
183 find_cinema (string cinema_name)
184 {
185         auto cinemas = Config::instance()->cinemas ();
186         auto i = cinemas.begin();
187         while (
188                 i != cinemas.end() &&
189                 (*i)->name != cinema_name &&
190                 find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
191
192                 ++i;
193         }
194
195         if (i == cinemas.end ()) {
196                 throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name));
197         }
198
199         return *i;
200 }
201
202
203 static
204 void
205 from_film (
206         vector<shared_ptr<Screen>> screens,
207         boost::filesystem::path film_dir,
208         bool verbose,
209         boost::filesystem::path output,
210         dcp::NameFormat container_name_format,
211         dcp::NameFormat filename_format,
212         boost::posix_time::ptime valid_from,
213         boost::posix_time::ptime valid_to,
214         dcp::Formulation formulation,
215         bool disable_forensic_marking_picture,
216         optional<int> disable_forensic_marking_audio,
217         bool email,
218         bool zip,
219         std::function<void (string)> out
220         )
221 {
222         shared_ptr<Film> film;
223         try {
224                 film = make_shared<Film>(film_dir);
225                 film->read_metadata ();
226                 if (verbose) {
227                         out (String::compose("Read film %1", film->name()));
228                 }
229         } catch (std::exception& e) {
230                 throw KDMCLIError (String::compose("error reading film \"%1\" (%2)", film_dir.string(), e.what()));
231         }
232
233         /* XXX: allow specification of this */
234         vector<CPLSummary> cpls = film->cpls ();
235         if (cpls.empty ()) {
236                 throw KDMCLIError ("no CPLs found in film");
237         } else if (cpls.size() > 1) {
238                 throw KDMCLIError ("more than one CPL found in film");
239         }
240
241         auto cpl = cpls.front().cpl_file;
242
243         std::vector<KDMCertificatePeriod> period_checks;
244
245         try {
246                 list<KDMWithMetadataPtr> kdms;
247                 for (auto i: screens) {
248                         std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) {
249                                 return film->make_kdm(cpl, begin, end);
250                         };
251                         auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks);
252                         if (p) {
253                                 kdms.push_back (p);
254                         }
255                 }
256
257
258                 if (find(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OUTSIDE_CERTIFICATE) != period_checks.end()) {
259                         throw KDMCLIError(
260                                 "Some KDMs would have validity periods which are completely outside the recipient certificate periods.  Such KDMs are very unlikely to work, so will not be created."
261                                 );
262                 }
263
264                 if (find(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OVERLAPS_CERTIFICATE) != period_checks.end()) {
265                         out("For some of these KDMs the recipient certificate's validity period will not cover the whole of the KDM validity period.  This might cause problems with the KDMs.");
266                 }
267
268                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
269                 if (email) {
270                         send_emails ({kdms}, container_name_format, filename_format, film->dcp_name(), {});
271                 }
272         } catch (FileError& e) {
273                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
274         }
275 }
276
277
278 static
279 optional<dcp::EncryptedKDM>
280 sub_find_dkdm (shared_ptr<DKDMGroup> group, string cpl_id)
281 {
282         for (auto i: group->children()) {
283                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
284                 if (g) {
285                         auto dkdm = sub_find_dkdm (g, cpl_id);
286                         if (dkdm) {
287                                 return dkdm;
288                         }
289                 } else {
290                         auto d = dynamic_pointer_cast<DKDM>(i);
291                         assert (d);
292                         if (d->dkdm().cpl_id() == cpl_id) {
293                                 return d->dkdm();
294                         }
295                 }
296         }
297
298         return {};
299 }
300
301
302 static
303 optional<dcp::EncryptedKDM>
304 find_dkdm (string cpl_id)
305 {
306         return sub_find_dkdm (Config::instance()->dkdms(), cpl_id);
307 }
308
309
310 static
311 dcp::EncryptedKDM
312 kdm_from_dkdm (
313         dcp::DecryptedKDM dkdm,
314         dcp::Certificate target,
315         vector<string> trusted_devices,
316         dcp::LocalTime valid_from,
317         dcp::LocalTime valid_to,
318         dcp::Formulation formulation,
319         bool disable_forensic_marking_picture,
320         optional<int> disable_forensic_marking_audio
321         )
322 {
323         /* Signer for new KDM */
324         auto signer = Config::instance()->signer_chain ();
325         if (!signer->valid ()) {
326                 throw KDMCLIError ("signing certificate chain is invalid.");
327         }
328
329         /* Make a new empty KDM and add the keys from the DKDM to it */
330         dcp::DecryptedKDM kdm (
331                 valid_from,
332                 valid_to,
333                 dkdm.annotation_text().get_value_or(""),
334                 dkdm.content_title_text(),
335                 dcp::LocalTime().as_string()
336                 );
337
338         for (auto const& j: dkdm.keys()) {
339                 kdm.add_key(j);
340         }
341
342         return kdm.encrypt (signer, target, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
343 }
344
345
346 static
347 void
348 from_dkdm (
349         vector<shared_ptr<Screen>> screens,
350         dcp::DecryptedKDM dkdm,
351         bool verbose,
352         boost::filesystem::path output,
353         dcp::NameFormat container_name_format,
354         dcp::NameFormat filename_format,
355         boost::posix_time::ptime valid_from,
356         boost::posix_time::ptime valid_to,
357         dcp::Formulation formulation,
358         bool disable_forensic_marking_picture,
359         optional<int> disable_forensic_marking_audio,
360         bool email,
361         bool zip,
362         std::function<void (string)> out
363         )
364 {
365         dcp::NameFormat::Map values;
366
367         try {
368                 list<KDMWithMetadataPtr> kdms;
369                 for (auto i: screens) {
370                         if (!i->recipient) {
371                                 continue;
372                         }
373
374                         int const offset_hour = i->cinema ? i->cinema->utc_offset_hour() : 0;
375                         int const offset_minute = i->cinema ? i->cinema->utc_offset_minute() : 0;
376
377                         dcp::LocalTime begin(valid_from, dcp::UTCOffset(offset_hour, offset_minute));
378                         dcp::LocalTime end(valid_to, dcp::UTCOffset(offset_hour, offset_minute));
379
380                         auto const kdm = kdm_from_dkdm(
381                                                         dkdm,
382                                                         i->recipient.get(),
383                                                         i->trusted_device_thumbprints(),
384                                                         begin,
385                                                         end,
386                                                         formulation,
387                                                         disable_forensic_marking_picture,
388                                                         disable_forensic_marking_audio
389                                                         );
390
391                         dcp::NameFormat::Map name_values;
392                         name_values['c'] = i->cinema ? i->cinema->name : "";
393                         name_values['s'] = i->name;
394                         name_values['f'] = kdm.content_title_text();
395                         name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
396                         name_values['e'] = end.date() + " " + end.time_of_day(true, false);
397                         name_values['i'] = kdm.cpl_id();
398
399                         kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
400                 }
401                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
402                 if (email) {
403                         send_emails ({kdms}, container_name_format, filename_format, dkdm.annotation_text().get_value_or(""), {});
404                 }
405         } catch (FileError& e) {
406                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
407         }
408 }
409
410
411 static
412 void
413 dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (string)> out)
414 {
415         auto const indent_string = string(indent, ' ');
416
417         if (indent > 0) {
418                 out (indent_string + group->name());
419         }
420         for (auto i: group->children()) {
421                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
422                 if (g) {
423                         dump_dkdm_group (g, indent + 2, out);
424                 } else {
425                         auto d = dynamic_pointer_cast<DKDM>(i);
426                         assert(d);
427                         out (indent_string + d->dkdm().cpl_id());
428                 }
429         }
430 }
431
432
433 optional<string>
434 kdm_cli (int argc, char* argv[], std::function<void (string)> out)
435 try
436 {
437         boost::filesystem::path output = boost::filesystem::current_path();
438         auto container_name_format = Config::instance()->kdm_container_name_format();
439         auto filename_format = Config::instance()->kdm_filename_format();
440         optional<string> cinema_name;
441         shared_ptr<Cinema> cinema;
442         optional<boost::filesystem::path> certificate;
443         optional<string> screen;
444         vector<shared_ptr<Screen>> screens;
445         optional<dcp::EncryptedKDM> dkdm;
446         optional<boost::posix_time::ptime> valid_from;
447         optional<boost::posix_time::ptime> valid_to;
448         bool zip = false;
449         bool list_cinemas = false;
450         bool list_dkdm_cpls = false;
451         optional<string> duration_string;
452         bool verbose = false;
453         dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
454         bool disable_forensic_marking_picture = false;
455         optional<int> disable_forensic_marking_audio;
456         bool email = false;
457
458         program_name = argv[0];
459
460         /* Reset getopt() so we can call this method several times in one test process */
461         optind = 1;
462
463         int option_index = 0;
464         while (true) {
465                 static struct option long_options[] = {
466                         { "help", no_argument, 0, 'h'},
467                         { "output", required_argument, 0, 'o'},
468                         { "filename-format", required_argument, 0, 'K'},
469                         { "container-name-format", required_argument, 0, 'Z'},
470                         { "valid-from", required_argument, 0, 'f'},
471                         { "valid-to", required_argument, 0, 't'},
472                         { "valid-duration", required_argument, 0, 'd'},
473                         { "formulation", required_argument, 0, 'F' },
474                         { "disable-forensic-marking-picture", no_argument, 0, 'p' },
475                         { "disable-forensic-marking-audio", optional_argument, 0, 'a' },
476                         { "email", no_argument, 0, 'e' },
477                         { "zip", no_argument, 0, 'z' },
478                         { "verbose", no_argument, 0, 'v' },
479                         { "cinema", required_argument, 0, 'c' },
480                         { "screen", required_argument, 0, 'S' },
481                         { "certificate", required_argument, 0, 'C' },
482                         { "trusted-device", required_argument, 0, 'T' },
483                         { "list-cinemas", no_argument, 0, 'B' },
484                         { "list-dkdm-cpls", no_argument, 0, 'D' },
485                         { 0, 0, 0, 0 }
486                 };
487
488                 int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BD", long_options, &option_index);
489
490                 if (c == -1) {
491                         break;
492                 }
493
494                 switch (c) {
495                 case 'h':
496                         help (out);
497                         exit (EXIT_SUCCESS);
498                 case 'o':
499                         output = optarg;
500                         break;
501                 case 'K':
502                         filename_format = dcp::NameFormat (optarg);
503                         break;
504                 case 'Z':
505                         container_name_format = dcp::NameFormat (optarg);
506                         break;
507                 case 'f':
508                         valid_from = time_from_string (optarg);
509                         break;
510                 case 't':
511                         valid_to = time_from_string (optarg);
512                         break;
513                 case 'd':
514                         duration_string = optarg;
515                         break;
516                 case 'F':
517                         if (string(optarg) == "modified-transitional-1") {
518                                 formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
519                         } else if (string(optarg) == "multiple-modified-transitional-1") {
520                                 formulation = dcp::Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
521                         } else if (string(optarg) == "dci-any") {
522                                 formulation = dcp::Formulation::DCI_ANY;
523                         } else if (string(optarg) == "dci-specific") {
524                                 formulation = dcp::Formulation::DCI_SPECIFIC;
525                         } else {
526                                 throw KDMCLIError ("unrecognised KDM formulation " + string (optarg));
527                         }
528                         break;
529                 case 'p':
530                         disable_forensic_marking_picture = true;
531                         break;
532                 case 'a':
533                         disable_forensic_marking_audio = 0;
534                         if (optarg == 0 && argv[optind] != 0 && argv[optind][0] != '-') {
535                                 disable_forensic_marking_audio = atoi (argv[optind++]);
536                         } else if (optarg) {
537                                 disable_forensic_marking_audio = atoi (optarg);
538                         }
539                         break;
540                 case 'e':
541                         email = true;
542                         break;
543                 case 'z':
544                         zip = true;
545                         break;
546                 case 'v':
547                         verbose = true;
548                         break;
549                 case 'c':
550                         /* This could be a cinema to search for in the configured list or the name of a cinema being
551                            built up on-the-fly in the option.  Cater for both possilibities here by storing the name
552                            (for lookup) and by creating a Cinema which the next Screen will be added to.
553                         */
554                         cinema_name = optarg;
555                         cinema = make_shared<Cinema>(optarg, vector<string>(), "", 0, 0);
556                         break;
557                 case 'S':
558                         /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
559                          * to search for.
560                          */
561                         screen = optarg;
562                         break;
563                 case 'C':
564                         certificate = optarg;
565                         break;
566                 case 'T':
567                         /* A trusted device ends up in the last screen we made */
568                         if (!screens.empty ()) {
569                                 screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
570                         }
571                         break;
572                 case 'B':
573                         list_cinemas = true;
574                         break;
575                 case 'D':
576                         list_dkdm_cpls = true;
577                         break;
578                 }
579         }
580
581         if (certificate) {
582                 /* Make a new screen and add it to the current cinema */
583                 dcp::CertificateChain chain(dcp::file_to_string(*certificate));
584                 auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
585                 if (cinema) {
586                         cinema->add_screen(screen_to_add);
587                 }
588                 screens.push_back(screen_to_add);
589         }
590
591         if (list_cinemas) {
592                 auto cinemas = Config::instance()->cinemas ();
593                 for (auto i: cinemas) {
594                         out (String::compose("%1 (%2)", i->name, Emailer::address_list (i->emails)));
595                 }
596                 exit (EXIT_SUCCESS);
597         }
598
599         if (list_dkdm_cpls) {
600                 dump_dkdm_group (Config::instance()->dkdms(), 0, out);
601                 exit (EXIT_SUCCESS);
602         }
603
604         if (!duration_string && !valid_to) {
605                 throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
606         }
607
608         if (!valid_from) {
609                 throw KDMCLIError ("you must specify --valid-from");
610         }
611
612         if (optind >= argc) {
613                 throw KDMCLIError ("no film, CPL ID or DKDM specified");
614         }
615
616         if (screens.empty()) {
617                 if (!cinema_name) {
618                         throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
619                 }
620
621                 screens = find_cinema (*cinema_name)->screens ();
622                 if (screen) {
623                         screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
624                 }
625         }
626
627         if (duration_string) {
628                 valid_to = valid_from.get() + duration_from_string (*duration_string);
629         }
630
631         if (verbose) {
632                 out (String::compose("Making KDMs valid from %1 to %2", boost::posix_time::to_simple_string(valid_from.get()), boost::posix_time::to_simple_string(valid_to.get())));
633         }
634
635         string const thing = argv[optind];
636         if (boost::filesystem::is_directory(thing) && boost::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
637                 from_film (
638                         screens,
639                         thing,
640                         verbose,
641                         output,
642                         container_name_format,
643                         filename_format,
644                         *valid_from,
645                         *valid_to,
646                         formulation,
647                         disable_forensic_marking_picture,
648                         disable_forensic_marking_audio,
649                         email,
650                         zip,
651                         out
652                         );
653         } else {
654                 if (boost::filesystem::is_regular_file(thing)) {
655                         dkdm = dcp::EncryptedKDM (dcp::file_to_string (thing));
656                 } else {
657                         dkdm = find_dkdm (thing);
658                 }
659
660                 if (!dkdm) {
661                         throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
662                 }
663
664                 from_dkdm (
665                         screens,
666                         dcp::DecryptedKDM (*dkdm, Config::instance()->decryption_chain()->key().get()),
667                         verbose,
668                         output,
669                         container_name_format,
670                         filename_format,
671                         *valid_from,
672                         *valid_to,
673                         formulation,
674                         disable_forensic_marking_picture,
675                         disable_forensic_marking_audio,
676                         email,
677                         zip,
678                         out
679                         );
680         }
681
682         return {};
683 } catch (std::exception& e) {
684         return string(e.what());
685 }
686