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
doStart(LogDataWorkerThread & workerThread) const40 void LogData::AttachOperation::doStart(
41 LogDataWorkerThread& workerThread ) const
42 {
43 LOG(logDEBUG) << "Attaching " << filename_.toStdString();
44 workerThread.attachFile( filename_ );
45 workerThread.indexAll();
46 }
47
doStart(LogDataWorkerThread & workerThread) const48 void LogData::FullIndexOperation::doStart(
49 LogDataWorkerThread& workerThread ) const
50 {
51 LOG(logDEBUG) << "Reindexing (full)";
52 workerThread.indexAll();
53 }
54
doStart(LogDataWorkerThread & workerThread) const55 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.
LogData()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
~LogData()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
attachFile(const QString & fileName)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
interruptLoading()124 void LogData::interruptLoading()
125 {
126 workerThread_.interrupt();
127 }
128
getFileSize() const129 qint64 LogData::getFileSize() const
130 {
131 return indexing_data_.getSize();
132 }
133
getLastModifiedDate() const134 QDateTime LogData::getLastModifiedDate() const
135 {
136 return lastModifiedDate_;
137 }
138
139 // Return an initialised LogFilteredData. The search is not started.
getNewFilteredData() const140 LogFilteredData* LogData::getNewFilteredData() const
141 {
142 LogFilteredData* newFilteredData = new LogFilteredData( this );
143
144 return newFilteredData;
145 }
146
reload()147 void LogData::reload()
148 {
149 workerThread_.interrupt();
150
151 // Re-open the file, useful in case the file has been moved
152 reOpenFile();
153
154 enqueueOperation( std::make_shared<FullIndexOperation>() );
155 }
156
setPollingInterval(uint32_t interval_ms)157 void LogData::setPollingInterval( uint32_t interval_ms )
158 {
159 fileWatcher_->setPollingInterval( interval_ms );
160 }
161
162 //
163 // Private functions
164 //
165
166 // Add an operation to the queue and perform it immediately if
167 // there is none ongoing.
enqueueOperation(std::shared_ptr<const LogDataOperation> new_operation)168 void LogData::enqueueOperation( std::shared_ptr<const LogDataOperation> new_operation )
169 {
170 if ( currentOperation_ == nullptr )
171 {
172 // We do it immediately
173 currentOperation_ = new_operation;
174 startOperation();
175 }
176 else
177 {
178 // An operation is in progress...
179 // ... we schedule the attach op for later
180 nextOperation_ = new_operation;
181 }
182 }
183
184 // Performs the current operation asynchronously, a indexingFinished
185 // signal will be received when it's finished.
startOperation()186 void LogData::startOperation()
187 {
188 if ( currentOperation_ )
189 {
190 LOG(logDEBUG) << "startOperation found something to do.";
191
192 // Let the operation do its stuff
193 currentOperation_->start( workerThread_ );
194 }
195 }
196
197 //
198 // Slots
199 //
200
fileChangedOnDisk()201 void LogData::fileChangedOnDisk()
202 {
203 const QString name = attached_file_->fileName();
204
205 LOG(logDEBUG) << "signalFileChanged: " << name.toStdString();
206
207 QFileInfo info( name );
208 qint64 file_size = indexing_data_.getSize();
209 LOG(logDEBUG) << "current indexed fileSize=" << file_size;
210 LOG(logDEBUG) << "info file_->size()=" << info.size();
211 LOG(logDEBUG) << "attached_file_->size()=" << attached_file_->size();
212 // In absence of any clearer information, we use the following size comparison
213 // to determine whether we are following the same file or not (i.e. the file
214 // has been moved and the inode we are following is now under a new name, if for
215 // instance log has been rotated). We want to follow the name so we have to reopen
216 // the file to ensure we are reading the right one.
217 // This is a crude heuristic but necessary for notification services that do not
218 // give details (e.g. kqueues)
219 if ( ( info.size() != attached_file_->size() )
220 || ( attached_file_->openMode() == QIODevice::NotOpen ) ) {
221 LOG(logINFO) << "Inconsistent size, the file might have changed, re-opening";
222 reOpenFile();
223
224 // We don't force a (slow) full reindex as this routinely happens if
225 // the file is appended quickly.
226 // This means we can occasionally have false negatives (should be dealt with at
227 // a lower level): e.g. if a new file is created with the same name as the old one
228 // and with a size greater than the old one (should be rare in practice).
229 }
230
231 std::shared_ptr<LogDataOperation> newOperation;
232
233 qint64 real_file_size = attached_file_->size();
234 if ( real_file_size < file_size ) {
235 fileChangedOnDisk_ = Truncated;
236 LOG(logINFO) << "File truncated";
237 newOperation = std::make_shared<FullIndexOperation>();
238 }
239 else if ( real_file_size == file_size ) {
240 LOG(logINFO) << "No change in file";
241 }
242 else if ( fileChangedOnDisk_ != DataAdded ) {
243 fileChangedOnDisk_ = DataAdded;
244 LOG(logINFO) << "New data on disk";
245 newOperation = std::make_shared<PartialIndexOperation>();
246 }
247
248 if ( newOperation ) {
249 enqueueOperation( newOperation );
250 lastModifiedDate_ = info.lastModified();
251
252 emit fileChanged( fileChangedOnDisk_ );
253 }
254 }
255
indexingFinished(LoadingStatus status)256 void LogData::indexingFinished( LoadingStatus status )
257 {
258 LOG(logDEBUG) << "indexingFinished: " <<
259 ( status == LoadingStatus::Successful ) <<
260 ", found " << indexing_data_.getNbLines() << " lines.";
261
262 if ( status == LoadingStatus::Successful ) {
263 // Start watching we watch the file for updates
264 fileChangedOnDisk_ = Unchanged;
265 fileWatcher_->addFile( attached_file_->fileName() );
266
267 // Update the modified date/time if the file exists
268 lastModifiedDate_ = QDateTime();
269 QFileInfo fileInfo( *attached_file_ );
270 if ( fileInfo.exists() )
271 lastModifiedDate_ = fileInfo.lastModified();
272 }
273
274 // FIXME be cleverer here as a notification might have arrived whilst we
275 // were indexing.
276 fileChangedOnDisk_ = Unchanged;
277
278 LOG(logDEBUG) << "Sending indexingFinished.";
279 emit loadingFinished( status );
280
281 // So now the operation is done, let's see if there is something
282 // else to do, in which case, do it!
283 assert( currentOperation_ );
284
285 currentOperation_ = std::move( nextOperation_ );
286 nextOperation_.reset();
287
288 if ( currentOperation_ ) {
289 LOG(logDEBUG) << "indexingFinished is performing the next operation";
290 startOperation();
291 }
292 }
293
294 //
295 // Implementation of virtual functions
296 //
doGetNbLine() const297 qint64 LogData::doGetNbLine() const
298 {
299 return indexing_data_.getNbLines();
300 }
301
doGetMaxLength() const302 int LogData::doGetMaxLength() const
303 {
304 return indexing_data_.getMaxLength();
305 }
306
doGetLineLength(qint64 line) const307 int LogData::doGetLineLength( qint64 line ) const
308 {
309 if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
310
311 int length = doGetExpandedLineString( line ).length();
312
313 return length;
314 }
315
doSetDisplayEncoding(Encoding encoding)316 void LogData::doSetDisplayEncoding( Encoding encoding )
317 {
318 LOG(logDEBUG) << "AbstractLogData::setDisplayEncoding: " << static_cast<int>( encoding );
319
320 static const char* latin1_encoding = "iso-8859-1";
321 static const char* utf8_encoding = "utf-8";
322 static const char* utf16le_encoding = "utf-16le";
323 static const char* utf16be_encoding = "utf-16be";
324 static const char* cp1251_encoding = "CP1251";
325 static const char* cp1252_encoding = "CP1252";
326 static const char* big5_encoding = "Big5";
327 static const char* gb18030_encoding = "GB18030";
328 static const char* shiftJIS_encoding = "Shift-JIS";
329 static const char* koi8r_encoding = "KOI8-R";
330
331 const char* qt_encoding = latin1_encoding;
332
333 // Default to 0, for 8bit encodings
334 int before_cr = 0;
335 int after_cr = 0;
336
337 switch ( encoding ) {
338 case Encoding::ENCODING_UTF8:
339 qt_encoding = utf8_encoding;
340 break;
341 case Encoding::ENCODING_UTF16LE:
342 qt_encoding = utf16le_encoding;
343 before_cr = 0;
344 after_cr = 1;
345 break;
346 case Encoding::ENCODING_UTF16BE:
347 qt_encoding = utf16be_encoding;
348 before_cr = 1;
349 after_cr = 0;
350 break;
351 case Encoding::ENCODING_CP1251:
352 qt_encoding = cp1251_encoding;
353 break;
354 case Encoding::ENCODING_CP1252:
355 qt_encoding = cp1252_encoding;
356 break;
357 case Encoding::ENCODING_BIG5:
358 qt_encoding = big5_encoding;
359 break;
360 case Encoding::ENCODING_GB18030:
361 qt_encoding = gb18030_encoding;
362 break;
363 case Encoding::ENCODING_SHIFT_JIS:
364 qt_encoding = shiftJIS_encoding;
365 break;
366 case Encoding::ENCODING_KOI8R:
367 qt_encoding = koi8r_encoding;
368 break;
369 case Encoding::ENCODING_ISO_8859_1:
370 qt_encoding = latin1_encoding;
371 break;
372 default:
373 LOG( logERROR ) << "Unknown encoding set!";
374 assert( false );
375 break;
376 }
377
378 doSetMultibyteEncodingOffsets( before_cr, after_cr );
379 codec_ = QTextCodec::codecForName( qt_encoding );
380 }
381
doSetMultibyteEncodingOffsets(int before_cr,int after_cr)382 void LogData::doSetMultibyteEncodingOffsets( int before_cr, int after_cr )
383 {
384 before_cr_offset_ = before_cr;
385 after_cr_offset_ = after_cr;
386 }
387
doGetLineString(qint64 line) const388 QString LogData::doGetLineString( qint64 line ) const
389 {
390 if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
391
392 fileMutex_.lock();
393
394 // end_byte is non-inclusive.(is not read)
395 const qint64 first_byte = (line == 0) ?
396 0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
397 const qint64 end_byte = endOfLinePosition( line );
398
399 attached_file_->seek( first_byte );
400
401 QString string = codec_->toUnicode( attached_file_->read( end_byte - first_byte ) );
402
403 fileMutex_.unlock();
404
405 return string;
406 }
407
doGetExpandedLineString(qint64 line) const408 QString LogData::doGetExpandedLineString( qint64 line ) const
409 {
410 if ( line >= indexing_data_.getNbLines() ) { return 0; /* exception? */ }
411
412 fileMutex_.lock();
413
414 // end_byte is non-inclusive.(is not read) We also exclude the final \r.
415 const qint64 first_byte = (line == 0) ?
416 0 : ( indexing_data_.getPosForLine( line-1 ) + after_cr_offset_ );
417 const qint64 end_byte = endOfLinePosition( line );
418
419 attached_file_->seek( first_byte );
420
421 // LOG(logDEBUG) << "LogData::doGetExpandedLineString first_byte:" << first_byte << " end_byte:" << end_byte;
422 QByteArray rawString = attached_file_->read( end_byte - first_byte );
423
424 fileMutex_.unlock();
425
426 QString string = untabify( codec_->toUnicode( rawString ) );
427
428 // LOG(logDEBUG) << "doGetExpandedLineString Line is: " << string.toStdString();
429
430 return string;
431 }
432
433 // Note this function is also called from the LogFilteredDataWorker thread, so
434 // data must be protected because they are changed in the main thread (by
435 // indexingFinished).
doGetLines(qint64 first_line,int number) const436 QStringList LogData::doGetLines( qint64 first_line, int number ) const
437 {
438 QStringList list;
439 const qint64 last_line = first_line + number - 1;
440
441 // LOG(logDEBUG) << "LogData::doGetLines first_line:" << first_line << " nb:" << number;
442
443 if ( number == 0 ) {
444 return QStringList();
445 }
446
447 if ( last_line >= indexing_data_.getNbLines() ) {
448 LOG(logWARNING) << "LogData::doGetLines Lines out of bound asked for";
449 return QStringList(); /* exception? */
450 }
451
452 fileMutex_.lock();
453
454 const qint64 first_byte = (first_line == 0) ?
455 0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
456 const qint64 end_byte = endOfLinePosition( last_line );
457 // LOG(logDEBUG) << "LogData::doGetLines first_byte:" << first_byte << " end_byte:" << end_byte;
458 attached_file_->seek( first_byte );
459 QByteArray blob = attached_file_->read( end_byte - first_byte );
460
461 fileMutex_.unlock();
462
463 qint64 beginning = 0;
464 qint64 end = 0;
465 for ( qint64 line = first_line; (line <= last_line); line++ ) {
466 end = endOfLinePosition( line ) - first_byte;
467 // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
468 QByteArray this_line = blob.mid( beginning, end - beginning );
469 // LOG(logDEBUG) << "Line is: " << QString( this_line ).toStdString();
470 list.append( codec_->toUnicode( this_line ) );
471 beginning = beginningOfNextLine( end );
472 }
473
474 return list;
475 }
476
doGetExpandedLines(qint64 first_line,int number) const477 QStringList LogData::doGetExpandedLines( qint64 first_line, int number ) const
478 {
479 QStringList list;
480 const qint64 last_line = first_line + number - 1;
481
482 if ( number == 0 ) {
483 return QStringList();
484 }
485
486 if ( last_line >= indexing_data_.getNbLines() ) {
487 LOG(logWARNING) << "LogData::doGetExpandedLines Lines out of bound asked for";
488 return QStringList(); /* exception? */
489 }
490
491 fileMutex_.lock();
492
493 // end_byte is non-inclusive.(is not read)
494 const qint64 first_byte = (first_line == 0) ?
495 0 : ( indexing_data_.getPosForLine( first_line-1 ) + after_cr_offset_ );
496 const qint64 end_byte = endOfLinePosition( last_line );
497 LOG(logDEBUG) << "LogData::doGetExpandedLines first_byte:" << first_byte << " end_byte:" << end_byte;
498
499 attached_file_->seek( first_byte );
500 QByteArray blob = attached_file_->read( end_byte - first_byte );
501
502 fileMutex_.unlock();
503
504 qint64 beginning = 0;
505 qint64 end = 0;
506 for ( qint64 line = first_line; (line <= last_line); line++ ) {
507 // end is non-inclusive
508 // LOG(logDEBUG) << "EoL " << line << ": " << indexing_data_.getPosForLine( line );
509 end = endOfLinePosition( line ) - first_byte;
510 // LOG(logDEBUG) << "Getting line " << line << " beginning " << beginning << " end " << end;
511 QByteArray this_line = blob.mid( beginning, end - beginning );
512 QString conv_line = codec_->toUnicode( this_line );
513 // LOG(logDEBUG) << "Line is: " << conv_line.toStdString();
514 list.append( untabify( conv_line ) );
515 beginning = beginningOfNextLine( end );
516 }
517
518 return list;
519 }
520
getDetectedEncoding() const521 EncodingSpeculator::Encoding LogData::getDetectedEncoding() const
522 {
523 return indexing_data_.getEncodingGuess();
524 }
525
526 // Given a line number, returns the position (offset in file) of
527 // the byte immediately past its end.
528 // e.g. in utf-16: T e s t \n2 n d l i n e \n
529 // --------------------------
530 // ^
531 // endOfLinePosition( 0 )
endOfLinePosition(qint64 line) const532 qint64 LogData::endOfLinePosition( qint64 line ) const
533 {
534 return indexing_data_.getPosForLine( line ) - 1 - before_cr_offset_;
535 }
536
537 // Given the position (offset in file) of the end of a line, returns
538 // the position of the beginning of the following, taking into account
539 // encoding and newline signalling.
beginningOfNextLine(qint64 end_pos) const540 qint64 LogData::beginningOfNextLine( qint64 end_pos ) const
541 {
542 return end_pos + 1 + before_cr_offset_ + after_cr_offset_;
543 }
544
545 // Close and reopen the file.
546 // Used if we suspect the file has been moved (we follow the old
547 // inode but really want the one now associated with the name)
reOpenFile()548 void LogData::reOpenFile()
549 {
550 auto reopened = std::make_unique<QFile>( attached_file_->fileName() );
551 reopened->open( QIODevice::ReadOnly );
552 QMutexLocker locker( &fileMutex_ );
553 attached_file_ = std::move( reopened ); // This will close the old one and open the new
554 }
555