headless-chicken: various 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, std::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                         std::cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << std::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                 std::cout << UTILNAME << ": Using existing midi source file : " << newsrc_path << std::endl;
110                 std::cout << "for region : " << region->name() << std::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                         std::cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << std::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                 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
127                 std::cout << "for region : " <<  region->name() << std::endl;
128
129         }
130
131         return newsrc;
132 }
133
134 boost::shared_ptr<MidiSource>
135 ensure_per_source_source (Session* session, std::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                         std::cout << UTILNAME << "An error occurred creating external source from " << newsrc_path << " exiting." << std::endl;
150                         session_fail (session);
151                 }
152
153                 std::cout << UTILNAME << ": Using existing midi source file " << newsrc_path << std::endl;
154                 std::cout << "for source : " <<  region->midi_source(0)->name() << std::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                         std::cout << UTILNAME << "An error occurred creating writeable source " << newsrc_path << " exiting." << std::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                 std::cout << UTILNAME << ": Created new midi source file " << newsrc_path << std::endl;
170                 std::cout << "for source : " <<  region->midi_source(0)->name() << std::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         std::cout << UTILNAME << ": Reset start and length beats for region : " << region->name() << std::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                                 if (!newsrc) {
242                                         std::cout << UTILNAME << ": an error occurred while creating a new source. exiting" << std::endl;
243                                         session_fail (session);
244                                 }
245
246                                 old_id_to_new_source.insert (make_pair (mr->midi_source()->id(), newsrc));
247
248                                 mr->midi_source(0)->set_name (newsrc->name());
249                         }
250                 }
251         }
252
253         /* remove new sources from the session. current snapshot is saved.*/
254         std::cout << UTILNAME << ": clearing new sources." << std::endl;
255
256         for (map<PBD::ID, boost::shared_ptr<MidiSource> >::iterator i = old_id_to_new_source.begin(); i != old_id_to_new_source.end(); ++i) {
257                 session->remove_source (boost::weak_ptr<MidiSource> ((*i).second));
258         }
259
260         return true;
261 }
262
263 static void usage (int status) {
264         // help2man compatible format (standard GNU help-text)
265         printf (UTILNAME " - convert an ardour session with 5.0 - 5.3 midi sources to be compatible with 5.4.\n\n");
266         printf ("Usage: " UTILNAME " [ OPTIONS ] <session-dir> <snapshot-name>\n\n");
267         printf ("Options:\n\
268   -h, --help                    display this help and exit\n\
269   -f, --force                   override detection of affected sessions\n\
270   -o, --output <snapshot-name>  output session snapshot name (without file suffix)\n\
271   -V, --version                 print version information and exit\n\
272 \n");
273         printf ("\n\
274 This Ardour-specific utility provides an upgrade path for sessions created or modified with Ardour versions 5.0 - 5.3.\n\
275 It creates a 5.4-compatible snapshot from affected Ardour session files.\n\
276 Affected versions (5.0 - 5.3 inclusive) contain a bug which caused some MIDI region properties and contents\n\
277 to be stored incorrectly (see more below).\n\n\
278 The utility will first determine whether or not a session requires any changes for 5.4 compatibility.\n\
279 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\
280 The first is to write a new MIDI source file for every existing MIDI source in the supplied snapshot.\n\
281 In the second approach, each MIDI region have its source converted and placed in the session midifiles directory\n\
282 as a new source (one source file per region).\n\
283 The second method is only used if the first approach cannot guarantee that the results would match the input snapshot.\n\n\
284 Both methods update MIDI region properties and save a new snapshot in the supplied session-dir, optionally using a supplied snapshot name (-o).\n\
285 The new snapshot may be used on Ardour-5.4.\n\n\
286 Running this utility will not alter any existing files, but it is recommended that you run it on a backup of the session directory.\n\n\
287 EXAMPLE:\n\
288 ardour5-headless-chicken -o bantam ~/studio/leghorn leghorn\n\
289 will create a new snapshot file ~/studio/leghorn/bantam.ardour from ~/studio/leghorn/leghorn.ardour\n\
290 Converted midi sources will be created in ~/studio/leghorn/interchange/leghorn/midifiles/\n\
291 If the output option (-o) is omitted, the string \"-a54-compat\" will be appended to the supplied snapshot name.\n\n\
292 About the Bug\n\
293 If a session from affected versions used MIDI regions and a meter note divisor was set to anything but quarter notes,\n\
294 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\
295 The region start and length offsets would also be stored incorrectly.\n\
296 If a MIDI session only contains quarter note meter divisors, it will be unaffected.\n\
297 \n");
298
299         printf ("Report bugs to <http://tracker.ardour.org/>\n"
300                 "Website: <http://ardour.org/>\n");
301         ::exit (status);
302 }
303
304 int main (int argc, char* argv[])
305 {
306         std::string outfile;
307         bool force = false;
308
309         const char *optstring = "hfo:r:V";
310
311         const struct option longopts[] = {
312                 { "help",       0, 0, 'h' },
313                 { "force",      0, 0, 'f' },
314                 { "output",     1, 0, 'o' },
315                 { "version",    0, 0, 'V' },
316         };
317
318         int c = 0;
319         while (EOF != (c = getopt_long (argc, argv,
320                                         optstring, longopts, (int *) 0))) {
321                 switch (c) {
322
323                 case 'f':
324                         force = true;
325                         break;
326
327                 case 'o':
328                         outfile = optarg;
329                         if (outfile.empty()) {
330                                 usage (0);
331                         }
332                         break;
333
334                 case 'V':
335                         printf ("ardour-utils version %s\n\n", VERSIONSTRING);
336                         printf ("Copyright (C) GPL 2015 Robin Gareus <robin@gareus.org>\n");
337                         exit (0);
338                         break;
339
340                 case 'h':
341                         usage (0);
342                         break;
343
344                 default:
345                         usage (EXIT_FAILURE);
346                         break;
347                 }
348         }
349
350         if (optind + 2 > argc) {
351                 usage (EXIT_FAILURE);
352         }
353
354         SessionDirectory* session_dir = new SessionDirectory (argv[optind]);
355         string snapshot_name (argv[optind+1]);
356         string statefile_suffix (X_(".ardour"));
357         string pending_suffix (X_(".pending"));
358
359         XMLTree* state_tree;
360
361         std::string xmlpath(argv[optind]);
362         string out_snapshot_name;
363
364         if (!outfile.empty()) {
365                 string file_test_path = Glib::build_filename (argv[optind], outfile + statefile_suffix);
366                 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
367                         std::cout << UTILNAME << ": session file " << file_test_path << " already exists!" << std::endl;
368                         ::exit (EXIT_FAILURE);
369                 }
370                 out_snapshot_name = outfile;
371         } else {
372                 string file_test_path = Glib::build_filename (argv[optind], snapshot_name + "-a54-compat" + statefile_suffix);
373                 if (Glib::file_test (file_test_path, Glib::FILE_TEST_EXISTS)) {
374                         std::cout << UTILNAME << ": session file " << file_test_path << " already exists!" << std::endl;
375                         ::exit (EXIT_FAILURE);
376                 }
377                 out_snapshot_name = snapshot_name + "-a54-compat";
378         }
379
380         xmlpath = Glib::build_filename (xmlpath, legalize_for_path (snapshot_name) + pending_suffix);
381
382         if (Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
383
384                 /* there is pending state from a crashed capture attempt */
385                 std::cout << UTILNAME << ": There seems to be pending state for snapshot : " << snapshot_name << std::endl;
386
387         }
388
389         xmlpath = Glib::build_filename (argv[optind], argv[optind+1]);
390
391         if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
392                 xmlpath = Glib::build_filename (argv[optind], legalize_for_path (argv[optind+1]) + ".ardour");
393                 if (!Glib::file_test (xmlpath, Glib::FILE_TEST_EXISTS)) {
394                         std::cout << UTILNAME << ": session file " << xmlpath << " doesn't exist!" << std::endl;
395                         ::exit (EXIT_FAILURE);
396                 }
397         }
398
399         state_tree = new XMLTree;
400
401         bool writable = PBD::exists_and_writable (xmlpath) && PBD::exists_and_writable(Glib::path_get_dirname(xmlpath));
402
403         if (!writable) {
404                 std::cout << UTILNAME << ": Error : The session directory must exist and be writable." << std::endl;
405                 return -1;
406         }
407
408         if (!state_tree->read (xmlpath)) {
409                 std::cout << UTILNAME << ": Could not understand session file " << xmlpath << std::endl;
410                 delete state_tree;
411                 state_tree = 0;
412                 ::exit (EXIT_FAILURE);
413         }
414
415         XMLNode const & root (*state_tree->root());
416
417         if (root.name() != X_("Session")) {
418                 std::cout << UTILNAME << ": Session file " << xmlpath<< " is not a session" << std::endl;
419                 delete state_tree;
420                 state_tree = 0;
421                 ::exit (EXIT_FAILURE);
422         }
423
424         XMLProperty const * prop;
425
426         if ((prop = root.property ("version")) == 0) {
427                 /* no version implies very old version of Ardour */
428                 std::cout << UTILNAME << ": The session " << snapshot_name << " has no version or is too old to be affected. exiting." << std::endl;
429                 ::exit (EXIT_FAILURE);
430         } else {
431                 if (prop->value().find ('.') != string::npos) {
432                         /* old school version format */
433                         std::cout << UTILNAME << ": The session " << snapshot_name << " is too old to be affected. exiting." << std::endl;
434                         ::exit (EXIT_FAILURE);
435                 } else {
436                         PBD::Stateful::loading_state_version = atoi (prop->value().c_str());
437                 }
438         }
439
440         std::cout <<  UTILNAME << ": Checking snapshot : " << snapshot_name << " in directory : " << session_dir->root_path() << std::endl;
441
442         bool midi_regions_use_bbt_beats = false;
443
444         if (PBD::Stateful::loading_state_version == 3002 && writable) {
445                 XMLNode* child;
446                 if ((child = find_named_node (root, "ProgramVersion")) != 0) {
447                         if ((prop = child->property ("modified-with")) != 0) {
448                                 std::string modified_with = prop->value ();
449
450                                 const double modified_with_version = atof (modified_with.substr ( modified_with.find(" ", 0) + 1, string::npos).c_str());
451                                 const int modified_with_revision = atoi (modified_with.substr (modified_with.find("-", 0) + 1, string::npos).c_str());
452
453                                 if (modified_with_version <= 5.3 && !(modified_with_version == 5.3 && modified_with_revision >= 42)) {
454                                         midi_regions_use_bbt_beats = true;
455                                 }
456                         }
457                 }
458         }
459
460         XMLNode* tm_node;
461         bool all_metrum_divisors_are_quarters = true;
462         list<double> divisor_list;
463
464         if ((tm_node = find_named_node (root, "TempoMap")) != 0) {
465                 XMLNodeList metrum;
466                 XMLNodeConstIterator niter;
467                 metrum = tm_node->children();
468                 for (niter = metrum.begin(); niter != metrum.end(); ++niter) {
469                         XMLNode* child = *niter;
470
471                         if (child->name() == MeterSection::xml_state_node_name && (prop = child->property ("note-type")) != 0) {
472                                 double note_type;
473
474                                 if (sscanf (prop->value().c_str(), "%lf", &note_type) ==1) {
475
476                                         if (note_type != 4.0) {
477                                                 all_metrum_divisors_are_quarters = false;
478                                         }
479                                         divisor_list.push_back (note_type);
480                                 }
481                         }
482                 }
483         } else {
484                 std::cout << UTILNAME << ": Session file " <<  xmlpath << " has no TempoMap node. exiting." << std::endl;
485                 ::exit (EXIT_FAILURE);
486         }
487
488         if (all_metrum_divisors_are_quarters && !force) {
489                 std::cout << UTILNAME << ": The session " << snapshot_name << " is clear for use in 5.4 (all divisors are quarters). Use -f to override." << std::endl;
490                 ::exit (EXIT_FAILURE);
491         }
492
493         /* check for multiple note divisors. if there is only one, we can create one file per source. */
494         bool one_source_file_per_source = false;
495         divisor_list.unique();
496
497         if (divisor_list.size() == 1) {
498                 std::cout  << UTILNAME << ": Snapshot " << snapshot_name << " will be converted using one new file per source." << std::endl;
499                 std::cout  << "To continue with per-source conversion press enter s. q to quit." << std::endl;
500
501                 while (1) {
502                         std::cout  << "[s/q]" << std::endl;
503
504                         string input;
505                         getline (std::cin, input);
506
507                         if (input == "s") {
508                                 break;
509                         }
510
511                         if (input == "q") {
512                                 exit (EXIT_SUCCESS);
513                                 break;
514                         }
515                 }
516
517                 one_source_file_per_source = true;
518         } else {
519
520                 std::cout  << UTILNAME << ": Snapshot " << snapshot_name << " contains multiple meter note divisors." << std::endl;
521                 std::cout  << "per-region source conversion guarantees that the output snapshot will be identical to the original," << std::endl;
522                 std::cout  << "however regions in the new snapshot will no longer share sources." << std::endl;
523                 std::cout  << "In many (but not all) cases per-source conversion will work equally well." << std::endl;
524                 std::cout  << "It is recommended that you test a snapshot created with the per-source method before using per-region conversion." << std::endl;
525                 std::cout  << "To continue with per-region conversion enter r. For per-source conversion, enter s. q to quit." << std::endl;
526
527                 while (1) {
528                         std::cout  << "[r/s/q]" << std::endl;
529
530                         string input;
531                         getline (std::cin, input);
532
533                         if (input == "s") {
534                                 one_source_file_per_source = true;
535                                 break;
536                         }
537
538                         if (input == "r") {
539                                 break;
540                         }
541
542                         if (input == "q") {
543                                 exit (EXIT_SUCCESS);
544                                 break;
545                         }
546                 }
547         }
548
549         if (midi_regions_use_bbt_beats || force) {
550
551                 if (force) {
552                         std::cout << UTILNAME << ": Forced update of snapshot : " << snapshot_name << std::endl;
553                 }
554
555                 SessionUtils::init();
556                 Session* s = 0;
557
558                 std::cout <<  UTILNAME << ": Loading snapshot." << std::endl;
559
560                 s = SessionUtils::load_session (argv[optind], argv[optind+1]);
561
562                 /* save new snapshot and prevent alteration of the original by switching to it.
563                    we know these files don't yet exist.
564                 */
565                 if (s->save_state (out_snapshot_name, false, true)) {
566                         std::cout << UTILNAME << ": Could not save new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
567
568                         session_fail (s);
569                 }
570
571                 std::cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
572
573                 if (one_source_file_per_source) {
574                         std::cout << UTILNAME << ": Will create one MIDI file per source." << std::endl;
575
576                         if (!apply_one_source_per_source_fix (s)) {
577                                 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting." << std::endl;
578                                 session_fail (s);
579                         }
580                 } else {
581                         std::cout << UTILNAME << ": Will create one MIDI file per midi region." << std::endl;
582
583                         if (!apply_one_source_per_region_fix (s)) {
584                                 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " is clear for use in 5.4 (no midi regions). exiting."  << std::endl;
585                                 session_fail (s);
586                         }
587
588                         if (s->save_state (out_snapshot_name, false, true)) {
589                                 std::cout << UTILNAME << ": Could not save snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
590                                 session_fail (s);
591                         }
592                         std::cout << UTILNAME << ": Saved new snapshot: " << out_snapshot_name << " in " << session_dir->root_path() << std::endl;
593                 }
594
595                 SessionUtils::unload_session(s);
596                 SessionUtils::cleanup();
597                 std::cout << UTILNAME << ": Snapshot " << out_snapshot_name << " is ready for use in 5.4" << std::endl;
598         } else {
599                 std::cout << UTILNAME << ": The snapshot " << snapshot_name << " doesn't require any change for use in 5.4. Use -f to override." << std::endl;
600                 ::exit (EXIT_FAILURE);
601         }
602
603         return 0;
604 }