xref: /glogg/src/data/logdata.cpp (revision c1f737e476dd5f2427c03f48844bf9ed87cac11b) !
1 /*
2  * Copyright (C) 2009, 2010, 2013, 2014, 2015 Nicolas Bonnefon and other contributors
3  *
4  * This file is part of glogg.
5  *
6  * glogg is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * glogg is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with glogg.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 // This file implements LogData, the content of a log file.
21 
22 #include <iostream>
23 
24 #include <cassert>
25 
26 #include <QFileInfo>
27 
28 #include "log.h"
29 
30 #include "logdata.h"
31 #include "logfiltereddata.h"
32 #if defined(GLOGG_SUPPORTS_INOTIFY) || defined(GLOGG_SUPPORTS_KQUEUE) || defined(WIN32)
33 #include "platformfilewatcher.h"
34 #else
35 #include "qtfilewatcher.h"
36 #endif
37 
38 // Implementation of the 'start' functions for each operation
39 
40 void LogData::AttachOperation::doStart(
41         LogDataWorkerThread& workerThread ) const
42 {
43     LOG(logDEBUG) << "Attaching " << filename_.toStdString();
44     workerThread.attachFile( filename_ );
45     workerThread.indexAll();
46 }
47 
48 void LogData::FullIndexOperation::doStart(
49         LogDataWorkerThread& workerThread ) const
50 {
51     LOG(logDEBUG) << "Reindexing (full)";
52     workerThread.indexAll();
53 }
54 
55 void LogData::PartialIndexOperation::doStart(
56         LogDataWorkerThread& workerThread ) const
57 {
58     LOG(logDEBUG) << "Reindexing (partial)";
59     workerThread.indexAdditionalLines();
60 }
61 
62 
63 // Constructs an empty log file.
64 // It must be displayed without error.
65 LogData::LogData() : AbstractLogData(), indexing_data_(),
66     fileMutex_(), workerThread_( &indexing_data_ )
67 {
68     // Start with an "empty" log
69     attached_file_ = nullptr;
70     currentOperation_ = nullptr;
71     nextOperation_    = nullptr;
72 
73     codec_ = QTextCodec::codecForName( "ISO-8859-1" );
74 
75 #if defined(GLOGG_SUPPORTS_INOTIFY) || defined(GLOGG_SUPPORTS_KQUEUE) || defined(WIN32)
76     fileWatcher_ = std::make_shared<PlatformFileWatcher>();
77 #else
78     fileWatcher_ = std::make_shared<QtFileWatcher>();
79 #endif
80 
81     // Initialise the file watcher
82     connect( fileWatcher_.get(), SIGNAL( fileChanged( const QString& ) ),
83             this, SLOT( fileChangedOnDisk() ) );
84     // Forward the update signal
85     connect( &workerThread_, SIGNAL( indexingProgressed( int ) ),
86             this, SIGNAL( loadingProgressed( int ) ) );
87     connect( &workerThread_, SIGNAL( indexingFinished( LoadingStatus ) ),
88             this, SLOT( indexingFinished( LoadingStatus ) ) );
89 
90     // Starts the worker thread
91     workerThread_.start();
92 }
93 
94 LogData::~LogData()
95 {
96     // Remove the current file from the watch list
97     if ( attached_file_ )
98         fileWatcher_->removeFile( attached_file_->fileName() );
99 
100     // FIXME
101     // workerThread_.stop();
102 }
103 
104 //
105 // Public functions
106 //
107 
108 void LogData::attachFile( const QString& fileName )
109 {
110     LOG(logDEBUG) << "LogData::attachFile " << fileName.toStdString();
111 
112     if ( attached_file_ ) {
113         // We cannot reattach
114         throw CantReattachErr();
115     }
116 
117     attached_file_.reset( new QFile( fileName ) );
118     attached_file_->open( QIODevice::ReadOnly );
119 
120     std::shared_ptr<const LogDataOperation> operation( new AttachOperation( fileName ) );
121     enqueueOperation( std::move( operation ) );
122 }
123 
124 void LogData::interruptLoading()
125 {
126     workerThread_.interrupt();
127 }
128 
129 qint64 LogData::getFileSize() const
130 {
131     return indexing_data_.getSize();
132 }
133 
134 QDateTime LogData::getLastModifiedDate() const
135 {
136     return lastModifiedDate_;
137 }
138 
139 // Return an initialised LogFilteredData. The search is not started.
140 LogFilteredData* LogData::getNewFilteredData() const
141 {
142     LogFilteredData* newFilteredData = new LogFilteredData( this );
143 
144     return newFilteredData;
145 }
146 
147 void LogData::reload()
148 {
149     workerThread_.interrupt();
150 
151     enqueueOperation( std::make_shared<FullIndexOperation>() );
152 }
153 
154 void LogData::setPollingInterval( uint32_t interval_ms )
155 {
156     fileWatcher_->setPollingInterval( interval_ms );
157 }
158 
159 //
160 // Private functions
161 //
162 
163 // Add an operation to the queue and perform it immediately if
164 // there is none ongoing.
165 void LogData::enqueueOperation( std::shared_ptr<const LogDataOperation> new_operation )
166 {
167     if ( currentOperation_ == nullptr )
168     {
169         // We do it immediately
170         currentOperation_ =  new_operation;
171         startOperation();
172     }
173     else
174     {
175         // An operation is in progress...
176         // ... we schedule the attach op for later
177         nextOperation_ = new_operation;
178     }
179 }
180 
181 // Performs the current operation asynchronously, a indexingFinished
182 // signal will be received when it's finished.
183 void LogData::startOperation()
184 {
185     if ( currentOperation_ )
186     {
187         LOG(logDEBUG) << "startOperation found something to do.";
188 
189         // Let the operation do its stuff
190         currentOperation_->start( workerThread_ );
191     }
192 }
193 
194 //
195 // Slots
196 //
197 
198 void LogData::fileChangedOnDisk()
199 {
200     LOG(logDEBUG) << "signalFileChanged";
201 
202     const QString name = attached_file_->fileName();
203     QFileInfo info( name );
204 
205     // Need to open the file in case it was absent
206     attached_file_->open( QIODevice::ReadOnly );
207 
208     std::shared_ptr<LogDataOperation> newOperation;
209 
210     qint64 file_size = indexing_data_.getSize();
211     LOG(logDEBUG) << "current fileSize=" << file_size;
212     LOG(logDEBUG) << "info file_->size()=" << info.size();
213     if ( info.size() < file_size ) {
214         fileChangedOnDisk_ = Truncated;
215         LOG(logINFO) << "File truncated";
216         newOperation = std::make_shared<FullIndexOperation>();
217     }
218     else if ( info.size() == file_size ) {
219         LOG(logINFO) << "No change in file";
220     }
221     else if ( fileChangedOnDisk_ != DataAdded ) {
222         fileChangedOnDisk_ = DataAdded;
223         LOG(logINFO) << "New data on disk";
224         newOperation = std::make_shared<PartialIndexOperation>();
225     }
226 
227     if ( newOperation ) {
228         enqueueOperation( newOperation );
229         lastModifiedDate_ = info.lastModified();
230 
231         emit fileChanged( fileChangedOnDisk_ );
232     }
233 }
234 
235 void LogData::indexingFinished( LoadingStatus status )
236 {
237     LOG(logDEBUG) << "indexingFinished: " <<
238         ( status == LoadingStatus::Successful ) <<
239         ", found " << indexing_data_.getNbLines() << " lines.";
240 
241     if ( status == LoadingStatus::Successful ) {
242         // Start watching we watch the file for updates
243         fileChangedOnDisk_ = Unchanged;
244         fileWatcher_->addFile( attached_file_->fileName() );
245 
246         // Update the modified date/time if the file exists
247         lastModifiedDate_ = QDateTime();
248         QFileInfo fileInfo( *attached_file_ );
249         if ( fileInfo.exists() )
250             lastModifiedDate_ = fileInfo.lastModified();
251     }
252 
253     // FIXME be cleverer here as a notification might have arrived whilst we
254     // were indexing.
255     fileChangedOnDisk_ = Unchanged;
256 
257     LOG(logDEBUG) << "Sending indexingFinished.";
258     emit loadingFinished( status );
259 
260     // So now the operation is done, let's see if there is something
261     // else to do, in which case, do it!
262     assert( currentOperation_ );
263 
264     currentOperation_ = std::move( nextOperation_ );
265     nextOperation_.reset();
266 
267     if ( currentOperation_ ) {
268         LOG(logDEBUG) << "indexingFinished is performing the next operation";
269         startOperation();
270     }
271 }
272 
273 //
274 // Implementation of virtual functions
275 //
276 qint64 LogData::doGetNbLine() const
277 {
278     return indexing_data_.getNbLines();
279 }
280 
281 int LogData::doGetMaxLength() const
282 {
283     return indexing_data_.getMaxLength();
284 }
285 
286 int LogData::doGetLineLength( qint64 line ) const
287 {
288     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
289 
290     int length = doGetExpandedLineString( line ).length();
291 
292     return length;
293 }
294 
295 void LogData::doSetDisplayEncoding( Encoding encoding )
296 {
297     LOG(logDEBUG) << "AbstractLogData::setDisplayEncoding: " << static_cast<int>( encoding );
298 
299     static const char* latin1_encoding = "iso-8859-1";
300     static const char* utf8_encoding   = "utf-8";
301     static const char* utf16le_encoding   = "utf-16le";
302     static const char* utf16be_encoding   = "utf-16be";
303     static const char* cp1251_encoding   = "CP1251";
304     static const char* cp1252_encoding   = "CP1252";
305 
306     const char* qt_encoding = latin1_encoding;
307 
308     // Default to 0, for 8bit encodings
309     int before_cr = 0;
310     int after_cr  = 0;
311 
312     switch ( encoding ) {
313         case Encoding::ENCODING_UTF8:
314             qt_encoding = utf8_encoding;
315             break;
316         case Encoding::ENCODING_UTF16LE:
317             qt_encoding = utf16le_encoding;
318             before_cr = 0;
319             after_cr  = 1;
320             break;
321         case Encoding::ENCODING_UTF16BE:
322             qt_encoding = utf16be_encoding;
323             before_cr = 1;
324             after_cr  = 0;
325             break;
326         case Encoding::ENCODING_CP1251:
327             qt_encoding = cp1251_encoding;
328             break;
329         case Encoding::ENCODING_CP1252:
330             qt_encoding = cp1252_encoding;
331             break;
332         case Encoding::ENCODING_ISO_8859_1:
333             qt_encoding = latin1_encoding;
334             break;
335         default:
336             LOG( logERROR ) << "Unknown encoding set!";
337             assert( false );
338             break;
339     }
340 
341     doSetMultibyteEncodingOffsets( before_cr, after_cr );
342     codec_ = QTextCodec::codecForName( qt_encoding );
343 }
344 
345 void LogData::doSetMultibyteEncodingOffsets( int before_cr, int after_cr )
346 {
347     before_cr_offset_ = before_cr;
348     after_cr_offset_ = after_cr;
349 }
350 
351 QString LogData::doGetLineString( qint64 line ) const
352 {
353     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
354 
355     fileMutex_.lock();
356 
357     // end_byte is non-inclusive.(is not read)
358     const qint64 first_byte = (line == 0) ?
359         0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
360     const qint64 end_byte  = endOfLinePosition( line );
361 
362     attached_file_->seek( first_byte );
363 
364     QString string = codec_->toUnicode( attached_file_->read( end_byte - first_byte ) );
365 
366     fileMutex_.unlock();
367 
368     return string;
369 }
370 
371 QString LogData::doGetExpandedLineString( qint64 line ) const
372 {
373     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
374 
375     fileMutex_.lock();
376 
377     // end_byte is non-inclusive.(is not read) We also exclude the final \r.
378     const qint64 first_byte = (line == 0) ?
379         0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
380     const qint64 end_byte  = endOfLinePosition( line );
381 
382     attached_file_->seek( first_byte );
383 
384     // LOG(logDEBUG) << "LogData::doGetExpandedLineString first_byte:" << first_byte << " end_byte:" << end_byte;
385     QByteArray rawString = attached_file_->read( end_byte - first_byte );
386 
387     fileMutex_.unlock();
388 
389     QString string = untabify( codec_->toUnicode( rawString ) );
390 
391     // LOG(logDEBUG) << "doGetExpandedLineString Line is: " << string.toStdString();
392 
393     return string;
394 }
395 
396 // Note this function is also called from the LogFilteredDataWorker thread, so
397 // data must be protected because they are changed in the main thread (by
398 // indexingFinished).
399 QStringList LogData::doGetLines( qint64 first_line, int number ) const
400 {
401     QStringList list;
402     const qint64 last_line = first_line + number - 1;
403 
404     // LOG(logDEBUG) << "LogData::doGetLines first_line:" << first_line << " nb:" << number;
405 
406     if ( number == 0 ) {
407         return QStringList();
408     }
409 
410     if ( last_line >= indexing_data_.getNbLines() ) {
411         LOG(logWARNING) << "LogData::doGetLines Lines out of bound asked for";
412         return QStringList(); /* exception? */
413     }
414 
415     fileMutex_.lock();
416 
417     const qint64 first_byte = (first_line == 0) ?
418         0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
419     const qint64 end_byte  = endOfLinePosition( last_line );
420     // LOG(logDEBUG) << "LogData::doGetLines first_byte:" << first_byte << " end_byte:" << end_byte;
421     attached_file_->seek( first_byte );
422     QByteArray blob = attached_file_->read( end_byte - first_byte );
423 
424     fileMutex_.unlock();
425 
426     qint64 beginning = 0;
427     qint64 end = 0;
428     for ( qint64 line = first_line; (line <= last_line); line++ ) {
429         end = endOfLinePosition( line ) - first_byte;
430         // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
431         QByteArray this_line = blob.mid( beginning, end - beginning );
432         // LOG(logDEBUG) << "Line is: " << QString( this_line ).toStdString();
433         list.append( codec_->toUnicode( this_line ) );
434         beginning = beginningOfNextLine( end );
435     }
436 
437     return list;
438 }
439 
440 QStringList LogData::doGetExpandedLines( qint64 first_line, int number ) const
441 {
442     QStringList list;
443     const qint64 last_line = first_line + number - 1;
444 
445     if ( number == 0 ) {
446         return QStringList();
447     }
448 
449     if ( last_line >= indexing_data_.getNbLines() ) {
450         LOG(logWARNING) << "LogData::doGetExpandedLines Lines out of bound asked for";
451         return QStringList(); /* exception? */
452     }
453 
454     fileMutex_.lock();
455 
456     // end_byte is non-inclusive.(is not read)
457     const qint64 first_byte = (first_line == 0) ?
458         0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
459     const qint64 end_byte  = endOfLinePosition( last_line );
460     LOG(logDEBUG) << "LogData::doGetExpandedLines first_byte:" << first_byte << " end_byte:" << end_byte;
461 
462     attached_file_->seek( first_byte );
463     QByteArray blob = attached_file_->read( end_byte - first_byte );
464 
465     fileMutex_.unlock();
466 
467     qint64 beginning = 0;
468     qint64 end = 0;
469     for ( qint64 line = first_line; (line <= last_line); line++ ) {
470         // end is non-inclusive
471         // LOG(logDEBUG) << "EoL " << line << ": " << indexing_data_.getPosForLine( line );
472         end = endOfLinePosition( line ) - first_byte;
473         // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
474         QByteArray this_line = blob.mid( beginning, end - beginning );
475         QString conv_line = codec_->toUnicode( this_line );
476         // LOG(logDEBUG) << "Line is: " << conv_line.toStdString();
477         list.append( untabify( conv_line ) );
478         beginning = beginningOfNextLine( end );
479     }
480 
481     return list;
482 }
483 
484 EncodingSpeculator::Encoding LogData::getDetectedEncoding() const
485 {
486     return indexing_data_.getEncodingGuess();
487 }
488 
489 // Given a line number, returns the position (offset in file) of
490 // the byte immediately past its end.
491 // e.g. in utf-16: T e s t \n2 n d l i n e \n
492 //                 --------------------------
493 //                           ^
494 //                   endOfLinePosition( 0 )
495 qint64 LogData::endOfLinePosition( qint64 line ) const
496 {
497     return indexing_data_.getPosForLine( line ) - 1 - before_cr_offset_;
498 }
499 
500 // Given the position (offset in file) of the end of a line, returns
501 // the position of the beginning of the following, taking into account
502 // encoding and newline signalling.
503 qint64 LogData::beginningOfNextLine( qint64 end_pos ) const
504 {
505     return end_pos + 1 + before_cr_offset_ + after_cr_offset_;
506 }
507