desktop: thread IDs are initialized by pthread_create
[barry.git] / desktop / src / MigrateDlg.cc
blob659335a1063f7b90b245b55d6e00cec8636953ce
1 ///
2 /// \file MigrateDlg.cc
3 /// Dialog for the "Migrate Device" main menu mode button...
4 /// going with a dialog instead of a mode class this time.
5 ///
7 /*
8 Copyright (C) 2012, Net Direct Inc. (http://www.netdirect.ca/)
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19 See the GNU General Public License in the COPYING file at the
20 root directory of this project for more details.
23 #include "MigrateDlg.h"
24 #include "windowids.h"
25 #include "configui.h"
26 #include "barrydesktop.h"
27 #include <string>
28 #include <wx/statline.h>
29 #include <barry/barrybackup.h>
30 #include <iostream>
32 using namespace std;
33 using namespace OpenSync;
35 DEFINE_EVENT_TYPE(MET_THREAD_FINISHED)
36 DEFINE_EVENT_TYPE(MET_CHECK_DEST_PIN)
37 DEFINE_EVENT_TYPE(MET_SET_STATUS_MSG)
38 DEFINE_EVENT_TYPE(MET_PROMPT_PASSWORD)
39 DEFINE_EVENT_TYPE(MET_ERROR_MSG)
41 BEGIN_EVENT_TABLE(MigrateDlg, wxDialog)
42 EVT_BUTTON (Dialog_Migrate_MigrateNowButton,
43 MigrateDlg::OnMigrateNow)
44 EVT_BUTTON (Dialog_Migrate_CancelButton,
45 MigrateDlg::OnCancel)
46 EVT_CLOSE (MigrateDlg::OnCloseWindow)
47 EVT_COMMAND (wxID_ANY, MET_THREAD_FINISHED,
48 MigrateDlg::OnThreadFinished)
49 EVT_COMMAND (wxID_ANY, MET_CHECK_DEST_PIN,
50 MigrateDlg::OnCheckDestPin)
51 EVT_COMMAND (wxID_ANY, MET_SET_STATUS_MSG,
52 MigrateDlg::OnSetStatusMsg)
53 EVT_COMMAND (wxID_ANY, MET_PROMPT_PASSWORD,
54 MigrateDlg::OnPromptPassword)
55 EVT_COMMAND (wxID_ANY, MET_ERROR_MSG,
56 MigrateDlg::OnErrorMsg)
57 END_EVENT_TABLE()
60 class EventDesktopConnector : public Barry::DesktopConnector
62 MigrateDlg *m_dlg;
64 public:
65 EventDesktopConnector(MigrateDlg *dlg, const char *password,
66 const std::string &locale, const Barry::ProbeResult &result)
67 : Barry::DesktopConnector(password, locale, result)
68 , m_dlg(dlg)
72 virtual bool PasswordPrompt(const Barry::BadPassword &bp,
73 std::string &password_result);
76 bool EventDesktopConnector::PasswordPrompt(const Barry::BadPassword &bp,
77 std::string &password_result)
79 // ping the parent and wait for finish
80 wxCommandEvent event(MET_PROMPT_PASSWORD, wxID_ANY);
81 event.SetEventObject(m_dlg);
82 event.SetInt(bp.remaining_tries());
83 m_dlg->AddPendingEvent(event);
84 m_dlg->WaitForEvent();
86 password_result = m_dlg->GetPassword().utf8_str();
88 // assume that a blank password means the user wishes to quit...
89 // wxWidgets doesn't seem to handle this very well?
90 return password_result.size() > 0;
94 //////////////////////////////////////////////////////////////////////////////
95 // MigrateDlg class
97 MigrateDlg::MigrateDlg(wxWindow *parent,
98 const Barry::Probe::Results &results,
99 int current_device_index)
100 : wxDialog(parent, Dialog_GroupCfg, _T("Migrate Device"))
101 , m_results(results)
102 , m_current_device_index(current_device_index)
103 , m_migrate_thread_created(false)
104 , m_abort_flag(false)
105 , m_thread_running(false)
106 , m_source_device(0)
107 , m_dest_device(0)
108 , m_topsizer(0)
109 , m_source_combo(0)
110 , m_dest_combo(0)
111 , m_write_mode_combo(0)
112 , m_migrate_button(0)
113 , m_wipe_check(0)
114 , m_status(0)
115 , m_progress(0)
117 // setup the raw GUI
118 CreateLayout();
121 void MigrateDlg::WaitForEvent()
123 m_waiter.Wait();
126 void MigrateDlg::CreateLayout()
128 m_topsizer = new wxBoxSizer(wxVERTICAL);
130 // AddDescriptionSizer(m_topsizer);
131 AddMainSizer(m_topsizer);
132 AddStatusSizer(m_topsizer);
134 SetSizer(m_topsizer);
135 m_topsizer->SetSizeHints(this);
136 m_topsizer->Layout();
139 void MigrateDlg::AddDescriptionSizer(wxSizer *sizer)
141 sizer->Add(
142 new wxStaticText(this, wxID_ANY,
143 _T("Migrate device data from source device to target device.")),
144 0, wxALIGN_CENTRE | wxALIGN_CENTRE_VERTICAL |
145 wxTOP | wxLEFT | wxRIGHT, 10);
148 void MigrateDlg::AddMainSizer(wxSizer *sizer)
150 // add 3 main sections together into one sizer
151 wxSizer *main = new wxBoxSizer(wxHORIZONTAL);
152 Main_AddSourceSizer(main);
153 Main_AddButtonSizer(main);
154 Main_AddDestSizer(main);
156 // add main sizer to top level sizer
157 sizer->Add(main, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
160 void MigrateDlg::AddStatusSizer(wxSizer *sizer)
162 sizer->Add( new wxStaticLine(this),
163 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, 10);
165 sizer->Add(
166 m_status = new wxStaticText(this, wxID_ANY, _T("Ready...")),
167 0, wxEXPAND | wxLEFT | wxRIGHT, 10);
168 // Reduce font size for status text
169 wxFont font = m_status->GetFont();
170 font.SetPointSize(font.GetPointSize() - 1);
171 m_status->SetFont( font );
173 sizer->Add(
174 m_progress = new wxGauge(this, wxID_ANY, 100),
175 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10);
178 void MigrateDlg::Main_AddSourceSizer(wxSizer *sizer)
180 wxArrayString devices;
181 for( Barry::Probe::Results::const_iterator i = m_results.begin();
182 i != m_results.end(); ++i )
184 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
187 wxSizer *source = new wxStaticBoxSizer(
188 new wxStaticBox(this, wxID_ANY, _T("Source device")),
189 wxVERTICAL
191 source->Add(
192 m_source_combo = new wxChoice(this, wxID_ANY,
193 wxDefaultPosition, wxSize(225, -1), devices),
194 0, wxALL, 5);
196 if( m_current_device_index >= 0 )
197 m_source_combo->SetSelection(m_current_device_index);
199 sizer->Add(source, 0, wxEXPAND, 0);
202 void MigrateDlg::Main_AddButtonSizer(wxSizer *sizer)
204 wxSizer *buttons = new wxBoxSizer(wxVERTICAL);
205 buttons->Add( m_migrate_button = new wxButton(this,
206 Dialog_Migrate_MigrateNowButton, _T("Migrate Now")),
207 0, wxALIGN_CENTRE, 0);
208 m_migrate_button->SetDefault();
209 buttons->AddSpacer(10);
210 buttons->Add( new wxButton(this, Dialog_Migrate_CancelButton,
211 _T("Cancel")),
212 0, wxALIGN_CENTRE, 0);
214 sizer->Add(buttons, 1, wxALIGN_CENTRE | wxLEFT | wxRIGHT, 10);
217 void MigrateDlg::Main_AddDestSizer(wxSizer *sizer)
219 wxArrayString devices;
220 devices.Add(_T("Prompt to plug in later..."));
221 for( Barry::Probe::Results::const_iterator i = m_results.begin();
222 i != m_results.end(); ++i )
224 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
227 wxSizer *dest = new wxStaticBoxSizer(
228 new wxStaticBox(this, wxID_ANY, _T("Destination device")),
229 wxVERTICAL
231 dest->Add(
232 m_dest_combo = new wxChoice(this, wxID_ANY,
233 wxDefaultPosition, wxSize(225, -1), devices),
234 0, wxALL, 5);
235 m_dest_combo->SetSelection(0);
238 wxArrayString write_modes;
239 write_modes.Add(_T("Erase all, then restore"));
240 write_modes.Add(_T("Add new, and overwrite existing"));
241 write_modes.Add(_T("Add only, don't overwrite existing"));
242 write_modes.Add(_T("Add every record as a new entry (may cause duplicates)"));
244 dest->Add( new wxStaticText(this, wxID_ANY, _T("Write Mode:")),
245 0, wxTOP | wxLEFT | wxRIGHT, 5);
246 dest->Add( m_write_mode_combo = new wxChoice(this, wxID_ANY,
247 wxDefaultPosition, wxSize(225, -1), write_modes),
248 0, wxALL, 5);
249 m_write_mode_combo->SetSelection(0);
251 // dest->Add( m_wipe_check = wxCheckBox(maybe a checkbox for "wipe device before restore"));
253 sizer->Add(dest, 0, wxEXPAND, 0);
256 void MigrateDlg::EnableControls(bool enable)
258 m_source_combo->Enable(enable);
259 m_dest_combo->Enable(enable);
260 m_write_mode_combo->Enable(enable);
261 m_migrate_button->Enable(enable);
262 //m_wipe_check->Enable(enable);
265 void MigrateDlg::DoSafeClose()
267 // if migrate thread is running, try to close it down first...
268 // do not exit the dialog until the thread is properly stopped!
269 if( m_thread_running ) {
270 m_abort_flag = true;
272 m_status->SetLabel(_T("Waiting for thread to close..."));
274 if( m_migrate_thread_created ) {
275 void *junk;
276 pthread_join(m_migrate_thread, &junk);
277 m_migrate_thread_created = false;
281 // all activity is stopped, so close dialog
282 EndModal(wxID_CANCEL);
285 void MigrateDlg::SendEvent(int event_type)
287 wxCommandEvent event(event_type, wxID_ANY);
288 event.SetEventObject(this);
289 AddPendingEvent(event);
292 void MigrateDlg::SendStatusEvent(const wxString &msg, int pos, int max)
294 wxCommandEvent event(MET_SET_STATUS_MSG, wxID_ANY);
295 event.SetEventObject(this);
297 event.SetString(msg);
299 int value = 0;
300 if( pos == -1 )
301 value |= 0xff00;
302 else
303 value |= (pos << 8);
305 if( max == -1 )
306 value |= 0xff;
307 else
308 value |= max & 0xff;
309 event.SetInt(value);
311 AddPendingEvent(event);
314 void MigrateDlg::SendErrorMsgEvent(const wxString &msg)
316 wxCommandEvent event(MET_ERROR_MSG, wxID_ANY);
317 event.SetEventObject(this);
318 event.SetString(msg);
319 AddPendingEvent(event);
322 void MigrateDlg::OnMigrateNow(wxCommandEvent &event)
324 // gather info from dialog
325 int source_index = m_source_combo->GetSelection();
326 int dest_index = m_dest_combo->GetSelection();
327 int write_mode_index = m_write_mode_combo->GetSelection();
329 // validate options
330 if( source_index == wxNOT_FOUND || dest_index == wxNOT_FOUND ||
331 write_mode_index == wxNOT_FOUND )
333 wxMessageBox(_T("Please select a source and destination device, as well as the write mode."),
334 _T("Migration Options Needed"), wxOK | wxICON_ERROR);
335 return;
338 // do not migrate from one PIN to the same PIN
339 if( source_index == (dest_index - 1) ) {
340 wxMessageBox(_T("Cannot migrate from and to the same PIN."),
341 _T("Migration Options Error"), wxOK | wxICON_ERROR);
342 return;
345 // set the migration arguments
346 m_source_device = &m_results[source_index];
347 if( dest_index > 0 ) {
348 m_dest_device = &m_results[dest_index-1];
350 else {
351 m_dest_device = 0; // an invalid dest causes a prompt
353 switch( write_mode_index )
355 case 0:
356 m_write_mode = Barry::DeviceParser::ERASE_ALL_WRITE_ALL;
357 break;
358 case 1:
359 m_write_mode = Barry::DeviceParser::INDIVIDUAL_OVERWRITE;
360 break;
361 case 2:
362 m_write_mode = Barry::DeviceParser::ADD_BUT_NO_OVERWRITE;
363 break;
364 case 3:
365 m_write_mode = Barry::DeviceParser::ADD_WITH_NEW_ID;
366 break;
367 default:
368 wxMessageBox(_T("Invalid write mode. This should never happen. Contact the developers."),
369 _T("Internal Logic Error"), wxOK | wxICON_ERROR);
370 return;
373 // disable all buttons and controls except cancel
374 EnableControls(false);
376 // turn off the stop flag
377 m_abort_flag = false;
378 m_thread_running = false;
380 // fire up migrate thread, and let the thread close the dialog when
381 // done (can we EndModal() from a thread?)
382 int ret = pthread_create(&m_migrate_thread, NULL,
383 &MigrateDlg::MigrateThread, this);
384 if( ret != 0 ) {
385 // go back to normal
386 EnableControls(true);
387 return;
389 else {
390 m_migrate_thread_created = true;
393 // thread started... let it finish
396 void MigrateDlg::OnCancel(wxCommandEvent &event)
398 DoSafeClose();
401 void MigrateDlg::OnCloseWindow(wxCloseEvent &event)
403 DoSafeClose();
406 void MigrateDlg::OnThreadFinished(wxCommandEvent &event)
408 if( m_migrate_thread_created ) {
409 m_status->SetLabel(_T("Waiting for thread..."));
410 void *junk;
411 pthread_join(m_migrate_thread, &junk);
412 m_migrate_thread_created = false;
415 if( m_abort_flag ) {
416 // user cancelled in some way, restore GUI
417 EnableControls(true);
418 m_status->SetLabel(_T("Cancelled by user..."));
420 else {
421 // if we were not aborted, then this is success, and we
422 // can close the original dialog
423 EndModal(wxID_CANCEL);
427 void MigrateDlg::OnCheckDestPin(wxCommandEvent &event)
429 ScopeSignaler done(m_waiter);
431 if( m_dest_device && m_dest_device->m_pin.Valid() )
432 return; // nothing to do
434 // no destination pin was available, so user may need to plugin
435 // the new device right now, before continuing
436 int response = wxMessageBox(_T("Please plug in the target device now."),
437 _T("Ready for Writing"), wxOK | wxCANCEL | wxICON_INFORMATION,
438 this);
439 if( response != wxOK ) {
440 // user cancelled
441 m_abort_flag = true;
442 return;
446 wxBusyCursor wait;
447 m_status->SetLabel(_T("Scanning USB for devices..."));
449 // pause for 2 seconds to let any new devices settle down
450 wxGetApp().Yield();
451 wxSleep(2);
453 // rescan the USB and try to find the new device
454 Barry::Probe probe;
455 m_new_results = probe.GetResults();
458 // now prompt the user... create a list of PIN display names
459 wxArrayString devices;
460 for( Barry::Probe::Results::const_iterator i = m_new_results.begin();
461 i != m_new_results.end(); ++i )
463 devices.Add(wxString(i->GetDisplayName().c_str(), wxConvUTF8));
466 m_status->SetLabel(_T("User input..."));
468 do {
470 // prompt the user with this list
471 int choice = wxGetSingleChoiceIndex(_T("Please select the target device to write to:"),
472 _T("Destination PIN"), devices, this);
473 if( choice == -1 ) {
474 // user cancelled
475 m_abort_flag = true;
476 return;
479 // found the new PIN to use!
480 m_dest_device = &m_new_results[choice];
482 // check if user needs to choose again
483 if( m_dest_device->m_pin == m_source_device->m_pin ) {
484 wxMessageBox(_T("Cannot use the same device PIN as migration destination."),
485 _T("Invalid Device Selection"),
486 wxOK | wxICON_ERROR, this);
489 } while( m_dest_device->m_pin == m_source_device->m_pin );
492 void MigrateDlg::OnSetStatusMsg(wxCommandEvent &event)
494 if( event.GetString().size() ) {
495 m_status->SetLabel(event.GetString());
498 if( (unsigned int)event.GetInt() != 0xffff ) {
499 unsigned int value = (unsigned int) event.GetInt();
500 unsigned int pos = (value & 0xff00) >> 8;
501 unsigned int max = (value & 0xff);
503 if( pos != 0xff ) {
504 m_progress->SetValue(pos);
507 if( max != 0xff ) {
508 m_progress->SetRange(max);
513 void MigrateDlg::OnPromptPassword(wxCommandEvent &event)
515 ScopeSignaler done(m_waiter);
517 // create prompt based on exception data
518 ostringstream oss;
519 oss << "Please enter device password: ("
520 << event.GetInt()
521 << " tries remaining)";
522 wxString prompt(oss.str().c_str(), wxConvUTF8);
524 // ask user for device password
525 m_password = wxGetPasswordFromUser(prompt,
526 _T("Device Password"), _T(""), this);
529 void MigrateDlg::OnErrorMsg(wxCommandEvent &event)
531 ScopeSignaler done(m_waiter);
532 wxMessageBox(event.GetString(), _T("Migration Error"),
533 wxOK | wxICON_ERROR, this);
536 void* MigrateDlg::MigrateThread(void *arg)
538 MigrateDlg *us = (MigrateDlg*) arg;
540 // we are running!
541 us->m_thread_running = true;
543 try {
544 // backup source PIN
545 us->BackupSource();
547 // make sure we have a destination PIN
548 if( !us->m_abort_flag )
549 us->CheckDestPin();
551 // restore to dest PIN
552 if( !us->m_abort_flag )
553 us->RestoreToDest();
555 catch( std::exception &e ) {
556 us->SendErrorMsgEvent(wxString(e.what(), wxConvUTF8));
557 us->m_waiter.Wait();
560 // invalidate the device selection pointers, since
561 // m_new_results may not always exist
562 us->m_source_device = us->m_dest_device = 0;
564 // we are done!
565 us->m_thread_running = false;
567 // send event to let main GUI thread we're finished
568 us->SendEvent(MET_THREAD_FINISHED);
570 return 0;
573 class PeekParser : public Barry::Parser
575 std::string m_dbname;
577 public:
578 virtual void ParseRecord(const Barry::DBData &data,
579 const Barry::IConverter *ic)
581 m_dbname = data.GetDBName();
584 const std::string& GetDBName() const { return m_dbname; }
588 // This is called from the thread
589 void MigrateDlg::BackupSource()
591 // connect to the source device
592 SendStatusEvent(_T("Connecting..."));
593 EventDesktopConnector connect(this, "", "utf-8", *m_source_device);
594 if( !connect.Reconnect(2) ) {
595 // user cancelled
596 m_abort_flag = true;
597 return;
600 // fetch DBDB, for list of databases to backup... back them all up
601 // remember to save this DBDB into the class, so it is available
602 // for the restore stage
603 m_source_dbdb = connect.GetDesktop().GetDBDB();
604 unsigned int total = m_source_dbdb.GetTotalRecordCount();
606 // create DeviceBuilder for fetching all
607 Barry::DeviceBuilder builder(connect.GetDesktop());
608 builder.Add(m_source_dbdb);
610 // calculate the default backup path location, based on user name
611 // (see backup GUI for code?)
612 Barry::ConfigFile cfg(m_source_device->m_pin);
613 Barry::ConfigFile::CheckPath(cfg.GetPath());
614 m_backup_tarfile = cfg.GetPath() + "/" +
615 Barry::MakeBackupFilename(m_source_device->m_pin, "migrate");
617 // and create tarball output parser
618 Barry::Backup parser(m_backup_tarfile);
620 // create the pipe
621 Barry::Pipe pipe(builder);
623 PeekParser peeker;
625 // create tee parser, so we can see what's being written
626 Barry::TeeParser tee;
627 tee.Add(peeker);
628 tee.Add(parser);
630 // setup status bar
631 unsigned int count = 0;
632 SendStatusEvent(_T(""), 0, 100);
634 // cycle through all databases
635 while( !builder.EndOfFile() ) {
636 if( !pipe.PumpEntry(tee) )
637 continue;
639 count++;
641 // ok, first entry has been pumped, so we can use
642 // peeker to create the status message, and only once
643 ostringstream oss;
644 oss << "Backing up database: " << peeker.GetDBName() << "...";
646 // calculate 1 to 100 percentage, based on number of
647 // databases being backed up, and update status bar too
648 int percent = count / (float)total * 100.0;
650 // send status event once
651 SendStatusEvent(wxString(oss.str().c_str(),wxConvUTF8), percent);
653 // backup the rest of the database, but don't update status
654 // for each record
655 int block = 0;
656 while( pipe.PumpEntry(tee) ) {
657 count++;
658 block++;
659 if( block >= 25 ) {
660 int percent = count / (float)total * 100.0;
661 SendStatusEvent(_T(""), percent);
662 block = 0;
665 // on each record (pump cycle?), as often as possible,
666 // check the m_abort_flag, and abort if necessary,
667 // updating the status message
668 if( m_abort_flag ) {
669 SendStatusEvent(_T("Backup aborted by user..."));
670 return;
675 // close all files, etc.
676 parser.Close();
679 // This is called from the thread
680 void MigrateDlg::CheckDestPin()
682 SendEvent(MET_CHECK_DEST_PIN);
683 m_waiter.Wait();
686 // This is called from the thread
687 void MigrateDlg::RestoreToDest()
689 // connect to the dest device
690 SendStatusEvent(_T("Connecting to target..."));
691 EventDesktopConnector connect(this, "", "utf-8", *m_dest_device);
692 if( !connect.Reconnect(2) ) {
693 // user cancelled
694 m_abort_flag = true;
695 return;
698 // fetch DBDB of dest device, for list of databases we can restore
699 // to... compare with the backup DBDB to create a list of similarly
700 // named databases which we can restore....
701 Barry::DatabaseDatabase dest_dbdb = connect.GetDesktop().GetDBDB();
703 // create the restore builder object, to read from tarball
704 Barry::Restore builder(m_backup_tarfile);
706 // add all database names from the _dest_ DBDB to the Restore
707 // filter... this way, only databases that exist on the new
708 // device will get restored
709 builder.Add(dest_dbdb);
710 unsigned int total = builder.GetRecordTotal();// counts filtered records
712 // create device parser, to write to device
713 Barry::DeviceParser parser(connect.GetDesktop(), m_write_mode);
715 // create the pipe
716 Barry::Pipe pipe(builder);
718 // setup status bar
719 unsigned int count = 0;
720 SendStatusEvent(_T(""), 0, 100);
722 // cycle through all databases
723 Barry::DBData meta; // record meta data of next tar record
724 while( !builder.EndOfFile() ) try {
725 if( !builder.GetNextMeta(meta) )
726 continue;
728 // ok, first entry has been pumped, so we can use
729 // peeker to create the status message, and only once
730 ostringstream oss;
731 oss << "Writing database: " << meta.GetDBName() << "...";
733 // for debugging purposes in the field, display the names
734 // of the databases we restore
735 cerr << oss.str() << endl;
737 // calculate 1 to 100 percentage, based on number of
738 // databases being backed up, and update status bar too
739 int percent = count / (float)total * 100.0;
741 // send status event once
742 SendStatusEvent(wxString(oss.str().c_str(),wxConvUTF8), percent);
744 // restore the rest of the database, but don't update status
745 // for each record
746 int block = 0;
747 while( pipe.PumpEntry(parser) ) {
748 count++;
749 block++;
750 if( block >= 25 ) {
751 int percent = count / (float)total * 100.0;
752 SendStatusEvent(_T(""), percent);
753 block = 0;
756 // on each record (pump cycle?), as often as possible,
757 // check the m_abort_flag, and abort if necessary,
758 // updating the status message
759 if( m_abort_flag ) {
760 SendStatusEvent(_T("Restore aborted by user..."));
761 return;
765 catch( Barry::ReturnCodeError &e ) {
766 cerr << "Unable to clear or write to database '" << meta.GetDBName() << "'"
767 << ": " << e.what() << endl;
768 cerr << "Continuing to process remaining records..." << endl;
770 // skip the problematic database, and keep on trying
771 builder.SkipCurrentDB();