headless-chicken: misc fixes.
[ardour.git] / session_utils / headless-chicken.cc
1 /*
2     Copyright (C) 2000-2006 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19 #include <iostream>
20 #include <cstdlib>
21 #include <getopt.h>
22
23 #include <glibmm.h>
24
25 #include "pbd/file_utils.h"
26 #include "pbd/i18n.h"
27 #include "pbd/stateful.h"
28
29 #include "ardour/region_factory.h"
30 #include "ardour/midi_model.h"
31 #include "ardour/midi_region.h"
32 #include "ardour/midi_source.h"
33 #include "ardour/playlist.h"
34 #include "ardour/region.h"
35 #include "ardour/session_directory.h"
36 #include "ardour/source.h"
37 #include "ardour/source_factory.h"
38 #include "ardour/tempo.h"
39
40 #include "evoral/Note.hpp"
41 #include "evoral/Sequence.hpp"
42
43 #include "common.h"
44
45 using namespace std;
46 using namespace ARDOUR;
47 using namespace SessionUtils;
48
49 void
50 session_fail (Session* session)
51 {
52         SessionUtils::unload_session(session);
53         SessionUtils::cleanup();
54         exit (EXIT_FAILURE);
55 }
56
57 bool
58 write_bbt_source_to_source (boost::shared_ptr<MidiSource>  bbt_source, boost::shared_ptr<MidiSource> source,
59                                  const Glib::Threads::Mutex::Lock& source_lock, const double session_offset)
60 {
61         const bool old_percussive = bbt_source->model()->percussive();
62
63         bbt_source->model()->set_percussive (false);
64
65         source->mark_streaming_midi_write_started (source_lock, bbt_source->model()->note_mode());
66
67         TempoMap& map (source->session().tempo_map());
68
69         for (Evoral::Sequence<MidiModel::TimeType>::const_iterator i = bbt_source->model()->begin(MidiModel::TimeType(), true); i != bbt_source->model()->end(); ++i) {
70                 const double new_time = map.quarter_note_at_beat ((*i).time().to_double() + map.beat_at_pulse (session_offset)) - (session_offset * 4.0);
71                 Evoral::Event<Evoral::Beats> new_ev (*i, true);
72                 new_ev.set_time (Evoral::Beats (new_time));
73                 source->append_event_beats (source_lock, new_ev);
74         }
75
76         bbt_source->model()->set_percussive (old_percussive);
77         source->mark_streaming_write_completed (source_lock);
78
79         return true;
80 }
81
82 boost::shared_ptr<MidiSource>
83 ensure_per_region_source (Session* session, string newsrc_path, boost::shared_ptr<MidiRegion> region)
84 {
85         boost::shared_ptr<MidiSource> newsrc;
86
87         /* create a new source if none exists and write corrected events to it.
88            if file exists, assume that it is correct.
89         */
90         if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
91                 Source::Flag flags =  Source::Flag (Source::Writable | Source::CanRename);
92                 newsrc = boost::dynamic_pointer_cast<MidiSource>(
93                         SourceFactory::createExternal(DataType::MIDI, *session,
94                                                       newsrc_path, 1, flags));
95                 if (!newsrc) {
96                         cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << endl;
97                         session_fail (session);
98                 }
99
100                 /* hack flags */
101                 XMLNode* node = new XMLNode (newsrc->get_state());
102
103                 if (node->property ("flags") != 0) {
104                         node->property ("flags")->set_value (enum_2_string (flags));
105                 }
106
107                 newsrc->set_state (*node, PBD::Stateful::loading_state_version);
108
109                 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
110                 cout << "for region : " << region->name() << endl;
111
112         } else {
113                 newsrc = boost::dynamic_pointer_cast<MidiSource>(
114                         SourceFactory::createWritable(DataType::MIDI, *session,
115                                                       newsrc_path, false, session->frame_rate()));
116
117                 if (!newsrc) {
118                         cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
119                         session_fail (session);
120                 }
121
122                 Source::Lock newsrc_lock (newsrc->mutex());
123
124                 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
125
126                 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
127                 cout << "for region : " <<  region->name() << endl;
128
129         }
130
131         return newsrc;
132 }
133
134 boost::shared_ptr<MidiSource>
135 ensure_per_source_source (Session* session, string newsrc_path, boost::shared_ptr<MidiRegion> region)
136 {
137         boost::shared_ptr<MidiSource> newsrc;
138
139         /* create a new source if none exists and write corrected events to it. */
140         if (Glib::file_test (newsrc_path, Glib::FILE_TEST_EXISTS)) {
141                 /* flags are ignored for external MIDI source */
142                 Source::Flag flags =  Source::Flag (Source::Writable | Source::CanRename);
143
144                 newsrc = boost::dynamic_pointer_cast<MidiSource>(
145                         SourceFactory::createExternal(DataType::MIDI, *session,
146                                                       newsrc_path, 1, flags));
147
148                 if (!newsrc) {
149                         cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << endl;
150                         session_fail (session);
151                 }
152
153                 cout << UTILNAME << ": Using existing midi source file " << newsrc_path << endl;
154                 cout << "for source : " <<  region->midi_source(0)->name() << endl;
155         } else {
156
157                 newsrc = boost::dynamic_pointer_cast<MidiSource>(
158                         SourceFactory::createWritable(DataType::MIDI, *session,
159                                                       newsrc_path, false, session->frame_rate()));
160                 if (!newsrc) {
161                         cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << endl;
162                         session_fail (session);
163                 }
164
165                 Source::Lock newsrc_lock (newsrc->mutex());
166
167                 write_bbt_source_to_source (region->midi_source(0), newsrc, newsrc_lock, region->pulse() - (region->start_beats().to_double() / 4.0));
168
169                 cout << UTILNAME << ": Created new midi source file " << newsrc_path << endl;
170                 cout << "for source : " <<  region->midi_source(0)->name() << endl;
171
172         }
173
174         return newsrc;
175 }
176
177 void
178 reset_start_and_length (Session* session, boost::shared_ptr<MidiRegion> region)
179 {
180         /* set start_beats & length_beats to quarter note value */
181         TempoMap& map (session->tempo_map());
182
183         region->set_start_beats (Evoral::Beats ((map.pulse_at_beat (region->beat())
184                                                  - map.pulse_at_beat (region->beat() - region->start_beats().to_double())) * 4.0));
185
186         region->set_length_beats (Evoral::Beats ((map.pulse_at_beat (region->beat() + region->length_beats().to_double())
187                                                   - map.pulse_at_beat (region->beat())) * 4.0));
188
189         cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << endl;
190 }
191
192 bool
193 apply_one_source_per_region_fix (Session* session)
194 {
195         const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
196
197         if (!region_map.size()) {
198                 return false;
199         }
200
201         /* for every midi region, ensure a new source and switch to it. */
202         for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
203                 boost::shared_ptr<MidiRegion> mr = 0;
204
205                 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
206                         reset_start_and_length (session, mr);
207                         string newsrc_filename = mr->name() +  "-a54-compat.mid";
208                         string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
209                         boost::shared_ptr<MidiSource> newsrc = ensure_per_region_source (session, newsrc_path, mr);
210                         mr->clobber_sources (newsrc);
211                 }
212         }
213
214         return true;
215 }
216
217 bool
218 apply_one_source_per_source_fix (Session* session)
219 {
220         const RegionFactory::RegionMap& region_map (RegionFactory::all_regions());
221
222         if (!region_map.size()) {
223                 return false;
224         }
225
226         map<PBD::ID, boost::shared_ptr<MidiSource> > old_id_to_new_source;
227         /* for every midi region, ensure a converted source exists. */
228         for (RegionFactory::RegionMap::const_iterator i = region_map.begin(); i != region_map.end(); ++i) {
229                 boost::shared_ptr<MidiRegion> mr = 0;
230                 map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator src_it;
231
232                 if ((mr = boost::dynamic_pointer_cast<MidiRegion>((*i).second)) != 0) {
233                         reset_start_and_length (session, mr);
234
235                         if ((src_it = old_id_to_new_source.find (mr->midi_source()->id())) == old_id_to_new_source.end()) {
236                                 string newsrc_filename = mr->source()->name() +  "-a54-compat.mid";
237                                 string newsrc_path = Glib::build_filename (session->session_directory().midi_path(), newsrc_filename);
238
239                                 boost::shared_ptr<MidiSource> newsrc = ensure_per_source_source (session, newsrc_path, mr);
240
241                                 old_id_to_new_source.insert (make_pair (mr->midi_source()->id(), newsrc));
242
243                                 mr->midi_source(0)->set_name (newsrc->name());
244                         }
245                 }
246         }
247
248         /* remove new sources from the session. current snapshot is saved.*/
249         cout << UTILNAME << ": clearing new sources." << endl;
250
251         for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_id_to_new_source.begin(); i != old_id_to_new_source.end(); ++i) {
252                 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
253         }
254
255         return true;
256 }
257
258 static void usage (int status) {
259         // help2man compatible format (standard GNU help-text)
260         printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
261         printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
262         printf ("Options:\n\
263   -h, --help                    display this help and exit\n\
264   -f, --force                   override detection of affected sessions\n\
265   -o, --output <snapshot-name>  output session snapshot name (without file suffix)\n\
266   -V, --version                 print version information and exit\n\
267 \n");
268         printf ("\n\
269 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
270 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
271 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
272 to be stored incorrectly (see more below).\n\n\
273 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
274 If a session is determined to be affected by the bug, the program will take one of two approaches to correcting the problem.\n\n\
275 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
276 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
277 as a new source (one source file per region).\n\
278 The second method is only offered if the first approach cannot logically ensure that the results would match the input snapshot.\n\
279 Using the first method even if the second method is offered will usually match the input exactly (partly due to a characteristic of the bug).\n\n\
280 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
281 The new snapshot may be used on Ardour-5.4.\n\n\
282 Running this utility should not alter any existing files, but it is recommended that you run it on a backup of the session directory.\n\n\
283 EXAMPLE:\n\
284 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
285 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
286 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
287 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
288 About the Bug\n\
289 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
290 the source smf files would contain events at a PPQN value derived from BBT beats (using meter note divisor) rather than quarter-note beatss.\n\
291 The region start and length offsets would also be stored incorrectly.\n\
292 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
293 \n");
294
295         printf ("Report bugs to <http://tracker.ardour.org/>\n"
296                 "Website: <http://ardour.org/>\n");
297         ::exit (status);
298 }
299
300 int main (int argc, char* argv[])
301 {
302         string outfile;
303         bool force = false;
304
305         const char *optstring = "hfo:r:V";
306
307         const struct option longopts[] = {
308                 { "help",       0, 0, 'h' },
309                 { "force",      0, 0, 'f' },
310                 { "output",     1, 0, 'o' },
311                 { "version",    0, 0, 'V' },
312         };
313
314         int c = 0;
315         while (EOF != (c = getopt_long (argc, argv,
316                                         optstring, longopts, (int *) 0))) {
317                 switch (c) {
318
319                 case 'f':
320                         force = true;
321                         break;
322
323                 case 'o':
324                         outfile = optarg;
325                         if (outfile.empty()) {
326                                 usage (0);
327                         }
328                         break;
329
330                 case 'V':
331                         printf ("ardour-utils version %s\n\n", VERSIONSTRING);
332                         printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
333                         exit (0);
334                         break;
335
336                 case 'h':
337                         usage (0);
338                         break;
339
340                 default:
341                         usage (EXIT_FAILURE);
342                         break;
343                 }
344         }
345
346         if (optind + 2 > argc) {
347                 usage (EXIT_FAILURE);
348         }
349
350         SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
351         string snapshot_name (argv[optind+1]);
352         string statefile_suffix (X_(".ardour"));
353         string pending_suffix (X_(".pending"));
354
355         XMLTree* state_tree;
356
357         string xmlpath(argv[optind]);
358         string out_snapshot_name;
359
360         if (!outfile.empty()) {
361                 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
362                 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
363                         cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
364                         ::exit (EXIT_FAILURE);
365                 }
366                 out_snapshot_name = outfile;
367         } else {
368                 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
369                 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
370                         cout << UTILNAME << ": session file " << file_test_path << " already exists!" << endl;
371                         ::exit (EXIT_FAILURE);
372                 }
373                 out_snapshot_name = snapshot_name + "-a54-compat";
374         }
375
376         xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
377
378         if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
379
380                 /* there is pending state from a crashed capture attempt */
381                 cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << endl;
382
383         }
384
385         xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
386
387         if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
388                 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
389                 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
390                         cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << endl;
391                         ::exit (EXIT_FAILURE);
392                 }
393         }
394
395         state_tree = new XMLTree;
396
397         bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
398
399         if (!writable) {
400                 cout << UTILNAME << ": Error : The session directory must exist and be writable." << endl;
401                 return -1;
402         }
403
404         if (!state_tree->read (xmlpath)) {
405                 cout << UTILNAME << ": Could not understand session file " << xmlpath << endl;
406                 delete state_tree;
407                 state_tree = 0;
408                 ::exit (EXIT_FAILURE);
409         }
410
411         XMLNode const & root (*state_tree->root());
412
413         if (root.name() != X_("Session")) {
414                 cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << endl;
415                 delete state_tree;
416                 state_tree = 0;
417                 ::exit (EXIT_FAILURE);
418         }
419
420         XMLProperty const * prop;
421
422         if ((prop = root.property ("version")) == 0) {
423                 /* no version implies very old version of Ardour */
424                 cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << endl;
425                 ::exit (EXIT_FAILURE);
426         } else {
427                 if (prop->value().find ('.') != string::npos) {
428                         /* old school version format */
429                         cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << endl;
430                         ::exit (EXIT_FAILURE);
431                 } else {
432                         PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
433                 }
434         }
435
436         cout <<  UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << endl;
437
438         bool midi_regions_use_bbt_beats = false;
439
440         if (PBD::Stateful::loading_state_version == 3002 && writable) {
441                 XMLNode* child;
442                 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
443                         if ((prop = child->property ("modified-with")) != 0) {
444                                 string modified_with = prop->value ();
445
446                                 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
447                                 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
448
449                                 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
450                                         midi_regions_use_bbt_beats = true;
451                                 }
452                         }
453                 }
454         }
455
456         XMLNode* tm_node;
457         bool all_metrum_divisors_are_quarters = true;
458         list<double> divisor_list;
459
460         if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
461                 XMLNodeList metrum;
462                 XMLNodeConstIterator niter;
463                 metrum = tm_node->children();
464                 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
465                         XMLNode* child = *niter;
466
467                         if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
468                                 double note_type;
469
470                                 if (sscanf (prop->value().c_str(), "%lf", &note_type) ==1) {
471
472                                         if (note_type != 4.0) {
473                                                 all_metrum_divisors_are_quarters = false;
474                                         }
475                                         divisor_list.push_back (note_type);
476                                 }
477                         }
478                 }
479         } else {
480                 cout << UTILNAME << ": Session file " <<  xmlpath << " has no TempoMap node. exiting." << endl;
481                 ::exit (EXIT_FAILURE);
482         }
483
484         if (all_metrum_divisors_are_quarters && !force) {
485                 cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << endl;
486                 ::exit (EXIT_FAILURE);
487         }
488
489         /* check for multiple note divisors. if there is only one, we can create one file per source. */
490         bool one_source_file_per_source = false;
491         divisor_list.unique();
492
493         if (divisor_list.size() == 1) {
494                 cout  << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << endl;
495                 cout  << "To continue with per-source conversion press enter s. q to quit." << endl;
496
497                 while (1) {
498                         cout  << "[s/q]" << endl;
499
500                         string input;
501                         getline (cin, input);
502
503                         if (input == "s") {
504                                 break;
505                         }
506
507                         if (input == "q") {
508                                 exit (EXIT_SUCCESS);
509                                 break;
510                         }
511                 }
512
513                 one_source_file_per_source = true;
514         } else {
515
516                 cout  << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << endl;
517                 cout  << "per-region source conversion ensures that the output snapshot will be identical to the original," << endl;
518                 cout  << "however regions in the new snapshot will no longer share sources." << endl;
519                 cout  << "In many (but not all) cases per-source conversion will work equally well." << endl;
520                 cout  << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << endl;
521                 cout  << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << endl;
522
523                 while (1) {
524                         cout  << "[r/s/q]" << endl;
525
526                         string input;
527                         getline (cin, input);
528
529                         if (input == "s") {
530                                 one_source_file_per_source = true;
531                                 break;
532                         }
533
534                         if (input == "r") {
535                                 break;
536                         }
537
538                         if (input == "q") {
539                                 exit (EXIT_SUCCESS);
540                                 break;
541                         }
542                 }
543         }
544
545         if (midi_regions_use_bbt_beats || force) {
546
547                 if (force) {
548                         cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << endl;
549                 }
550
551                 SessionUtils::init();
552                 Session* s = 0;
553
554                 cout <<  UTILNAME << ": Loading snapshot." << endl;
555
556                 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
557
558                 /* save new snapshot and prevent alteration of the original by switching to it.
559                    we know these files don't yet exist.
560                 */
561                 if (s->save_state (out_snapshot_name, false, true)) {
562                         cout << UTILNAME << ": Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
563
564                         session_fail (s);
565                 }
566
567                 cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
568
569                 if (one_source_file_per_source) {
570                         cout << UTILNAME << ": Will create one MIDI file per source." << endl;
571
572                         if (!apply_one_source_per_source_fix (s)) {
573                                 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << endl;
574                                 session_fail (s);
575                         }
576                 } else {
577                         cout << UTILNAME << ": Will create one MIDI file per midi region." << endl;
578
579                         if (!apply_one_source_per_region_fix (s)) {
580                                 cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting."  << endl;
581                                 session_fail (s);
582                         }
583
584                         if (s->save_state (out_snapshot_name, false, true)) {
585                                 cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
586                                 session_fail (s);
587                         }
588                         cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << endl;
589                 }
590
591                 SessionUtils::unload_session(s);
592                 SessionUtils::cleanup();
593                 cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << endl;
594         } else {
595                 cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << endl;
596                 ::exit (EXIT_FAILURE);
597         }
598
599         return 0;
600 }