/* Copyright (C) 2005 J.F.Dockes * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "autoconfig.h" #include #include "safeunistd.h" #include #include #include "execmd.h" #include "log.h" #include "transcode.h" #include "indexer.h" #include "rclmain_w.h" #include "specialindex.h" #include "readfile.h" #include "snippets_w.h" #include "idxstatus.h" using namespace std; // This is called from periodic100 if we started an indexer, or from // the rclmain idxstatus file watcher, every time the file changes. void RclMain::updateIdxStatus() { DbIxStatus status; readIdxStatus(theconfig, status); QString msg = tr("Indexing in progress: "); QString phs; switch (status.phase) { case DbIxStatus::DBIXS_NONE:phs=tr("None");break; case DbIxStatus::DBIXS_FILES: phs=tr("Updating");break; case DbIxStatus::DBIXS_PURGE: phs=tr("Purge");break; case DbIxStatus::DBIXS_STEMDB: phs=tr("Stemdb");break; case DbIxStatus::DBIXS_CLOSING:phs=tr("Closing");break; case DbIxStatus::DBIXS_DONE:phs=tr("Done");break; case DbIxStatus::DBIXS_MONITOR:phs=tr("Monitor");break; default: phs=tr("Unknown");break; } msg += phs + " "; if (status.phase == DbIxStatus::DBIXS_FILES) { char cnts[100]; if (status.dbtotdocs > 0) { string format = qs2utf8s(tr("(%d documents/%d files/%d errors/%d total files) ")); sprintf(cnts, format.c_str(), status.docsdone, status.filesdone, status.fileerrors, status.totfiles); } else { string format = qs2utf8s(tr("(%d documents/%d files/%d errors) ")); sprintf(cnts, format.c_str(), status.docsdone, status.filesdone, status.fileerrors); } msg += QString::fromUtf8(cnts) + " "; } string mf;int ecnt = 0; string fcharset = theconfig->getDefCharset(true); // If already UTF-8 let it be, else try to transcode, or url-encode if (!transcode(status.fn, mf, "UTF-8", "UTF-8", &ecnt) || ecnt) { if (!transcode(status.fn, mf, fcharset, "UTF-8", &ecnt) || ecnt) { mf = url_encode(status.fn, 0); } } msg += QString::fromUtf8(mf.c_str()); statusBar()->showMessage(msg, 4000); } // This is called by a periodic timer to check the status of // indexing, a possible need to exit, and cleanup exited viewers void RclMain::periodic100() { LOGDEB2("Periodic100\n" ); if (m_idxproc) { // An indexing process was launched. If its' done, see status. int status; bool exited = m_idxproc->maybereap(&status); if (exited) { QString reasonmsg; if (m_idxreasontmp && m_idxreasontmp->ok()) { string reasons; file_to_string(m_idxreasontmp->filename(), reasons); if (!reasons.empty()) { ConfSimple rsn(reasons); vector sects = rsn.getNames(""); for (const auto& nm : sects) { string val; if (rsn.get(nm, val)) { reasonmsg.append(u8s2qs(string("
") + nm + " : " + val)); } } } } deleteZ(m_idxproc); if (status) { if (m_idxkilled) { QMessageBox::warning(0, "Recoll", tr("Indexing interrupted")); m_idxkilled = false; } else { QString msg(tr("Indexing failed")); if (!reasonmsg.isEmpty()) { msg.append(tr(" with additional message: ")); msg.append(reasonmsg); } QMessageBox::warning(0, "Recoll", msg); } } else { // On the first run, show missing helpers. We only do this once if (m_firstIndexing) showMissingHelpers(); if (!reasonmsg.isEmpty()) { QString msg = tr("Non-fatal indexing message: "); msg.append(reasonmsg); QMessageBox::warning(0, "Recoll", msg); } } string reason; maybeOpenDb(reason, 1); } else { // update/show status even if the status file did not // change (else the status line goes blank during // lengthy operations). updateIdxStatus(); } } // Update the "start/stop indexing" menu entry, can't be done from // the "start/stop indexing" slot itself IndexerState prevstate = m_indexerState; if (m_idxproc) { m_indexerState = IXST_RUNNINGMINE; fileToggleIndexingAction->setText(tr("Stop &Indexing")); fileToggleIndexingAction->setEnabled(true); fileRebuildIndexAction->setEnabled(false); actionSpecial_Indexing->setEnabled(false); periodictimer->setInterval(200); } else { Pidfile pidfile(theconfig->getPidfile()); pid_t pid = pidfile.open(); fileBumpIndexingAction->setEnabled(false); if (pid == getpid()) { // Locked by me m_indexerState = IXST_NOTRUNNING; fileToggleIndexingAction->setText(tr("Index locked")); fileToggleIndexingAction->setEnabled(false); fileRebuildIndexAction->setEnabled(false); actionSpecial_Indexing->setEnabled(false); periodictimer->setInterval(1000); } else if (pid == 0) { m_indexerState = IXST_NOTRUNNING; fileToggleIndexingAction->setText(tr("Update &Index")); fileToggleIndexingAction->setEnabled(true); fileRebuildIndexAction->setEnabled(true); actionSpecial_Indexing->setEnabled(true); periodictimer->setInterval(1000); } else { // Real time or externally started batch indexer running m_indexerState = IXST_RUNNINGNOTMINE; fileToggleIndexingAction->setText(tr("Stop &Indexing")); DbIxStatus status; readIdxStatus(theconfig, status); if (status.hasmonitor) { // Real-time indexer running. We can trigger an // incremental pass fileBumpIndexingAction->setEnabled(true); } fileToggleIndexingAction->setEnabled(true); fileRebuildIndexAction->setEnabled(false); actionSpecial_Indexing->setEnabled(false); periodictimer->setInterval(200); } } if ((prevstate == IXST_RUNNINGMINE || prevstate == IXST_RUNNINGNOTMINE) && m_indexerState == IXST_NOTRUNNING) { showTrayMessage(tr("Indexing done")); } // Possibly cleanup the dead viewers for (vector::iterator it = m_viewers.begin(); it != m_viewers.end(); it++) { int status; if ((*it)->maybereap(&status)) { deleteZ(*it); } } vector v; for (vector::iterator it = m_viewers.begin(); it != m_viewers.end(); it++) { if (*it) v.push_back(*it); } m_viewers = v; if (recollNeedsExit) fileExit(); } bool RclMain::checkIdxPaths() { string badpaths; vector args {"recollindex", "-c", theconfig->getConfDir(), "-E"}; ExecCmd::backtick(args, badpaths); if (!badpaths.empty()) { int rep = QMessageBox::warning( 0, tr("Bad paths"), tr("Empty or non-existant paths in configuration file. " "Click Ok to start indexing anyway " "(absent data will not be purged from the index):\n") + QString::fromLocal8Bit(badpaths.c_str()), QMessageBox::Ok, QMessageBox::Cancel, QMessageBox::NoButton); if (rep == QMessageBox::Cancel) return false; } return true; } // This gets called when the "update index" action is activated. It executes // the requested action, and disables the menu entry. This will be // re-enabled by the indexing status check void RclMain::toggleIndexing() { if (!m_idxreasontmp || !m_idxreasontmp->ok()) { // We just store the pointer and let the tempfile cleaner deal // with delete on exiting TempFile temp(".txt"); m_idxreasontmp = rememberTempFile(temp); } switch (m_indexerState) { case IXST_RUNNINGMINE: if (m_idxproc) { // Indexing was in progress, request stop. Let the periodic // routine check for the results. if (m_idxproc->requestChildExit()) { m_idxkilled = true; } } break; case IXST_RUNNINGNOTMINE: { int rep = QMessageBox::information( 0, tr("Warning"), tr("The current indexing process was not started from this " "interface. Click Ok to kill it " "anyway, or Cancel to leave it alone"), QMessageBox::Ok, QMessageBox::Cancel, QMessageBox::NoButton); if (rep == QMessageBox::Ok) { #ifdef _WIN32 // No simple way to signal the process. Use the stop file ::close(::creat(theconfig->getIdxStopFile().c_str(), 0666)); #else Pidfile pidfile(theconfig->getPidfile()); pid_t pid = pidfile.open(); if (pid > 0) kill(pid, SIGTERM); #endif // !_WIN32 } } break; case IXST_NOTRUNNING: { // Could also mean that no helpers are missing, but then we // won't try to show a message anyway (which is what // firstIndexing is used for) string mhd; m_firstIndexing = !theconfig->getMissingHelperDesc(mhd); if (!checkIdxPaths()) { return; } vector args{"-c", theconfig->getConfDir()}; if (m_idxreasontmp && m_idxreasontmp->ok()) { args.push_back("-R"); args.push_back(m_idxreasontmp->filename()); } m_idxproc = new ExecCmd; m_idxproc->startExec("recollindex", args, false, false); } break; case IXST_UNKNOWN: return; } } #ifndef _WIN32 void RclMain::bumpIndexing() { DbIxStatus status; readIdxStatus(theconfig, status); if (status.hasmonitor) { string cmd("touch "); string path = path_cat(theconfig->getConfDir(), "recoll.conf"); cmd += path; int status; if ((status = system(cmd.c_str()))) { cerr << cmd << " failed with status " << status << endl; } } } #else // Because moc does not understand ifdefs, have to have this as an empty func void RclMain::bumpIndexing() { } #endif static void delay(int millisecondsWait) { QEventLoop loop; QTimer t; t.connect(&t, SIGNAL(timeout()), &loop, SLOT(quit())); t.start(millisecondsWait); loop.exec(); } void RclMain::rebuildIndex() { if (!m_idxreasontmp || !m_idxreasontmp->ok()) { // We just store the pointer and let the tempfile cleaner deal // with delete on exiting TempFile temp(".txt"); m_idxreasontmp = rememberTempFile(temp); } if (m_indexerState == IXST_UNKNOWN) { delay(1500); } switch (m_indexerState) { case IXST_UNKNOWN: case IXST_RUNNINGMINE: case IXST_RUNNINGNOTMINE: return; //?? Should not have been called case IXST_NOTRUNNING: { if (m_idxproc) { LOGERR("RclMain::rebuildIndex: current indexer exec not null\n" ); return; } int rep = QMessageBox::warning(0, tr("Erasing index"), tr("Reset the index and start " "from scratch ?"), QMessageBox::Ok, QMessageBox::Cancel, QMessageBox::NoButton); if (rep == QMessageBox::Ok) { #ifdef _WIN32 // Under windows, it is necessary to close the db here, // else Xapian won't be able to do what it wants with the // (open) files. Of course if there are several GUI // instances, this won't work... Also it's quite difficult // to make sure that there are no more references to the // db because, for example of the Enquire objects inside // Query inside Docsource etc. // // !! At this moment, this does not work if a preview has // !! been opened. Could not find the reason (mysterious // !! Xapian::Database reference somewhere?). The indexing // !! fails, leaving a partial index directory. Then need // !! to restart the GUI to succeed in reindexing. if (rcldb) { resetSearch(); deleteZ(m_snippets); rcldb->close(); } #endif // _WIN32 // Could also mean that no helpers are missing, but then we // won't try to show a message anyway (which is what // firstIndexing is used for) string mhd; m_firstIndexing = !theconfig->getMissingHelperDesc(mhd); if (!checkIdxPaths()) { return; } vector args{"-c", theconfig->getConfDir(), "-z"}; if (m_idxreasontmp && m_idxreasontmp->ok()) { args.push_back("-R"); args.push_back(m_idxreasontmp->filename()); } m_idxproc = new ExecCmd; m_idxproc->startExec("recollindex", args, false, false); } } break; } } void SpecIdxW::onBrowsePB_clicked() { QString dir = myGetFileName(true, tr("Top indexed entity"), true); targLE->setText(dir); } bool SpecIdxW::noRetryFailed() { return !retryFailedCB->isChecked(); } bool SpecIdxW::eraseFirst() { return eraseBeforeCB->isChecked(); } std::vector SpecIdxW::selpatterns() { vector pats; string text = qs2utf8s(selPatsLE->text()); if (!text.empty()) { stringToStrings(text, pats); } return pats; } std::string SpecIdxW::toptarg() { return qs2utf8s(targLE->text()); } void SpecIdxW::onTargLE_textChanged(const QString& text) { if (text.isEmpty()) selPatsLE->setEnabled(false); else selPatsLE->setEnabled(true); } static string execToString(const string& cmd, const vector& args) { string command = cmd + " "; for (vector::const_iterator it = args.begin(); it != args.end(); it++) { command += "{" + *it + "} "; } return command; } void RclMain::specialIndex() { LOGDEB("RclMain::specialIndex\n" ); if (!m_idxreasontmp || !m_idxreasontmp->ok()) { // We just store the pointer and let the tempfile cleaner deal // with delete on exiting TempFile temp(".txt"); m_idxreasontmp = rememberTempFile(temp); } switch (m_indexerState) { case IXST_UNKNOWN: case IXST_RUNNINGMINE: case IXST_RUNNINGNOTMINE: return; //?? Should not have been called case IXST_NOTRUNNING: default: break; } if (m_idxproc) { LOGERR("RclMain::rebuildIndex: current indexer exec not null\n" ); return; } if (!specidx) // ?? return; vector args{"-c", theconfig->getConfDir()}; if (m_idxreasontmp && m_idxreasontmp->ok()) { args.push_back("-R"); args.push_back(m_idxreasontmp->filename()); } string top = specidx->toptarg(); if (!top.empty()) { args.push_back("-r"); } if (specidx->eraseFirst()) { if (top.empty()) { args.push_back("-Z"); } else { args.push_back("-e"); } } if (!specidx->noRetryFailed()) { args.push_back("-k"); } else { args.push_back("-K"); } vector selpats = specidx->selpatterns(); if (!selpats.empty() && top.empty()) { QMessageBox::warning(0, tr("Selection patterns need topdir"), tr("Selection patterns can only be used with a " "start directory"), QMessageBox::Ok, QMessageBox::NoButton); return; } for (vector::const_iterator it = selpats.begin(); it != selpats.end(); it++) { args.push_back("-p"); args.push_back(*it); } if (!top.empty()) { args.push_back(top); } m_idxproc = new ExecCmd; LOGINFO("specialIndex: exec: " << execToString("recollindex", args) <startExec("recollindex", args, false, false); } void RclMain::updateIdxForDocs(vector& docs) { if (m_idxproc) { QMessageBox::warning(0, tr("Warning"), tr("Can't update index: indexer running"), QMessageBox::Ok, QMessageBox::NoButton); return; } vector paths; if (Rcl::docsToPaths(docs, paths)) { vector args{"-c", theconfig->getConfDir(), "-e", "-i"}; if (m_idxreasontmp && m_idxreasontmp->ok()) { args.push_back("-R"); args.push_back(m_idxreasontmp->filename()); } args.insert(args.end(), paths.begin(), paths.end()); m_idxproc = new ExecCmd; m_idxproc->startExec("recollindex", args, false, false); fileToggleIndexingAction->setText(tr("Stop &Indexing")); } fileToggleIndexingAction->setEnabled(false); actionSpecial_Indexing->setEnabled(false); }