tdesktop/Telegram/SourceFiles/logs.cpp

581 lines
17 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#include "logs.h"
#include "platform/platform_specific.h"
#include "mtproto/connection.h"
#include "core/crash_reports.h"
#include "core/launcher.h"
enum LogDataType {
LogDataMain,
LogDataDebug,
LogDataTcp,
LogDataMtp,
LogDataCount
};
QMutex *_logsMutex(LogDataType type, bool clear = false) {
static QMutex *LogsMutexes = 0;
if (clear) {
delete[] LogsMutexes;
LogsMutexes = 0;
} else if (!LogsMutexes) {
LogsMutexes = new QMutex[LogDataCount];
}
return &LogsMutexes[type];
}
QString _logsFilePath(LogDataType type, const QString &postfix = QString()) {
QString path(cWorkingDir());
switch (type) {
case LogDataMain: path += qstr("log") + postfix + qstr(".txt"); break;
case LogDataDebug: path += qstr("DebugLogs/log") + postfix + qstr(".txt"); break;
case LogDataTcp: path += qstr("DebugLogs/tcp") + postfix + qstr(".txt"); break;
case LogDataMtp: path += qstr("DebugLogs/mtp") + postfix + qstr(".txt"); break;
}
return path;
}
int32 LogsStartIndexChosen = -1;
QString _logsEntryStart() {
static int32 index = 0;
QDateTime tm(QDateTime::currentDateTime());
auto thread = qobject_cast<MTP::internal::Thread*>(QThread::currentThread());
auto threadId = thread ? thread->getThreadIndex() : 0;
return QString("[%1 %2-%3]").arg(tm.toString("hh:mm:ss.zzz")).arg(QString("%1").arg(threadId, 2, 10, QChar('0'))).arg(++index, 7, 10, QChar('0'));
}
class LogsDataFields {
public:
LogsDataFields() {
for (int32 i = 0; i < LogDataCount; ++i) {
files[i].reset(new QFile());
}
}
bool openMain() {
return reopen(LogDataMain, 0, qsl("start"));
}
void closeMain() {
QMutexLocker lock(_logsMutex(LogDataMain));
if (files[LogDataMain]) {
streams[LogDataMain].setDevice(0);
files[LogDataMain]->close();
}
}
bool instanceChecked() {
return reopen(LogDataMain, 0, QString());
}
QString full() {
if (!streams[LogDataMain].device()) {
return QString();
}
QFile out(files[LogDataMain]->fileName());
if (out.open(QIODevice::ReadOnly)) {
return QString::fromUtf8(out.readAll());
}
return QString();
}
void write(LogDataType type, const QString &msg) {
QMutexLocker lock(_logsMutex(type));
if (type != LogDataMain) reopenDebug();
if (!streams[type].device()) return;
streams[type] << msg;
streams[type].flush();
}
private:
std::unique_ptr<QFile> files[LogDataCount];
QTextStream streams[LogDataCount];
int32 part = -1;
bool reopen(LogDataType type, int32 dayIndex, const QString &postfix) {
if (streams[type].device()) {
if (type == LogDataMain) {
if (!postfix.isEmpty()) {
return true;
}
} else {
streams[type].setDevice(0);
files[type]->close();
}
}
auto mode = QIODevice::WriteOnly | QIODevice::Text;
if (type == LogDataMain) { // we can call LOG() in LogDataMain reopen - mutex not locked
if (postfix.isEmpty()) { // instance checked, need to move to log.txt
Assert(!files[type]->fileName().isEmpty()); // one of log_startXX.txt should've been opened already
auto to = std::make_unique<QFile>(_logsFilePath(type, postfix));
if (to->exists() && !to->remove()) {
LOG(("Could not delete '%1' file to start new logging!").arg(to->fileName()));
return false;
}
if (!QFile(files[type]->fileName()).copy(to->fileName())) { // don't close files[type] yet
LOG(("Could not copy '%1' to '%2' to start new logging!").arg(files[type]->fileName()).arg(to->fileName()));
return false;
}
if (to->open(mode | QIODevice::Append)) {
std::swap(files[type], to);
streams[type].setDevice(files[type].get());
streams[type].setCodec("UTF-8");
LOG(("Moved logging from '%1' to '%2'!").arg(to->fileName()).arg(files[type]->fileName()));
to->remove();
LogsStartIndexChosen = -1;
QDir working(cWorkingDir()); // delete all other log_startXX.txt that we can
QStringList oldlogs = working.entryList(QStringList("log_start*.txt"), QDir::Files);
for (QStringList::const_iterator i = oldlogs.cbegin(), e = oldlogs.cend(); i != e; ++i) {
QString oldlog = cWorkingDir() + *i, oldlogend = i->mid(qstr("log_start").size());
if (oldlogend.size() == 1 + qstr(".txt").size() && oldlogend.at(0).isDigit() && oldlogend.midRef(1) == qstr(".txt")) {
bool removed = QFile(*i).remove();
LOG(("Old start log '%1' found, deleted: %2").arg(*i).arg(Logs::b(removed)));
}
}
return true;
}
LOG(("Could not open '%1' file to start new logging!").arg(to->fileName()));
return false;
} else {
bool found = false;
int32 oldest = -1; // find not existing log_startX.txt or pick the oldest one (by lastModified)
QDateTime oldestLastModified;
for (int32 i = 0; i < 10; ++i) {
QString trying = _logsFilePath(type, qsl("_start%1").arg(i));
files[type]->setFileName(trying);
if (!files[type]->exists()) {
LogsStartIndexChosen = i;
found = true;
break;
}
QDateTime lastModified = QFileInfo(trying).lastModified();
if (oldest < 0 || lastModified < oldestLastModified) {
oldestLastModified = lastModified;
oldest = i;
}
}
if (!found) {
files[type]->setFileName(_logsFilePath(type, qsl("_start%1").arg(oldest)));
LogsStartIndexChosen = oldest;
}
}
} else {
files[type]->setFileName(_logsFilePath(type, postfix));
if (files[type]->exists()) {
if (files[type]->open(QIODevice::ReadOnly | QIODevice::Text)) {
if (QString::fromUtf8(files[type]->readLine()).toInt() == dayIndex) {
mode |= QIODevice::Append;
}
files[type]->close();
}
} else {
QDir().mkdir(cWorkingDir() + qstr("DebugLogs"));
}
}
if (files[type]->open(mode)) {
streams[type].setDevice(files[type].get());
streams[type].setCodec("UTF-8");
if (type != LogDataMain) {
streams[type] << ((mode & QIODevice::Append)
? qsl("\
----------------------------------------------------------------\n\
NEW LOGGING INSTANCE STARTED!!!\n\
----------------------------------------------------------------\n")
: qsl("%1\n").arg(dayIndex));
streams[type].flush();
}
return true;
} else if (type != LogDataMain) {
LOG(("Could not open debug log '%1'!").arg(files[type]->fileName()));
}
return false;
}
void reopenDebug() {
time_t t = time(NULL);
struct tm tm;
mylocaltime(&tm, &t);
static const int switchEach = 15; // minutes
int32 newPart = (tm.tm_min + tm.tm_hour * 60) / switchEach;
if (newPart == part) return;
part = newPart;
int32 dayIndex = (tm.tm_year + 1900) * 10000 + (tm.tm_mon + 1) * 100 + tm.tm_mday;
QString postfix = QString("_%4_%5").arg((part * switchEach) / 60, 2, 10, QChar('0')).arg((part * switchEach) % 60, 2, 10, QChar('0'));
reopen(LogDataDebug, dayIndex, postfix);
reopen(LogDataTcp, dayIndex, postfix);
reopen(LogDataMtp, dayIndex, postfix);
}
};
LogsDataFields *LogsData = 0;
typedef QList<QPair<LogDataType, QString> > LogsInMemoryList;
LogsInMemoryList *LogsInMemory = 0;
LogsInMemoryList *DeletedLogsInMemory = SharedMemoryLocation<LogsInMemoryList, 0>();
QString LogsBeforeSingleInstanceChecked; // LogsInMemory already dumped in LogsData, but LogsData is about to be deleted
void _logsWrite(LogDataType type, const QString &msg) {
if (LogsData && (type == LogDataMain || LogsStartIndexChosen < 0)) {
if (type == LogDataMain || cDebug()) {
LogsData->write(type, msg);
}
} else if (LogsInMemory != DeletedLogsInMemory) {
if (!LogsInMemory) {
LogsInMemory = new LogsInMemoryList;
}
LogsInMemory->push_back(qMakePair(type, msg));
} else if (!LogsBeforeSingleInstanceChecked.isEmpty() && type == LogDataMain) {
LogsBeforeSingleInstanceChecked += msg;
}
}
namespace Logs {
namespace {
void MoveOldDataFiles(const QString &wasDir) {
QFile data(wasDir + "data"), dataConfig(wasDir + "data_config"), tdataConfig(wasDir + "tdata/config");
if (data.exists() && dataConfig.exists() && !QFileInfo(cWorkingDir() + "data").exists() && !QFileInfo(cWorkingDir() + "data_config").exists()) { // move to home dir
LOG(("Copying data to home dir '%1' from '%2'").arg(cWorkingDir()).arg(wasDir));
if (data.copy(cWorkingDir() + "data")) {
LOG(("Copied 'data' to home dir"));
if (dataConfig.copy(cWorkingDir() + "data_config")) {
LOG(("Copied 'data_config' to home dir"));
bool tdataGood = true;
if (tdataConfig.exists()) {
tdataGood = false;
QDir().mkpath(cWorkingDir() + "tdata");
if (tdataConfig.copy(cWorkingDir() + "tdata/config")) {
LOG(("Copied 'tdata/config' to home dir"));
tdataGood = true;
} else {
LOG(("Copied 'data' and 'data_config', but could not copy 'tdata/config'!"));
}
}
if (tdataGood) {
if (data.remove()) {
LOG(("Removed 'data'"));
} else {
LOG(("Could not remove 'data'"));
}
if (dataConfig.remove()) {
LOG(("Removed 'data_config'"));
} else {
LOG(("Could not remove 'data_config'"));
}
if (!tdataConfig.exists() || tdataConfig.remove()) {
LOG(("Removed 'tdata/config'"));
} else {
LOG(("Could not remove 'tdata/config'"));
}
QDir().rmdir(wasDir + "tdata");
}
} else {
LOG(("Copied 'data', but could not copy 'data_config'!!"));
}
} else {
LOG(("Could not copy 'data'!"));
}
}
}
} // namespace
void start(not_null<Core::Launcher*> launcher) {
Assert(LogsData == 0);
if (!Sandbox::CheckBetaVersionDir()) {
return;
}
auto initialWorkingDir = QDir(cWorkingDir()).absolutePath() + '/';
auto moveOldDataFrom = QString();
auto workingDirChosen = false;
if (cBetaVersion()) {
cSetDebug(true);
workingDirChosen = true;
#if defined Q_OS_MAC || defined Q_OS_LINUX
} else {
#ifdef _DEBUG
cForceWorkingDir(cExeDir());
#else // _DEBUG
if (!cWorkingDir().isEmpty()) {
// This value must come only from the "-workdir" argument.
cForceWorkingDir(cWorkingDir());
} else {
cForceWorkingDir(psAppDataPath());
}
#endif // !_DEBUG
workingDirChosen = true;
#if defined Q_OS_LINUX && !defined _DEBUG // fix first version
moveOldDataFrom = initialWorkingDir;
#endif // Q_OS_LINUX && !_DEBUG
#elif defined Q_OS_WINRT // Q_OS_MAC || Q_OS_LINUX
} else {
cForceWorkingDir(psAppDataPath());
workingDirChosen = true;
#elif defined OS_WIN_STORE // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT
#ifdef _DEBUG
cForceWorkingDir(cExeDir());
#else // _DEBUG
cForceWorkingDir(psAppDataPath());
#endif // !_DEBUG
workingDirChosen = true;
#elif defined Q_OS_WIN
} else {
if (!cWorkingDir().isEmpty()) {
// This value must come only from the "-workdir" argument.
cForceWorkingDir(cWorkingDir());
workingDirChosen = true;
}
#endif // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT || OS_WIN_STORE
}
LogsData = new LogsDataFields();
if (!workingDirChosen) {
cForceWorkingDir(cExeDir());
if (!LogsData->openMain()) {
cForceWorkingDir(psAppDataPath());
}
}
cForceWorkingDir(QDir(cWorkingDir()).absolutePath() + '/');
// WinRT build requires the working dir to stay the same for plugin loading.
#ifndef Q_OS_WINRT
QDir().setCurrent(cWorkingDir());
#endif // !Q_OS_WINRT
QDir().mkpath(cWorkingDir() + qstr("tdata"));
Sandbox::WorkingDirReady();
CrashReports::StartCatching();
if (!LogsData->openMain()) {
delete LogsData;
LogsData = 0;
}
LOG(("Launched version: %1, alpha: %2, beta: %3, debug mode: %4, test dc: %5").arg(AppVersion).arg(Logs::b(cAlphaVersion())).arg(cBetaVersion()).arg(Logs::b(cDebug())).arg(Logs::b(cTestMode())));
LOG(("Executable dir: %1, name: %2").arg(cExeDir()).arg(cExeName()));
LOG(("Initial working dir: %1").arg(initialWorkingDir));
LOG(("Working dir: %1").arg(cWorkingDir()));
LOG(("Command line: %1").arg(launcher->argumentsString()));
if (!LogsData) {
LOG(("FATAL: Could not open '%1' for writing log!").arg(_logsFilePath(LogDataMain, qsl("_startXX"))));
return;
}
#ifdef Q_OS_WIN
if (cWorkingDir() == psAppDataPath()) { // fix old "Telegram Win (Unofficial)" version
moveOldDataFrom = psAppDataPathOld();
}
#endif
if (!moveOldDataFrom.isEmpty()) {
MoveOldDataFiles(moveOldDataFrom);
}
if (LogsInMemory) {
Assert(LogsInMemory != DeletedLogsInMemory);
LogsInMemoryList list = *LogsInMemory;
for (LogsInMemoryList::const_iterator i = list.cbegin(), e = list.cend(); i != e; ++i) {
if (i->first == LogDataMain) {
_logsWrite(i->first, i->second);
}
}
}
LOG(("Logs started"));
}
void finish() {
delete LogsData;
LogsData = 0;
if (LogsInMemory && LogsInMemory != DeletedLogsInMemory) {
delete LogsInMemory;
}
LogsInMemory = DeletedLogsInMemory;
_logsMutex(LogDataMain, true);
CrashReports::FinishCatching();
}
bool started() {
return LogsData != 0;
}
bool instanceChecked() {
if (!LogsData) return false;
if (!LogsData->instanceChecked()) {
LogsBeforeSingleInstanceChecked = Logs::full();
delete LogsData;
LogsData = 0;
LOG(("FATAL: Could not move logging to '%1'!").arg(_logsFilePath(LogDataMain)));
return false;
}
if (LogsInMemory) {
Assert(LogsInMemory != DeletedLogsInMemory);
LogsInMemoryList list = *LogsInMemory;
for (LogsInMemoryList::const_iterator i = list.cbegin(), e = list.cend(); i != e; ++i) {
if (i->first != LogDataMain) {
_logsWrite(i->first, i->second);
}
}
}
if (LogsInMemory) {
Assert(LogsInMemory != DeletedLogsInMemory);
delete LogsInMemory;
}
LogsInMemory = DeletedLogsInMemory;
DEBUG_LOG(("Debug logs started."));
LogsBeforeSingleInstanceChecked.clear();
return true;
}
void multipleInstances() {
if (LogsInMemory) {
Assert(LogsInMemory != DeletedLogsInMemory);
delete LogsInMemory;
}
LogsInMemory = DeletedLogsInMemory;
if (cDebug()) {
LOG(("WARNING: debug logs are not written in multiple instances mode!"));
}
LogsBeforeSingleInstanceChecked.clear();
}
void closeMain() {
LOG(("Explicitly closing main log and finishing crash handlers."));
if (LogsData) {
LogsData->closeMain();
}
}
void writeMain(const QString &v) {
time_t t = time(NULL);
struct tm tm;
mylocaltime(&tm, &t);
QString msg(QString("[%1.%2.%3 %4:%5:%6] %7\n").arg(tm.tm_year + 1900).arg(tm.tm_mon + 1, 2, 10, QChar('0')).arg(tm.tm_mday, 2, 10, QChar('0')).arg(tm.tm_hour, 2, 10, QChar('0')).arg(tm.tm_min, 2, 10, QChar('0')).arg(tm.tm_sec, 2, 10, QChar('0')).arg(v));
_logsWrite(LogDataMain, msg);
QString debugmsg(QString("%1 %2\n").arg(_logsEntryStart()).arg(v));
_logsWrite(LogDataDebug, debugmsg);
}
void writeDebug(const char *file, int32 line, const QString &v) {
const char *last = strstr(file, "/"), *found = 0;
while (last) {
found = last;
last = strstr(last + 1, "/");
}
last = strstr(file, "\\");
while (last) {
found = last;
last = strstr(last + 1, "\\");
}
if (found) {
file = found + 1;
}
QString msg(QString("%1 %2 (%3 : %4)\n").arg(_logsEntryStart()).arg(v).arg(file).arg(line));
_logsWrite(LogDataDebug, msg);
#ifdef Q_OS_WIN
//OutputDebugString(reinterpret_cast<const wchar_t *>(msg.utf16()));
#elif defined Q_OS_MAC
//objc_outputDebugString(msg);
#elif defined Q_OS_LINUX && defined _DEBUG
//std::cout << msg.toUtf8().constData();
#endif
}
void writeTcp(const QString &v) {
QString msg(QString("%1 %2\n").arg(_logsEntryStart()).arg(v));
_logsWrite(LogDataTcp, msg);
}
void writeMtp(int32 dc, const QString &v) {
QString msg(QString("%1 (dc:%2) %3\n").arg(_logsEntryStart()).arg(dc).arg(v));
_logsWrite(LogDataMtp, msg);
}
QString full() {
if (LogsData) {
return LogsData->full();
}
if (!LogsInMemory || LogsInMemory == DeletedLogsInMemory) {
return LogsBeforeSingleInstanceChecked;
}
int32 size = LogsBeforeSingleInstanceChecked.size();
for (LogsInMemoryList::const_iterator i = LogsInMemory->cbegin(), e = LogsInMemory->cend(); i != e; ++i) {
if (i->first == LogDataMain) {
size += i->second.size();
}
}
QString result;
result.reserve(size);
if (!LogsBeforeSingleInstanceChecked.isEmpty()) {
result.append(LogsBeforeSingleInstanceChecked);
}
for (LogsInMemoryList::const_iterator i = LogsInMemory->cbegin(), e = LogsInMemory->cend(); i != e; ++i) {
if (i->first == LogDataMain) {
result += i->second;
}
}
return result;
}
} // namespace Logs