xref: /glogg/src/data/logdata.cpp (revision f869e41d2c129cd0f2f3eccb5e9d0d80a5998201)
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 ( fileChangedOnDisk_ != DataAdded ) {
219         fileChangedOnDisk_ = DataAdded;
220         LOG(logINFO) << "New data on disk";
221         newOperation = std::make_shared<PartialIndexOperation>();
222     }
223 
224     if ( newOperation )
225         enqueueOperation( newOperation );
226 
227     lastModifiedDate_ = info.lastModified();
228 
229     emit fileChanged( fileChangedOnDisk_ );
230     // TODO: fileChangedOnDisk_, fileSize_
231 }
232 
233 void LogData::indexingFinished( LoadingStatus status )
234 {
235     LOG(logDEBUG) << "indexingFinished: " <<
236         ( status == LoadingStatus::Successful ) <<
237         ", found " << indexing_data_.getNbLines() << " lines.";
238 
239     if ( status == LoadingStatus::Successful ) {
240         // Start watching we watch the file for updates
241         fileChangedOnDisk_ = Unchanged;
242         fileWatcher_->addFile( attached_file_->fileName() );
243 
244         // Update the modified date/time if the file exists
245         lastModifiedDate_ = QDateTime();
246         QFileInfo fileInfo( *attached_file_ );
247         if ( fileInfo.exists() )
248             lastModifiedDate_ = fileInfo.lastModified();
249     }
250 
251     // FIXME be cleverer here as a notification might have arrived whilst we
252     // were indexing.
253     fileChangedOnDisk_ = Unchanged;
254 
255     LOG(logDEBUG) << "Sending indexingFinished.";
256     emit loadingFinished( status );
257 
258     // So now the operation is done, let's see if there is something
259     // else to do, in which case, do it!
260     assert( currentOperation_ );
261 
262     currentOperation_ = std::move( nextOperation_ );
263     nextOperation_.reset();
264 
265     if ( currentOperation_ ) {
266         LOG(logDEBUG) << "indexingFinished is performing the next operation";
267         startOperation();
268     }
269 }
270 
271 //
272 // Implementation of virtual functions
273 //
274 qint64 LogData::doGetNbLine() const
275 {
276     return indexing_data_.getNbLines();
277 }
278 
279 int LogData::doGetMaxLength() const
280 {
281     return indexing_data_.getMaxLength();
282 }
283 
284 int LogData::doGetLineLength( qint64 line ) const
285 {
286     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
287 
288     int length = doGetExpandedLineString( line ).length();
289 
290     return length;
291 }
292 
293 void LogData::doSetDisplayEncoding( Encoding encoding )
294 {
295     LOG(logDEBUG) << "AbstractLogData::setDisplayEncoding: " << static_cast<int>( encoding );
296 
297     static const char* latin1_encoding = "iso-8859-1";
298     static const char* utf8_encoding   = "utf-8";
299     static const char* utf16le_encoding   = "utf-16le";
300     static const char* utf16be_encoding   = "utf-16be";
301     static const char* cp1251_encoding   = "CP1251";
302     static const char* cp1252_encoding   = "CP1252";
303 
304     const char* qt_encoding = latin1_encoding;
305 
306     // Default to 0, for 8bit encodings
307     int before_cr = 0;
308     int after_cr  = 0;
309 
310     switch ( encoding ) {
311         case Encoding::ENCODING_UTF8:
312             qt_encoding = utf8_encoding;
313             break;
314         case Encoding::ENCODING_UTF16LE:
315             qt_encoding = utf16le_encoding;
316             before_cr = 0;
317             after_cr  = 1;
318             break;
319         case Encoding::ENCODING_UTF16BE:
320             qt_encoding = utf16be_encoding;
321             before_cr = 1;
322             after_cr  = 0;
323             break;
324         case Encoding::ENCODING_CP1251:
325             qt_encoding = cp1251_encoding;
326             break;
327         case Encoding::ENCODING_CP1252:
328             qt_encoding = cp1252_encoding;
329             break;
330         case Encoding::ENCODING_ISO_8859_1:
331             qt_encoding = latin1_encoding;
332             break;
333         default:
334             LOG( logERROR ) << "Unknown encoding set!";
335             assert( false );
336             break;
337     }
338 
339     doSetMultibyteEncodingOffsets( before_cr, after_cr );
340     codec_ = QTextCodec::codecForName( qt_encoding );
341 }
342 
343 void LogData::doSetMultibyteEncodingOffsets( int before_cr, int after_cr )
344 {
345     before_cr_offset_ = before_cr;
346     after_cr_offset_ = after_cr;
347 }
348 
349 QString LogData::doGetLineString( qint64 line ) const
350 {
351     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
352 
353     fileMutex_.lock();
354 
355     // end_byte is non-inclusive.(is not read)
356     const qint64 first_byte = (line == 0) ?
357         0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
358     const qint64 end_byte  = endOfLinePosition( line );
359 
360     attached_file_->seek( first_byte );
361 
362     QString string = codec_->toUnicode( attached_file_->read( end_byte - first_byte ) );
363 
364     fileMutex_.unlock();
365 
366     return string;
367 }
368 
369 QString LogData::doGetExpandedLineString( qint64 line ) const
370 {
371     if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
372 
373     fileMutex_.lock();
374 
375     // end_byte is non-inclusive.(is not read) We also exclude the final \r.
376     const qint64 first_byte = (line == 0) ?
377         0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
378     const qint64 end_byte  = endOfLinePosition( line );
379 
380     attached_file_->seek( first_byte );
381 
382     // LOG(logDEBUG) << "LogData::doGetExpandedLineString first_byte:" << first_byte << " end_byte:" << end_byte;
383     QByteArray rawString = attached_file_->read( end_byte - first_byte );
384 
385     fileMutex_.unlock();
386 
387     QString string = untabify( codec_->toUnicode( rawString ) );
388 
389     // LOG(logDEBUG) << "doGetExpandedLineString Line is: " << string.toStdString();
390 
391     return string;
392 }
393 
394 // Note this function is also called from the LogFilteredDataWorker thread, so
395 // data must be protected because they are changed in the main thread (by
396 // indexingFinished).
397 QStringList LogData::doGetLines( qint64 first_line, int number ) const
398 {
399     QStringList list;
400     const qint64 last_line = first_line + number - 1;
401 
402     // LOG(logDEBUG) << "LogData::doGetLines first_line:" << first_line << " nb:" << number;
403 
404     if ( number == 0 ) {
405         return QStringList();
406     }
407 
408     if ( last_line >= indexing_data_.getNbLines() ) {
409         LOG(logWARNING) << "LogData::doGetLines Lines out of bound asked for";
410         return QStringList(); /* exception? */
411     }
412 
413     fileMutex_.lock();
414 
415     const qint64 first_byte = (first_line == 0) ?
416         0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
417     const qint64 end_byte  = endOfLinePosition( last_line );
418     // LOG(logDEBUG) << "LogData::doGetLines first_byte:" << first_byte << " end_byte:" << end_byte;
419     attached_file_->seek( first_byte );
420     QByteArray blob = attached_file_->read( end_byte - first_byte );
421 
422     fileMutex_.unlock();
423 
424     qint64 beginning = 0;
425     qint64 end = 0;
426     for ( qint64 line = first_line; (line <= last_line); line++ ) {
427         end = endOfLinePosition( line ) - first_byte;
428         // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
429         QByteArray this_line = blob.mid( beginning, end - beginning );
430         // LOG(logDEBUG) << "Line is: " << QString( this_line ).toStdString();
431         list.append( codec_->toUnicode( this_line ) );
432         beginning = beginningOfNextLine( end );
433     }
434 
435     return list;
436 }
437 
438 QStringList LogData::doGetExpandedLines( qint64 first_line, int number ) const
439 {
440     QStringList list;
441     const qint64 last_line = first_line + number - 1;
442 
443     if ( number == 0 ) {
444         return QStringList();
445     }
446 
447     if ( last_line >= indexing_data_.getNbLines() ) {
448         LOG(logWARNING) << "LogData::doGetExpandedLines Lines out of bound asked for";
449         return QStringList(); /* exception? */
450     }
451 
452     fileMutex_.lock();
453 
454     // end_byte is non-inclusive.(is not read)
455     const qint64 first_byte = (first_line == 0) ?
456         0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
457     const qint64 end_byte  = endOfLinePosition( last_line );
458     LOG(logDEBUG) << "LogData::doGetExpandedLines first_byte:" << first_byte << " end_byte:" << end_byte;
459 
460     attached_file_->seek( first_byte );
461     QByteArray blob = attached_file_->read( end_byte - first_byte );
462 
463     fileMutex_.unlock();
464 
465     qint64 beginning = 0;
466     qint64 end = 0;
467     for ( qint64 line = first_line; (line <= last_line); line++ ) {
468         // end is non-inclusive
469         // LOG(logDEBUG) << "EoL " << line << ": " << indexing_data_.getPosForLine( line );
470         end = endOfLinePosition( line ) - first_byte;
471         // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
472         QByteArray this_line = blob.mid( beginning, end - beginning );
473         QString conv_line = codec_->toUnicode( this_line );
474         // LOG(logDEBUG) << "Line is: " << conv_line.toStdString();
475         list.append( untabify( conv_line ) );
476         beginning = beginningOfNextLine( end );
477     }
478 
479     return list;
480 }
481 
482 EncodingSpeculator::Encoding LogData::getDetectedEncoding() const
483 {
484     return indexing_data_.getEncodingGuess();
485 }
486 
487 // Given a line number, returns the position (offset in file) of
488 // the byte immediately past its end.
489 // e.g. in utf-16: T e s t \n2 n d l i n e \n
490 //                 --------------------------
491 //                           ^
492 //                   endOfLinePosition( 0 )
493 qint64 LogData::endOfLinePosition( qint64 line ) const
494 {
495     return indexing_data_.getPosForLine( line ) - 1 - before_cr_offset_;
496 }
497 
498 // Given the position (offset in file) of the end of a line, returns
499 // the position of the beginning of the following, taking into account
500 // encoding and newline signalling.
501 qint64 LogData::beginningOfNextLine( qint64 end_pos ) const
502 {
503     return end_pos + 1 + before_cr_offset_ + after_cr_offset_;
504 }
505