rpcsx/rpcs3/rpcs3qt/log_viewer.cpp
Megamouse 76629e1b52 Qt: Allow to use .gz files in Log viewer
Log viewer can open .gz files
Log viewer can save log as .gz
Refactored most instances of zip and unzip code to seperate functions
2023-10-13 07:45:16 +02:00

500 lines
13 KiB
C++

#include "stdafx.h"
#include "log_viewer.h"
#include "gui_settings.h"
#include "syntax_highlighter.h"
#include "config_checker.h"
#include "Crypto/unzip.h"
#include <QActionGroup>
#include <QApplication>
#include <QClipboard>
#include <QMenu>
#include <QFile>
#include <QFileDialog>
#include <QInputDialog>
#include <QTextStream>
#include <QHBoxLayout>
#include <QFontDatabase>
#include <QMimeData>
#include <QScrollBar>
#include <QMessageBox>
#include <deque>
#include <map>
LOG_CHANNEL(gui_log, "GUI");
log_viewer::log_viewer(std::shared_ptr<gui_settings> gui_settings)
: m_gui_settings(std::move(gui_settings))
{
setWindowTitle(tr("Log Viewer"));
setObjectName("log_viewer");
setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_StyledBackground);
setAcceptDrops(true);
setMinimumSize(QSize(200, 150)); // seems fine on win 10
resize(QSize(620, 395));
m_path_last = m_gui_settings->GetValue(gui::fd_log_viewer).toString();
m_show_timestamps = m_gui_settings->GetValue(gui::lv_show_timestamps).toBool();
m_show_threads = m_gui_settings->GetValue(gui::lv_show_threads).toBool();
m_log_levels = std::bitset<32>(m_gui_settings->GetValue(gui::lv_log_levels).toUInt());
m_log_text = new QPlainTextEdit(this);
m_log_text->setReadOnly(true);
m_log_text->setContextMenuPolicy(Qt::CustomContextMenu);
m_log_text->setWordWrapMode(QTextOption::NoWrap);
m_log_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
m_log_text->installEventFilter(this);
// m_log_text syntax highlighter
m_log_highlighter = new LogHighlighter(m_log_text->document());
QHBoxLayout* layout = new QHBoxLayout();
layout->addWidget(m_log_text);
setLayout(layout);
connect(m_log_text, &QWidget::customContextMenuRequested, this, &log_viewer::show_context_menu);
}
void log_viewer::show_context_menu(const QPoint& pos)
{
QMenu menu;
QAction* clear = new QAction(tr("&Clear"));
QAction* copy = new QAction(tr("&Copy"));
QAction* open = new QAction(tr("&Open log file"));
QAction* save = new QAction(tr("&Save filtered log"));
QAction* filter = new QAction(tr("&Filter log"));
QAction* config = new QAction(tr("&Check config"));
QAction* timestamps = new QAction(tr("&Show Timestamps"));
timestamps->setCheckable(true);
timestamps->setChecked(m_show_timestamps);
QAction* threads = new QAction(tr("&Show Threads"));
threads->setCheckable(true);
threads->setChecked(m_show_threads);
QAction* last_actions_only = new QAction(tr("&Last actions only"));
last_actions_only->setCheckable(true);
last_actions_only->setChecked(m_last_actions_only);
QActionGroup* log_level_acts = new QActionGroup(this);
QAction* fatal_act = new QAction(tr("Fatal"), log_level_acts);
QAction* error_act = new QAction(tr("Error"), log_level_acts);
QAction* todo_act = new QAction(tr("Todo"), log_level_acts);
QAction* success_act = new QAction(tr("Success"), log_level_acts);
QAction* warning_act = new QAction(tr("Warning"), log_level_acts);
QAction* notice_act = new QAction(tr("Notice"), log_level_acts);
QAction* trace_act = new QAction(tr("Trace"), log_level_acts);
log_level_acts->setExclusive(false);
auto init_action = [this](QAction* act, logs::level logLevel)
{
act->setCheckable(true);
act->setChecked(m_log_levels.test(static_cast<u32>(logLevel)));
// This sets the log level properly when the action is triggered.
connect(act, &QAction::triggered, this, [this, logLevel](bool checked)
{
m_log_levels.set(static_cast<u32>(logLevel), checked);
m_gui_settings->SetValue(gui::lv_log_levels, ::narrow<u32>(m_log_levels.to_ulong()));
filter_log();
});
};
init_action(fatal_act, logs::level::fatal);
init_action(error_act, logs::level::error);
init_action(todo_act, logs::level::todo);
init_action(success_act, logs::level::success);
init_action(warning_act, logs::level::warning);
init_action(notice_act, logs::level::notice);
init_action(trace_act, logs::level::trace);
menu.addAction(copy);
menu.addSeparator();
menu.addAction(open);
menu.addSeparator();
menu.addAction(save);
menu.addSeparator();
menu.addAction(config);
menu.addSeparator();
menu.addAction(filter);
menu.addSeparator();
menu.addAction(timestamps);
menu.addSeparator();
menu.addAction(threads);
menu.addSeparator();
menu.addAction(last_actions_only);
menu.addSeparator();
menu.addActions(log_level_acts->actions());
menu.addSeparator();
menu.addAction(clear);
connect(copy, &QAction::triggered, this, [this]()
{
m_log_text->copy();
});
connect(clear, &QAction::triggered, this, [this]()
{
m_log_text->clear();
m_full_log.clear();
});
connect(open, &QAction::triggered, this, [this]()
{
const QString file_path = QFileDialog::getOpenFileName(this, tr("Select log file"), m_path_last, tr("Log files (*.log *.gz);;All files (*.*)"));
if (file_path.isEmpty())
return;
m_path_last = file_path;
show_log();
});
connect(save, &QAction::triggered, this, [this]()
{
const QString file_path = QFileDialog::getSaveFileName(this, tr("Save to file"), m_path_last, tr("Log files (*.log *.gz);;All files (*.*)"));
if (file_path.isEmpty())
return;
if (fs::file log_file; log_file.open(file_path.toStdString(), fs::rewrite))
{
const QByteArray bytes = m_log_text->toPlainText().toUtf8();
if (file_path.endsWith(".gz"))
{
if (!zip(bytes.constData(), bytes.size(), log_file))
{
gui_log.error("Failed to zip filtered log to file '%s'", file_path);
return;
}
}
else
{
log_file.write(bytes.constData(), bytes.size());
}
gui_log.success("Exported filtered log to file '%s'", file_path);
}
else
{
gui_log.error("Failed to export filtered log to file '%s'", file_path);
}
});
connect(config, &QAction::triggered, this, [this]()
{
config_checker* dlg = new config_checker(this, m_full_log, true);
dlg->exec();
});
connect(filter, &QAction::triggered, this, [this]()
{
m_filter_term = QInputDialog::getText(this, tr("Filter log"), tr("Enter text"), QLineEdit::EchoMode::Normal, m_filter_term);
filter_log();
});
connect(threads, &QAction::toggled, this, [this](bool checked)
{
m_show_threads = checked;
m_gui_settings->SetValue(gui::lv_show_threads, m_show_threads);
filter_log();
});
connect(timestamps, &QAction::toggled, this, [this](bool checked)
{
m_show_timestamps = checked;
m_gui_settings->SetValue(gui::lv_show_timestamps, m_show_timestamps);
filter_log();
});
connect(last_actions_only, &QAction::toggled, this, [this](bool checked)
{
m_last_actions_only = checked;
filter_log();
});
const auto obj = qobject_cast<QPlainTextEdit*>(sender());
QPoint origin;
if (obj == m_log_text)
{
origin = m_log_text->viewport()->mapToGlobal(pos);
}
else
{
origin = mapToGlobal(pos);
}
menu.exec(origin);
}
void log_viewer::show_log()
{
if (m_path_last.isEmpty())
{
return;
}
m_full_log.clear();
m_log_text->clear();
m_log_text->setPlainText(tr("Loading file..."));
QApplication::processEvents();
bool failed = false;
if (m_path_last.endsWith(".gz"))
{
if (fs::file file{m_path_last.toStdString()})
{
const std::vector<u8> decompressed = unzip(file.to_vector<u8>());
m_full_log = QString::fromUtf8(reinterpret_cast<const char*>(decompressed.data()), decompressed.size());
}
else
{
failed = true;
}
}
else if (QFile file(m_path_last); file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text))
{
// TODO: Due to a bug in Qt 6.5.2 QTextStream::readAll is ridiculously slow to the point where it gets stuck on large files.
// In Qt 5.15.2 this was much faster than QFile::readAll. Use QTextStream again once this bug is fixed upstream.
//QTextStream stream(&file);
//m_full_log = stream.readAll();
m_full_log = file.readAll();
}
else
{
failed = true;
}
if (failed)
{
gui_log.error("log_viewer: Failed to open %s", m_path_last);
m_log_text->setPlainText(tr("Failed to open '%0'").arg(m_path_last));
}
else
{
m_gui_settings->SetValue(gui::fd_log_viewer, m_path_last);
m_full_log.replace('\0', '0');
}
filter_log();
}
void log_viewer::set_text_and_keep_position(const QString& text)
{
m_log_text->setPlainText(tr("Pasting..."));
QApplication::processEvents();
const int pos = m_log_text->verticalScrollBar()->value();
m_log_text->setPlainText(text);
m_log_text->verticalScrollBar()->setValue(pos);
}
void log_viewer::filter_log()
{
if (m_full_log.isEmpty())
{
set_text_and_keep_position(m_full_log);
return;
}
m_log_text->setPlainText(tr("Filtering..."));
std::vector<QString> excluded_log_levels;
if (!m_log_levels.test(static_cast<u32>(logs::level::fatal))) excluded_log_levels.push_back("·F ");
if (!m_log_levels.test(static_cast<u32>(logs::level::error))) excluded_log_levels.push_back("·E ");
if (!m_log_levels.test(static_cast<u32>(logs::level::todo))) excluded_log_levels.push_back("·U ");
if (!m_log_levels.test(static_cast<u32>(logs::level::success))) excluded_log_levels.push_back("·S ");
if (!m_log_levels.test(static_cast<u32>(logs::level::warning))) excluded_log_levels.push_back("·W ");
if (!m_log_levels.test(static_cast<u32>(logs::level::notice))) excluded_log_levels.push_back("·! ");
if (!m_log_levels.test(static_cast<u32>(logs::level::trace))) excluded_log_levels.push_back("·T ");
if (m_filter_term.isEmpty() && excluded_log_levels.empty() && m_show_timestamps && m_show_threads && !m_last_actions_only)
{
set_text_and_keep_position(m_full_log);
return;
}
QString result;
QTextStream stream(&m_full_log);
const QRegularExpression thread_regexp("\\{.*\\} ");
const QRegularExpression timestamp_regexp("\\d?\\d:\\d\\d:\\d\\d\\.\\d\\d\\d\\d\\d\\d ");
const auto add_line = [this, &result, &excluded_log_levels, &timestamp_regexp, &thread_regexp](QString& line)
{
bool exclude_line = false;
for (const QString& log_level_prefix : excluded_log_levels)
{
if (line.startsWith(log_level_prefix))
{
exclude_line = true;
break;
}
}
if (exclude_line)
{
return;
}
if (m_filter_term.isEmpty() || line.contains(m_filter_term))
{
if (line.isEmpty())
{
result += "\n";
return;
}
if (!m_show_timestamps)
{
line.remove(timestamp_regexp);
}
if (!m_show_threads)
{
line.remove(thread_regexp);
}
if (!line.isEmpty())
{
result += line + "\n";
}
}
};
if (m_last_actions_only)
{
if (const int start_pos = m_full_log.lastIndexOf("LDR: Used configuration:"); start_pos >= 0)
{
if (!stream.seek(start_pos)) // TODO: is this correct ?
{
gui_log.error("Log viewer failed to seek to pos %d of log.", start_pos);
}
std::map<QString, std::deque<QString>> all_thread_actions;
for (QString line = stream.readLine(); !line.isNull(); line = stream.readLine())
{
if (const QRegularExpressionMatch match = thread_regexp.match(line); match.hasMatch())
{
if (const QString thread_name = match.captured(); !thread_name.isEmpty())
{
std::deque<QString>& actions = all_thread_actions[thread_name];
actions.push_back(line);
if (actions.size() > 10)
{
actions.pop_front();
}
}
}
}
for (auto& [thread_name, actions] : all_thread_actions)
{
for (QString& line : actions)
{
add_line(line);
}
}
set_text_and_keep_position(result);
return;
}
QMessageBox::information(this, tr("Ooops!"), tr("Cannot find any game boot!"));
// Pass through to regular log filter
}
if (!stream.seek(0))
{
gui_log.error("Log viewer failed to seek to beginning of log.");
}
for (QString line = stream.readLine(); !line.isNull(); line = stream.readLine())
{
add_line(line);
};
set_text_and_keep_position(result);
}
bool log_viewer::is_valid_file(const QMimeData& md, bool save)
{
const QList<QUrl> urls = md.urls();
if (urls.count() > 1)
{
return false;
}
const QString suffix = QFileInfo(urls[0].fileName()).suffix().toLower();
if (suffix == "log" || suffix == "gz")
{
if (save)
{
m_path_last = urls[0].toLocalFile();
}
return true;
}
return false;
}
void log_viewer::dropEvent(QDropEvent* ev)
{
if (is_valid_file(*ev->mimeData(), true))
{
show_log();
}
}
void log_viewer::dragEnterEvent(QDragEnterEvent* ev)
{
if (is_valid_file(*ev->mimeData()))
{
ev->accept();
}
}
void log_viewer::dragMoveEvent(QDragMoveEvent* ev)
{
if (is_valid_file(*ev->mimeData()))
{
ev->accept();
}
}
void log_viewer::dragLeaveEvent(QDragLeaveEvent* ev)
{
ev->accept();
}
bool log_viewer::eventFilter(QObject* object, QEvent* event)
{
if (object != m_log_text)
{
return QWidget::eventFilter(object, event);
}
if (event->type() == QEvent::KeyPress)
{
QKeyEvent* e = static_cast<QKeyEvent*>(event);
if (e && !e->isAutoRepeat() && e->modifiers() == Qt::ControlModifier && e->key() == Qt::Key_F)
{
if (m_find_dialog && m_find_dialog->isVisible())
m_find_dialog->close();
m_find_dialog.reset(new find_dialog(static_cast<QPlainTextEdit*>(object), this));
}
}
return QWidget::eventFilter(object, event);
}