2 * Copyright (C) 2016-2019 Robin Gareus <robin@gareus.org>
3 * Copyright (C) 2016 Nick Mainsbridge <mainsbridge@gmail.com>
4 * Copyright (C) 2017 Paul Davis <paul@linuxaudiosystems.com>
6 * This program 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.
11 * This program 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.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27 #include "pbd/file_utils.h"
29 #include "pbd/stateful.h"
31 #include "ardour/region_factory.h"
32 #include "ardour/midi_model.h"
33 #include "ardour/midi_region.h"
34 #include "ardour/midi_source.h"
35 #include "ardour/playlist.h"
36 #include "ardour/region.h"
37 #include "ardour/session_directory.h"
38 #include "ardour/source.h"
39 #include "ardour/source_factory.h"
40 #include "ardour/tempo.h"
42 #include "evoral/Note.h"
43 #include "evoral/Sequence.h"
48 using namespace ARDOUR;
49 using namespace SessionUtils;
52 session_fail (Session* session)
54 SessionUtils::unload_session(session);
55 SessionUtils::cleanup();
60 write_bbt_source_to_source (boost::shared_ptr<MidiSource> bbt_source, boost::shared_ptr<MidiSource> source,
61 const Glib::Threads::Mutex::Lock& source_lock, const double session_offset)
63 assert (source->empty());
64 const bool old_percussive = bbt_source->model()->percussive();
66 bbt_source->model()->set_percussive (false);
68 source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
70 TempoMap& map (source->session().tempo_map());
72 for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
73 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_quarter_note (session_offset * 4.0)) - (session_offset * 4.0);
74 Evoral::Event<Temporal::Beats> new_ev (*i, true);
75 new_ev.set_time (Temporal::Beats (new_time));
76 source->append_event_beats (source_lock, new_ev);
79 bbt_source->model()->set_percussive (old_percussive);
80 source->mark_streaming_write_completed (source_lock);
81 source->set_natural_position (bbt_source->natural_position());
86 boost::shared_ptr<MidiSource>
87 ensure_per_region_source (Session* session, boost::shared_ptr<MidiRegion> region, string newsrc_path)
89 boost::shared_ptr<MidiSource> newsrc;
91 /* create a new source if none exists and write corrected events to it.
92 if file exists, assume that it is correct.
94 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
95 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
96 newsrc = boost::dynamic_pointer_cast<MidiSource>(
97 SourceFactory::createExternal(DataType::MIDI, *session,
98 newsrc_path, 1, flags));
100 cout << UTILNAME << ":" << endl
101 << " An error occurred creating external source from " << newsrc_path << " exiting." << endl;
102 session_fail (session);
106 XMLNode* node = new XMLNode (newsrc->get_state());
108 if (node->property ("flags") != 0) {
109 node->property ("flags")->set_value (enum_2_string (flags));
112 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
116 cout << UTILNAME << ":" << endl
117 << " Using existing midi source file" << endl
118 << " " << newsrc_path << endl
119 << " for region " << region->name() << endl;
122 newsrc = boost::dynamic_pointer_cast<MidiSource>(
123 SourceFactory::createWritable(DataType::MIDI, *session,
124 newsrc_path, false, session->sample_rate()));
127 cout << UTILNAME << ":" << endl
128 << " An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
129 session_fail (session);
132 if (!newsrc->empty()) {
133 cout << UTILNAME << ":" << endl
134 << " An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
135 session_fail (session);
138 Source::Lock newsrc_lock (newsrc->mutex());
140 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, (region->quarter_note() - region->start_beats()) / 4.0);
142 cout << UTILNAME << ":" << endl
143 << " Created new midi source file" << endl
144 << " " << newsrc_path << endl
145 << " for region " << region->name() << endl;
151 boost::shared_ptr<MidiSource>
152 ensure_per_source_source (Session* session, boost::shared_ptr<MidiRegion> region, string newsrc_path)
154 boost::shared_ptr<MidiSource> newsrc;
156 /* create a new source if none exists and write corrected events to it. */
157 if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
158 /* flags are ignored for external MIDI source */
159 Source::Flag flags = Source::Flag (Source::Writable | Source::CanRename);
161 newsrc = boost::dynamic_pointer_cast<MidiSource>(
162 SourceFactory::createExternal(DataType::MIDI, *session,
163 newsrc_path, 1, flags));
166 cout << UTILNAME << ":" << endl
167 << " An error occurred creating external source from " << newsrc_path << " exiting." << endl;
168 session_fail (session);
171 cout << UTILNAME << ":" << endl
172 << " Using existing midi source file" << endl
173 << " " << newsrc_path << endl
174 << " for source " << region->midi_source(0)->name() << endl;
177 newsrc = boost::dynamic_pointer_cast<MidiSource>(
178 SourceFactory::createWritable(DataType::MIDI, *session,
179 newsrc_path, false, session->sample_rate()));
181 cout << UTILNAME << ":" << endl
182 <<" An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
183 session_fail (session);
186 if (!newsrc->empty()) {
187 cout << UTILNAME << ":" << endl
188 << " An error occurred/ " << newsrc->name() << " is not empty. exiting." << endl;
189 session_fail (session);
192 Source::Lock newsrc_lock (newsrc->mutex());
194 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, (region->quarter_note() - region->start_beats()) / 4.0);
196 cout << UTILNAME << ":" << endl
197 << " Created new midi source file" << endl
198 << " " << newsrc_path << endl
199 << " for source " << region->midi_source(0)->name() << endl;
207 reset_start (Session* session, boost::shared_ptr<MidiRegion> region)
209 /* set start_beats to quarter note value from incorrect bbt*/
210 TempoMap& tmap (session->tempo_map());
211 double new_start_qn = tmap.quarter_note_at_beat (region->beat()) - tmap.quarter_note_at_beat (region->beat() - region->start_beats());
213 /* force a change to start and start_beats */
214 PositionLockStyle old_pls = region->position_lock_style();
215 region->set_position_lock_style (AudioTime);
216 region->set_start (tmap.sample_at_quarter_note (region->quarter_note()) - tmap.sample_at_quarter_note (region->quarter_note() - new_start_qn) + 1);
217 region->set_start (tmap.sample_at_quarter_note (region->quarter_note()) - tmap.sample_at_quarter_note (region->quarter_note() - new_start_qn));
218 region->set_position_lock_style (old_pls);
223 reset_length (Session* session, boost::shared_ptr<MidiRegion> region)
225 /* set length_beats to quarter note value */
226 TempoMap& tmap (session->tempo_map());
227 double new_length_qn = tmap.quarter_note_at_beat (region->beat() + region->length_beats())
228 - tmap.quarter_note_at_beat (region->beat());
230 /* force a change to length and length_beats */
231 PositionLockStyle old_pls = region->position_lock_style();
232 region->set_position_lock_style (AudioTime);
233 region->set_length (tmap.sample_at_quarter_note (region->quarter_note() + new_length_qn) + 1 - region->position(), 0);
234 region->set_length (tmap.sample_at_quarter_note (region->quarter_note() + new_length_qn)- region->position(), 0);
235 region->set_position_lock_style (old_pls);
239 apply_one_source_per_region_fix (Session* session)
241 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
243 if (!region_map.size()) {
247 list<boost::shared_ptr<MidiSource> > old_source_list;
249 /* set start and length for every midi region. ensure a new converted source exists and switch to it. */
250 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
251 boost::shared_ptr<MidiRegion> mr;
253 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
255 if (!mr->midi_source()->writable()) {
256 /* we know the midi dir is writable, so this region is external. leave it alone*/
257 cout << mr->source()->name() << "is not writable. skipping." << endl;
261 old_source_list.push_back (mr->midi_source());
263 reset_start (session, mr);
264 reset_length (session, mr);
266 string newsrc_filename = mr->name() + "-a54-compat.mid";
267 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
269 boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, mr, newsrc_path);
271 mr->clobber_sources (newsrc);
275 old_source_list.unique();
277 /* remove old sources from the session. current snapshot is saved.*/
278 cout << UTILNAME << ":" << endl
279 << " clearing old sources." << endl;
281 for (list<boost::shared_ptr<MidiSource> >::iterator i = old_source_list.begin(); i != old_source_list.end(); ++i) {
282 session->remove_source (boost::weak_ptr<MidiSource> (*i));
289 apply_one_source_per_source_fix (Session* session)
291 const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
293 if (!region_map.size()) {
297 map<PBD::ID, boost::shared_ptr<MidiSource> > old_source_to_new;
298 /* reset every midi region's start and length. ensure its corrected source exists. */
299 for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
300 boost::shared_ptr<MidiRegion> mr;
301 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
303 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
305 if (!mr->midi_source()->writable()) {
306 cout << mr->source()->name() << "is not writable. skipping." << endl;
310 reset_start (session, mr);
311 reset_length (session, mr);
313 if ((src_it = old_source_to_new.find (mr->midi_source()->id())) == old_source_to_new.end()) {
314 string newsrc_filename = mr->source()->name() + "-a54-compat.mid";
315 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
317 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, mr, newsrc_path);
319 old_source_to_new.insert (make_pair (mr->midi_source()->id(), newsrc));
321 mr->midi_source(0)->set_name (newsrc->name());
326 /* remove new sources from the session. current snapshot is saved.*/
327 cout << UTILNAME << ":" << endl
328 << " clearing new sources." << endl;
330 for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_source_to_new.begin(); i != old_source_to_new.end(); ++i) {
331 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
337 static void usage () {
338 // help2man compatible format (standard GNU help-text)
339 printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
340 printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
342 -h, --help display this help and exit\n\
343 -f, --force override detection of affected sessions\n\
344 -o, --output <snapshot-name> output session snapshot name (without file suffix)\n\
345 -V, --version print version information and exit\n\
348 This Ardour-specific utility provides an upgrade path for sessions created or\n\
349 modified with Ardour versions 5.0 - 5.3.\n\
350 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
351 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some\n\
352 MIDI region properties and contents to be stored incorrectly\n\
353 (see more below).\n\n\
354 The utility will first determine whether or not a session requires any\n\
355 changes for 5.4 compatibility.\n\
356 If a session is determined to be affected by the bug, the program will take\n\
357 one of two approaches to correcting the problem.\n\n\
358 The first is to write a new MIDI source file for every existing MIDI source\n\
359 in the supplied snapshot.\n\
360 In the second approach, each MIDI region have its source converted and placed\n\
361 in the session midifiles directory as a new source\n\
362 (one source file per region).\n\
363 The second method is only offered if the first approach cannot logically ensure\n\
364 that the results would match the input snapshot.\n\
365 Using the first method even if the second method is offered\n\
366 will usually match the input exactly\n\
367 (partly due to a characteristic of the bug).\n\n\
368 Both methods update MIDI region properties and save a new snapshot in the\n\
369 supplied session-dir, optionally using a supplied snapshot name (-o).\n\
370 The new snapshot may be used on Ardour-5.4.\n\n\
371 Running this utility should not alter any existing files,\n\
372 but it is recommended that you run it on a backup of the session directory.\n\n\
374 ardour5-fix_bbtppq -o bantam ~/studio/leghorn leghorn\n\
375 will create a new snapshot file ~/studio/leghorn/bantam.ardour from\n\
376 ~/studio/leghorn/leghorn.ardour\n\
377 Converted midi sources will be created in\n\
378 ~/studio/leghorn/interchange/leghorn/midifiles/\n\
379 If the output option (-o) is omitted, the string \"-a54-compat\"\n\
380 will be appended to the supplied snapshot name.\n\n\
382 If a session from affected versions used MIDI regions and a meter note divisor\n\
383 was set to anything but quarter notes, the source smf files would contain events\n\
384 at a PPQN value derived from BBT beats (using meter note divisor)\n\
385 rather than quarter-note beats.\n\
386 The region start and length offsets would also be stored incorrectly.\n\
387 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
390 printf ("Report bugs to <http://tracker.ardour.org/>\n"
391 "Website: <http://ardour.org/>\n");
392 ::exit (EXIT_SUCCESS);
395 int main (int argc, char* argv[])
400 const char *optstring = "hfo:r:V";
402 const struct option longopts[] = {
403 { "help", 0, 0, 'h' },
404 { "force", 0, 0, 'f' },
405 { "output", 1, 0, 'o' },
406 { "version", 0, 0, 'V' },
410 while (EOF != (c = getopt_long (argc, argv,
411 optstring, longopts, (int *) 0))) {
423 printf ("ardour-utils version %s\n\n", VERSIONSTRING);
424 printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
433 cerr << "Error: unrecognized option. See --help for usage information.\n";
434 ::exit (EXIT_FAILURE);
439 if (optind + 2 > argc) {
440 cerr << "Error: Missing parameter. See --help for usage information.\n";
441 ::exit (EXIT_FAILURE);
444 SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
445 string snapshot_name (argv[optind+1]);
446 string statefile_suffix (X_(".ardour"));
447 string pending_suffix (X_(".pending"));
451 string xmlpath(argv[optind]);
452 string out_snapshot_name;
454 if (!outfile.empty()) {
455 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
456 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
457 cout << UTILNAME << ":" << endl
458 << " session file " << file_test_path << " already exists!" << endl;
459 ::exit (EXIT_FAILURE);
461 out_snapshot_name = outfile;
463 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
464 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
465 cout << UTILNAME << ":" << endl
466 << " session file " << file_test_path << " already exists!" << endl;
467 ::exit (EXIT_FAILURE);
469 out_snapshot_name = snapshot_name + "-a54-compat";
472 xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
474 if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
476 /* there is pending state from a crashed capture attempt */
477 cout << UTILNAME << ":" << endl
478 << " There seems to be pending state for snapshot : " << snapshot_name << endl;
482 xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
484 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
485 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
486 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
487 cout << UTILNAME << ":" << endl
488 << " session file " << xmlpath << " doesn't exist!" << endl;
489 ::exit (EXIT_FAILURE);
493 state_tree = new XMLTree;
495 bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
498 cout << UTILNAME << ":" << endl
499 << " Error : The session directory must exist and be writable." << endl;
503 if (!PBD::exists_and_writable (Glib::path_get_dirname (session_dir->midi_path()))) {
504 cout << UTILNAME << ":" << endl
505 << " Error : The session midi directory " << session_dir->midi_path() << " must be writable. exiting." << endl;
506 ::exit (EXIT_FAILURE);
509 if (!state_tree->read (xmlpath)) {
510 cout << UTILNAME << ":" << endl
511 << " Could not understand session file " << xmlpath << endl;
514 ::exit (EXIT_FAILURE);
517 XMLNode const & root (*state_tree->root());
519 if (root.name() != X_("Session")) {
520 cout << UTILNAME << ":" << endl
521 << " Session file " << xmlpath<< " is not a session" << endl;
524 ::exit (EXIT_FAILURE);
527 XMLProperty const * prop;
529 if ((prop = root.property ("version")) == 0) {
530 /* no version implies very old version of Ardour */
531 cout << UTILNAME << ":" << endl
532 << " The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
533 ::exit (EXIT_FAILURE);
535 if (prop->value().find ('.') != string::npos) {
536 /* old school version format */
537 cout << UTILNAME << ":" << endl
538 << " The session " << snapshot_name << " is too old to be affected. exiting." << endl;
539 ::exit (EXIT_FAILURE);
541 PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
545 cout << UTILNAME << ":" << endl
546 << " Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
548 bool midi_regions_use_bbt_beats = false;
550 if (PBD::Stateful::loading_state_version == 3002 && writable) {
552 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
553 if ((prop = child->property ("modified-with")) != 0) {
554 string modified_with = prop->value ();
556 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
557 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
559 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
560 midi_regions_use_bbt_beats = true;
567 bool all_metrum_divisors_are_quarters = true;
568 list<double> divisor_list;
570 if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
572 XMLNodeConstIterator niter;
573 metrum = tm_node->children();
574 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
575 XMLNode* child = *niter;
577 if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
580 if (sscanf (prop->value().c_str(), "%lf", ¬e_type) ==1) {
582 if (note_type != 4.0) {
583 all_metrum_divisors_are_quarters = false;
586 divisor_list.push_back (note_type);
591 cout << UTILNAME << ":" << endl
592 << " Session file " << xmlpath << " has no TempoMap node. exiting." << endl;
593 ::exit (EXIT_FAILURE);
596 if (all_metrum_divisors_are_quarters && !force) {
597 cout << UTILNAME << ":" << endl
598 << " The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
599 ::exit (EXIT_FAILURE);
602 /* check for multiple note divisors. if there is only one, we can create one file per source. */
603 bool one_source_file_per_source = false;
604 divisor_list.unique();
606 if (divisor_list.size() == 1) {
607 cout << endl << UTILNAME << ":" << endl
608 << " Snapshot " << snapshot_name << " will be converted using one new file per source." << endl
609 << " To continue with per-source conversion enter s. q to quit." << endl;
612 cout << " [s/q]" << endl;
615 getline (cin, input);
627 one_source_file_per_source = true;
630 cout << endl << UTILNAME << ":" << endl
631 << " Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl
632 << " Per-region source conversion ensures that the output snapshot will be identical to the original," << endl
633 << " however regions in the new snapshot will no longer share sources." << endl << endl
634 << " In many (but not all) cases per-source conversion will work equally well." << endl
635 << " It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl << endl
636 << " To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
639 cout << " [r/s/q]" << endl;
642 getline (cin, input);
645 one_source_file_per_source = true;
660 if (midi_regions_use_bbt_beats || force) {
663 cout << UTILNAME << ":" << endl
664 << " Forced update of snapshot : " << snapshot_name << endl;
667 SessionUtils::init();
670 cout << UTILNAME << ":" << endl
671 << " Loading snapshot " << snapshot_name << endl;
673 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
675 /* save new snapshot and prevent alteration of the original by switching to it.
676 we know these files don't yet exist.
678 if (s->save_state (out_snapshot_name, false, true)) {
679 cout << UTILNAME << ":" << endl
680 << " Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
685 cout << UTILNAME << ":" << endl
686 << " Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
688 if (one_source_file_per_source) {
689 cout << UTILNAME << ":" << endl
690 << " Will create one MIDI file per source." << endl;
692 if (!apply_one_source_per_source_fix (s)) {
693 cout << UTILNAME << ":" << endl
694 << " The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
698 cout << UTILNAME << ":" << endl
699 << " Will create one MIDI file per midi region." << endl;
701 if (!apply_one_source_per_region_fix (s)) {
702 cout << UTILNAME << ":" << endl
703 << " The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
707 if (s->save_state (out_snapshot_name, false, true)) {
708 cout << UTILNAME << ":" << endl
709 << " Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
712 cout << UTILNAME << ":" << endl
713 << " Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
716 SessionUtils::unload_session(s);
717 SessionUtils::cleanup();
718 cout << UTILNAME << ":" << endl
719 << " Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
721 cout << UTILNAME << ":" << endl
722 << " The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
723 ::exit (EXIT_FAILURE);