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