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