File indexing completed on 2024-05-05 04:48:50
0001 /**************************************************************************************** 0002 * Copyright (c) 2003-2008 Mark Kretschmann <kretschmann@kde.org> * 0003 * Copyright (c) 2007 Maximilian Kossick <maximilian.kossick@googlemail.com> * 0004 * Copyright (c) 2007 Casey Link <unnamedrambler@gmail.com> * 0005 * Copyright (c) 2008-2009 Jeff Mitchell <mitchell@kde.org> * 0006 * Copyright (c) 2010-2011 Ralf Engels <ralf-engels@gmx.de> * 0007 * Copyright (c) 2011 Bart Cerneels <bart.cerneels@kde.org> * 0008 * Copyright (c) 2013 Ralf Engels <ralf-engels@gmx.de> * 0009 * * 0010 * This program is free software; you can redistribute it and/or modify it under * 0011 * the terms of the GNU General Public License as published by the Free Software * 0012 * Foundation; either version 2 of the License, or (at your option) any later * 0013 * version. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0016 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0017 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0018 * * 0019 * You should have received a copy of the GNU General Public License along with * 0020 * this program. If not, see <http://www.gnu.org/licenses/>. * 0021 ****************************************************************************************/ 0022 0023 #define DEBUG_PREFIX "GenericScannerJob" 0024 0025 #include "GenericScannerJob.h" 0026 0027 #include "App.h" 0028 #include "GenericScanManager.h" 0029 #include "core/support/Debug.h" 0030 #include "collectionscanner/ScanningState.h" 0031 0032 #include <KProcess> 0033 0034 #include <QFile> 0035 #include <QSharedMemory> 0036 #include <QStandardPaths> 0037 #include <QUuid> 0038 0039 static const int MAX_RESTARTS = 40; 0040 static const int SHARED_MEMORY_SIZE = 1024 * 1024; // 1 MB shared memory 0041 0042 GenericScannerJob::GenericScannerJob( GenericScanManager* manager, 0043 const QStringList &scanDirsRequested, 0044 GenericScanManager::ScanType type, 0045 bool recursive, bool detectCharset ) 0046 : QObject() 0047 , ThreadWeaver::Job( ) 0048 , m_manager( manager ) 0049 , m_type( type ) 0050 , m_scanDirsRequested( scanDirsRequested ) 0051 , m_input( nullptr ) 0052 , m_restartCount( 0 ) 0053 , m_abortRequested( false ) 0054 , m_scanner( nullptr ) 0055 , m_scannerStateMemory( nullptr ) 0056 , m_recursive( recursive ) 0057 , m_charsetDetect( detectCharset ) 0058 { 0059 } 0060 0061 GenericScannerJob::GenericScannerJob( GenericScanManager* manager, 0062 QIODevice *input, 0063 GenericScanManager::ScanType type ) 0064 : QObject() 0065 , ThreadWeaver::Job( ) 0066 , m_manager( manager ) 0067 , m_type( type ) 0068 , m_input( input ) 0069 , m_restartCount( 0 ) 0070 , m_abortRequested( false ) 0071 , m_scanner( nullptr ) 0072 , m_scannerStateMemory( nullptr ) 0073 , m_recursive( true ) 0074 , m_charsetDetect( false ) 0075 { 0076 } 0077 0078 0079 GenericScannerJob::~GenericScannerJob() 0080 { 0081 delete m_scanner; 0082 delete m_scannerStateMemory; 0083 0084 if( !m_batchfilePath.isEmpty() ) 0085 QFile( m_batchfilePath ).remove(); 0086 } 0087 0088 void 0089 GenericScannerJob::run(ThreadWeaver::JobPointer self, ThreadWeaver::Thread *thread) 0090 { 0091 Q_UNUSED(self); 0092 Q_UNUSED(thread); 0093 // -- initialize the input 0094 // - from io device 0095 if( m_input ) 0096 { 0097 m_reader.setDevice( m_input ); 0098 } 0099 // - from process 0100 else 0101 { 0102 if( !createScannerProcess() ) 0103 return; 0104 } 0105 0106 Q_EMIT started( m_type ); 0107 0108 // -- read the input and loop 0109 bool finished = false; 0110 do 0111 { 0112 // -- check if we were aborted, have finished or need to wait for new data 0113 { 0114 QMutexLocker locker( &m_mutex ); 0115 if( m_abortRequested ) 0116 { 0117 debug() << "Aborting ScannerJob"; 0118 Q_EMIT failed( i18n( "Abort for scanner requested" ) ); 0119 closeScannerProcess(); 0120 return; 0121 } 0122 } 0123 0124 if( m_scanner ) 0125 { 0126 if( m_reader.atEnd() ) 0127 getScannerOutput(); 0128 0129 if( m_scanner->exitStatus() != QProcess::NormalExit ) 0130 { 0131 if( !restartScannerProcess() ) 0132 return; 0133 } 0134 } 0135 0136 // -- scan as many directory tags as we added to the data 0137 finished = parseScannerOutput(); 0138 0139 } while( !finished && 0140 (!m_reader.hasError() || m_reader.error() == QXmlStreamReader::PrematureEndOfDocumentError) ); 0141 0142 { 0143 QMutexLocker locker( &m_mutex ); 0144 if( !finished && m_reader.hasError() ) 0145 { 0146 warning() << "Aborting ScannerJob with error" << m_reader.errorString(); 0147 Q_EMIT failed( i18n( "Aborting scanner with error: %1", m_reader.errorString() ) ); 0148 closeScannerProcess(); 0149 return; 0150 } 0151 else 0152 { 0153 Q_EMIT succeeded(); 0154 closeScannerProcess(); 0155 return; 0156 } 0157 } 0158 } 0159 0160 void 0161 GenericScannerJob::defaultBegin(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0162 { 0163 Q_EMIT started(self); 0164 ThreadWeaver::Job::defaultBegin(self, thread); 0165 } 0166 0167 void 0168 GenericScannerJob::defaultEnd(const ThreadWeaver::JobPointer& self, ThreadWeaver::Thread *thread) 0169 { 0170 ThreadWeaver::Job::defaultEnd(self, thread); 0171 if (!self->success()) { 0172 Q_EMIT failed(self); 0173 } 0174 Q_EMIT done(self); 0175 } 0176 0177 void 0178 GenericScannerJob::abort() 0179 { 0180 QMutexLocker locker( &m_mutex ); 0181 m_abortRequested = true; 0182 } 0183 0184 QString 0185 GenericScannerJob::scannerPath() 0186 { 0187 // Defined in the tests so we use the recently built scanner for testing 0188 const QString overridePath = qApp->property( "overrideUtilitiesPath" ).toString(); 0189 QString path; 0190 if( overridePath.isEmpty() ) // Not running a test 0191 { 0192 path = QStandardPaths::findExecutable( QStringLiteral("amarokcollectionscanner") ); 0193 0194 // TODO: Not sure this is still useful... 0195 // If the binary is not in $PATH, then search in the application folder too 0196 if( path.isEmpty() ) 0197 path = App::applicationDirPath() + "/amarokcollectionscanner"; 0198 } 0199 else 0200 { 0201 // Running a test, use the path + append collectionscanner 0202 path = overridePath + "/collectionscanner/amarokcollectionscanner"; 0203 } 0204 0205 if( !QFile::exists( path ) ) 0206 { 0207 error() << "Cannot find amarokcollectionscanner! Check your install"; 0208 Q_EMIT failed( i18n( "Could not find amarokcollectionscanner!" ) ); 0209 return QString(); 0210 } 0211 return path; 0212 } 0213 0214 bool 0215 GenericScannerJob::createScannerProcess( bool restart ) 0216 { 0217 // -- create the shared memory 0218 if( !m_scannerStateMemory && !restart ) 0219 { 0220 QString sharedMemoryKey = "AmarokScannerMemory"+QUuid::createUuid().toString(); 0221 m_scannerStateMemory = new QSharedMemory( sharedMemoryKey ); 0222 if( !m_scannerStateMemory->create( SHARED_MEMORY_SIZE ) ) 0223 { 0224 warning() << "Unable to create shared memory for collection scanner"; 0225 warning() << "Shared Memory error: " << m_scannerStateMemory->errorString(); 0226 delete m_scannerStateMemory; 0227 m_scannerStateMemory = nullptr; 0228 } 0229 } 0230 0231 // -- create the scanner process 0232 KProcess *scanner = new KProcess(); //not parented since in a different thread 0233 scanner->setOutputChannelMode( KProcess::OnlyStdoutChannel ); 0234 0235 // debug() << "creating options"; 0236 *scanner << scannerPath() << QStringLiteral("--idlepriority"); 0237 0238 if( m_type != GenericScanManager::FullScan ) // we don't need a batch file for a full scan 0239 m_batchfilePath = m_manager->getBatchFile( m_scanDirsRequested ); 0240 0241 if( m_type != GenericScanManager::FullScan ) 0242 *scanner << QStringLiteral("-i"); 0243 0244 if( !m_batchfilePath.isEmpty() ) 0245 *scanner << QStringLiteral("--batch") << m_batchfilePath; 0246 0247 if( m_recursive ) 0248 *scanner << QStringLiteral("-r"); 0249 0250 if( m_charsetDetect ) 0251 *scanner << QStringLiteral("-c"); 0252 0253 if( restart ) 0254 *scanner << QStringLiteral("-s"); 0255 0256 // debug() << "creating shared memory"; 0257 if( m_scannerStateMemory ) 0258 *scanner << QStringLiteral("--sharedmemory") << m_scannerStateMemory->key(); 0259 0260 *scanner << m_scanDirsRequested; 0261 0262 // debug() << "starting"; 0263 scanner->start(); 0264 if( !scanner->waitForStarted( 5000 ) ) 0265 { 0266 delete scanner; 0267 0268 warning() << "Unable to start Amarok collection scanner."; 0269 Q_EMIT failed( i18n("Unable to start Amarok collection scanner." ) ); 0270 return false; 0271 } 0272 // debug() << "finished"; 0273 0274 m_scanner = scanner; 0275 return true; 0276 } 0277 0278 bool 0279 GenericScannerJob::restartScannerProcess() 0280 { 0281 if( m_scanner->exitStatus() == QProcess::NormalExit ) 0282 return true; // all shiny. no need to restart 0283 0284 m_restartCount++; 0285 warning() << __PRETTY_FUNCTION__ << scannerPath().toLocal8Bit().data() 0286 << "crashed, restart count is " << m_restartCount; 0287 0288 // -- try to determine the offending file 0289 QStringList badFiles; 0290 if( m_scannerStateMemory ) 0291 { 0292 using namespace CollectionScanner; 0293 ScanningState scanningState; 0294 scanningState.setKey( m_scannerStateMemory->key() ); 0295 scanningState.readFull(); 0296 0297 badFiles << scanningState.badFiles(); 0298 // yes, the last file is also bad, CollectionScanner only adds it after restart 0299 badFiles << scanningState.lastFile(); 0300 0301 debug() << __PRETTY_FUNCTION__ << "lastDirectory" << scanningState.lastDirectory(); 0302 debug() << __PRETTY_FUNCTION__ << "lastFile" << scanningState.lastFile(); 0303 } 0304 else 0305 debug() << __PRETTY_FUNCTION__ << "m_scannerStateMemory is null"; 0306 0307 // -- delete the old scanner 0308 delete m_scanner; 0309 m_scanner = nullptr; 0310 0311 if( m_restartCount >= MAX_RESTARTS ) 0312 { 0313 debug() << __PRETTY_FUNCTION__ << "Following files made amarokcollectionscanner (or TagLib) crash:"; 0314 foreach( const QString &file, badFiles ) 0315 debug() << __PRETTY_FUNCTION__ << file; 0316 0317 // TODO: this message doesn't seem to be propagated to the UI 0318 QString text = i18n( "The collection scan had to be aborted. Too many crashes (%1) " 0319 "were encountered during the scan. Following files caused the crashes:\n\n%2", 0320 m_restartCount, badFiles.join( QStringLiteral("\n") ) ); 0321 0322 Q_EMIT failed( text ); 0323 return false; 0324 } 0325 0326 createScannerProcess( true ); 0327 0328 return (m_scanner != nullptr); 0329 } 0330 0331 void 0332 GenericScannerJob::closeScannerProcess() 0333 { 0334 if( !m_scanner ) 0335 return; 0336 0337 m_scanner->close(); 0338 m_scanner->waitForFinished(); // waits at most 3 seconds 0339 delete m_scanner; 0340 m_scanner = nullptr; 0341 } 0342 0343 0344 bool 0345 GenericScannerJob::parseScannerOutput() 0346 { 0347 // DEBUG_BLOCK; 0348 while( !m_reader.atEnd() ) 0349 { 0350 // -- check if we were aborted, have finished or need to wait for new data 0351 { 0352 QMutexLocker locker( &m_mutex ); 0353 if( m_abortRequested ) 0354 return false; 0355 } 0356 0357 m_reader.readNext(); 0358 if( m_reader.hasError() ) 0359 { 0360 return false; 0361 } 0362 else if( m_reader.isStartElement() ) 0363 { 0364 QStringRef name = m_reader.name(); 0365 if( name == "scanner" ) 0366 { 0367 int totalCount = m_reader.attributes().value( QStringLiteral("count") ).toString().toInt(); 0368 Q_EMIT directoryCount( totalCount ); 0369 } 0370 else if( name == "directory" ) 0371 { 0372 QSharedPointer<CollectionScanner::Directory> dir( new CollectionScanner::Directory( &m_reader ) ); 0373 0374 Q_EMIT directoryScanned( dir ); 0375 } 0376 else 0377 { 0378 warning() << "Unexpected xml start element"<<name<<"in input"; 0379 m_reader.skipCurrentElement(); 0380 } 0381 0382 } 0383 else if( m_reader.isEndElement() ) 0384 { 0385 if( m_reader.name() == "scanner" ) // ok. finished 0386 return true; 0387 } 0388 else if( m_reader.isEndDocument() ) 0389 { 0390 return true; 0391 } 0392 } 0393 0394 return false; 0395 } 0396 0397 void 0398 GenericScannerJob::getScannerOutput() 0399 { 0400 if( !m_scanner->waitForReadyRead( -1 ) ) 0401 return; 0402 QByteArray newData = m_scanner->readAll(); 0403 m_incompleteTagBuffer += newData; 0404 0405 int index = m_incompleteTagBuffer.lastIndexOf( QLatin1String("</scanner>") ); 0406 if( index >= 0 ) 0407 { 0408 // append new data (we need to be locked. the reader is probably not thread save) 0409 m_reader.addData( QString( m_incompleteTagBuffer.left( index + 10 ) ) ); 0410 m_incompleteTagBuffer = m_incompleteTagBuffer.mid( index + 10 ); 0411 } 0412 else 0413 { 0414 index = m_incompleteTagBuffer.lastIndexOf( QLatin1String("</directory>") ); 0415 if( index >= 0 ) 0416 { 0417 // append new data (we need to be locked. the reader is probably not thread save) 0418 m_reader.addData( QString( m_incompleteTagBuffer.left( index + 12 ) ) ); 0419 m_incompleteTagBuffer = m_incompleteTagBuffer.mid( index + 12 ); 0420 } 0421 } 0422 0423 } 0424