1 /*
2     SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 // Own
8 #include "HistoryFile.h"
9 
10 // Konsole
11 #include "KonsoleSettings.h"
12 #include "konsoledebug.h"
13 
14 // System
15 #include <cerrno>
16 #include <unistd.h>
17 
18 // Qt
19 #include <QDir>
20 #include <QUrl>
21 
22 // KDE
23 #include <KConfigGroup>
24 #include <KSharedConfig>
25 
26 using namespace Konsole;
27 
Q_GLOBAL_STATIC(QString,historyFileLocation)28 Q_GLOBAL_STATIC(QString, historyFileLocation)
29 
30 // History File ///////////////////////////////////////////
31 HistoryFile::HistoryFile()
32     : _length(0)
33     , _fileMap(nullptr)
34     , _readWriteBalance(0)
35 {
36     // Determine the temp directory once
37     // This class is called 3 times for each "unlimited" scrollback.
38     // This has the down-side that users must restart to
39     // load changes.
40     if (!historyFileLocation.exists()) {
41         QString fileLocation;
42         KSharedConfigPtr appConfig = KSharedConfig::openConfig();
43         if (qApp->applicationName() != QLatin1String("konsole")) {
44             // Check if "kpart"rc has "FileLocation" group; AFAIK
45             // only possible if user manually added it. If not
46             // found, use konsole's config.
47             if (!appConfig->hasGroup("FileLocation")) {
48                 appConfig = KSharedConfig::openConfig(QStringLiteral("konsolerc"));
49             }
50         }
51 
52         KConfigGroup configGroup = appConfig->group("FileLocation");
53         if (configGroup.readEntry("scrollbackUseCacheLocation", false)) {
54             fileLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
55         } else if (configGroup.readEntry("scrollbackUseSpecifiedLocation", false)) {
56             const QUrl specifiedUrl = KonsoleSettings::scrollbackUseSpecifiedLocationDirectory();
57             fileLocation = specifiedUrl.path();
58         } else {
59             fileLocation = QDir::tempPath();
60         }
61         // Validate file location
62         const QFileInfo fi(fileLocation);
63         if (fileLocation.isEmpty() || !fi.exists() || !fi.isDir() || !fi.isWritable()) {
64             qCWarning(KonsoleDebug) << "Invalid scrollback folder " << fileLocation << "; using "
65                                     << QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
66             // Per Qt docs, this path is never empty; not sure if that
67             // means it always exists.
68             fileLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
69             const QFileInfo fi2(fileLocation);
70             if (!fi2.exists()) {
71                 if (!QDir().mkpath(fileLocation)) {
72                     qCWarning(KonsoleDebug) << "Unable to create scrollback folder " << fileLocation;
73                 }
74             }
75         }
76         *historyFileLocation() = fileLocation;
77     }
78     const QString tmpDir = *historyFileLocation();
79     const QString tmpFormat = tmpDir + QLatin1Char('/') + QLatin1String("konsole-XXXXXX.history");
80     _tmpFile.setFileTemplate(tmpFormat);
81     if (_tmpFile.open()) {
82 #if defined(Q_OS_LINUX)
83         qCDebug(KonsoleDebug, "HistoryFile: /proc/%lld/fd/%d", qApp->applicationPid(), _tmpFile.handle());
84 #endif
85         // On some systems QTemporaryFile creates unnamed file.
86         // Do not interfere in such cases.
87         if (_tmpFile.exists()) {
88             // Remove file entry from filesystem. Since the file
89             // is opened, it will still be available for reading
90             // and writing. This guarantees the file won't remain
91             // in filesystem after process termination, even when
92             // there was a crash.
93             unlink(QFile::encodeName(_tmpFile.fileName()).constData());
94         }
95     }
96 }
97 
~HistoryFile()98 HistoryFile::~HistoryFile()
99 {
100     if (_fileMap != nullptr) {
101         unmap();
102     }
103 }
104 
105 // TODO:  Mapping the entire file in will cause problems if the history file becomes exceedingly large,
106 //(ie. larger than available memory).  HistoryFile::map() should only map in sections of the file at a time,
107 // to avoid this.
map()108 void HistoryFile::map()
109 {
110     Q_ASSERT(_fileMap == nullptr);
111 
112     if (_tmpFile.flush()) {
113         Q_ASSERT(_tmpFile.size() >= _length);
114         _fileMap = _tmpFile.map(0, _length);
115     }
116 
117     // if mmap'ing fails, fall back to the read-lseek combination
118     if (_fileMap == nullptr) {
119         _readWriteBalance = 0;
120         qCDebug(KonsoleDebug) << "mmap'ing history failed.  errno = " << errno;
121     }
122 }
123 
unmap()124 void HistoryFile::unmap()
125 {
126     Q_ASSERT(_fileMap != nullptr);
127 
128     if (_tmpFile.unmap(_fileMap)) {
129         _fileMap = nullptr;
130     }
131 
132     Q_ASSERT(_fileMap == nullptr);
133 }
134 
add(const char * buffer,qint64 count)135 void HistoryFile::add(const char *buffer, qint64 count)
136 {
137     if (_fileMap != nullptr) {
138         unmap();
139     }
140 
141     if (_readWriteBalance < INT_MAX) {
142         _readWriteBalance++;
143     }
144 
145     qint64 rc = 0;
146 
147     if (!_tmpFile.seek(_length)) {
148         perror("HistoryFile::add.seek");
149         return;
150     }
151     rc = _tmpFile.write(buffer, count);
152     if (rc < 0) {
153         perror("HistoryFile::add.write");
154         return;
155     }
156     _length += rc;
157 }
158 
get(char * buffer,qint64 size,qint64 loc)159 void HistoryFile::get(char *buffer, qint64 size, qint64 loc)
160 {
161     if (loc < 0 || size < 0 || loc + size > _length) {
162         fprintf(stderr, "getHist(...,%lld,%lld): invalid args.\n", size, loc);
163         return;
164     }
165 
166     // count number of get() calls vs. number of add() calls.
167     // If there are many more get() calls compared with add()
168     // calls (decided by using MAP_THRESHOLD) then mmap the log
169     // file to improve performance.
170     if (_readWriteBalance > INT_MIN) {
171         _readWriteBalance--;
172     }
173     if ((_fileMap == nullptr) && _readWriteBalance < MAP_THRESHOLD) {
174         map();
175     }
176 
177     if (_fileMap != nullptr) {
178         memcpy(buffer, _fileMap + loc, size);
179     } else {
180         qint64 rc = 0;
181 
182         if (!_tmpFile.seek(loc)) {
183             perror("HistoryFile::get.seek");
184             return;
185         }
186         rc = _tmpFile.read(buffer, size);
187         if (rc < 0) {
188             perror("HistoryFile::get.read");
189             return;
190         }
191     }
192 }
193 
removeLast(qint64 loc)194 void HistoryFile::removeLast(qint64 loc)
195 {
196     if (loc < 0 || loc > _length) {
197         fprintf(stderr, "removeLast(%lld): invalid args.\n", loc);
198         return;
199     }
200     _length = loc;
201 }
202 
len() const203 qint64 HistoryFile::len() const
204 {
205     return _length;
206 }
207