forked from Silverfish/proton-bridge
feat(GODT-2482): more attachment to relevant exceptions.
This commit is contained in:
@ -18,26 +18,13 @@
|
|||||||
|
|
||||||
#include "LogUtils.h"
|
#include "LogUtils.h"
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
|
#include <bridgepp/Log/LogUtils.h>
|
||||||
#include <bridgepp/BridgeUtils.h>
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
|
||||||
|
|
||||||
using namespace bridgepp;
|
using namespace bridgepp;
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
qsizetype const logFileTailMaxLength = 25 * 1024; ///< The maximum length of the portion of log returned by tailOfLatestBridgeLog()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \return user logs directory used by bridge.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString userLogsDir() {
|
|
||||||
QString const path = QDir(bridgepp::userDataDir()).absoluteFilePath("logs");
|
|
||||||
QDir().mkpath(path);
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
/// \return A reference to the log.
|
/// \return A reference to the log.
|
||||||
//****************************************************************************************************************************************************
|
//****************************************************************************************************************************************************
|
||||||
@ -69,36 +56,3 @@ Log &initLog() {
|
|||||||
|
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// \brief Return the path of the latest bridge log.
|
|
||||||
/// \return The path of the latest bridge log file.
|
|
||||||
/// \return An empty string if no bridge log file was found.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QString latestBridgeLogPath() {
|
|
||||||
QDir const logsDir(userLogsDir());
|
|
||||||
if (logsDir.isEmpty()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
|
||||||
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
|
||||||
return lhs.birthTime() < rhs.birthTime();
|
|
||||||
});
|
|
||||||
return files.back().absoluteFilePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
/// Return the maxSize last bytes of the latest bridge log.
|
|
||||||
//****************************************************************************************************************************************************
|
|
||||||
QByteArray tailOfLatestBridgeLog() {
|
|
||||||
QString path = latestBridgeLogPath();
|
|
||||||
if (path.isEmpty()) {
|
|
||||||
return QByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile file(path);
|
|
||||||
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(logFileTailMaxLength) : QByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
|
|
||||||
bridgepp::Log &initLog(); ///< Initialize the application log.
|
bridgepp::Log &initLog(); ///< Initialize the application log.
|
||||||
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
|
||||||
|
|
||||||
|
|
||||||
#endif //BRIDGE_GUI_LOG_UTILS_H
|
#endif //BRIDGE_GUI_LOG_UTILS_H
|
||||||
|
|||||||
@ -19,9 +19,9 @@
|
|||||||
#include "QMLBackend.h"
|
#include "QMLBackend.h"
|
||||||
#include "BuildConfig.h"
|
#include "BuildConfig.h"
|
||||||
#include "EventStreamWorker.h"
|
#include "EventStreamWorker.h"
|
||||||
#include "LogUtils.h"
|
|
||||||
#include <bridgepp/BridgeUtils.h>
|
#include <bridgepp/BridgeUtils.h>
|
||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
|
#include <bridgepp/Log/LogUtils.h>
|
||||||
#include <bridgepp/GRPC/GRPCClient.h>
|
#include <bridgepp/GRPC/GRPCClient.h>
|
||||||
#include <bridgepp/Worker/Overseer.h>
|
#include <bridgepp/Worker/Overseer.h>
|
||||||
#include <bridgepp/BridgeUtils.h>
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
#include <bridgepp/Exception/Exception.h>
|
#include <bridgepp/Exception/Exception.h>
|
||||||
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
#include <bridgepp/FocusGRPC/FocusGRPCClient.h>
|
||||||
#include <bridgepp/Log/Log.h>
|
#include <bridgepp/Log/Log.h>
|
||||||
|
#include <bridgepp/Log/LogUtils.h>
|
||||||
#include <bridgepp/ProcessMonitor.h>
|
#include <bridgepp/ProcessMonitor.h>
|
||||||
|
|
||||||
|
|
||||||
@ -116,7 +117,7 @@ QQmlComponent *createRootQmlComponent(QQmlApplicationEngine &engine) {
|
|||||||
|
|
||||||
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
rootComponent->loadUrl(QUrl(qrcQmlDir + "/Bridge.qml"));
|
||||||
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
if (rootComponent->status() != QQmlComponent::Status::Ready) {
|
||||||
QString const &err =rootComponent->errorString();
|
QString const &err = rootComponent->errorString();
|
||||||
app().log().error(err);
|
app().log().error(err);
|
||||||
throw Exception("Could not load QML component", err);
|
throw Exception("Could not load QML component", err);
|
||||||
}
|
}
|
||||||
@ -211,7 +212,7 @@ void focusOtherInstance() {
|
|||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
if (!client.connectToServer(5000, sc.port, &error)) {
|
if (!client.connectToServer(5000, sc.port, &error)) {
|
||||||
throw Exception(QString("Could not connect to bridge focus service for a raise call: %1").arg(error));
|
throw Exception("Could not connect to bridge focus service for a raise call.", error);
|
||||||
}
|
}
|
||||||
if (!client.raise().ok()) {
|
if (!client.raise().ok()) {
|
||||||
throw Exception(QString("The raise call to the bridge focus service failed."));
|
throw Exception(QString("The raise call to the bridge focus service failed."));
|
||||||
@ -220,7 +221,7 @@ void focusOtherInstance() {
|
|||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
app().log().error(e.qwhat());
|
app().log().error(e.qwhat());
|
||||||
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
auto uuid = reportSentryException("Exception occurred during focusOtherInstance()", e);
|
||||||
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex()).arg(e.qwhat()));
|
app().log().fatal(QString("reportID: %1 Captured exception: %2").arg(QByteArray(uuid.bytes, 16).toHex(), e.qwhat()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,15 +277,12 @@ int main(int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BridgeApp guiApp(argc, argv);
|
BridgeApp guiApp(argc, argv);
|
||||||
|
initSentry();
|
||||||
|
auto sentryCloser = qScopeGuard([] { sentry_close(); });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
QString const& configDir = bridgepp::userConfigDir();
|
QString const& configDir = bridgepp::userConfigDir();
|
||||||
|
|
||||||
// Init sentry.
|
|
||||||
initSentry();
|
|
||||||
|
|
||||||
auto sentryClose = qScopeGuard([] { sentry_close(); });
|
|
||||||
|
|
||||||
|
|
||||||
initQtApplication();
|
initQtApplication();
|
||||||
|
|
||||||
@ -311,7 +309,7 @@ int main(int argc, char *argv[]) {
|
|||||||
if (!cliOptions.attach) {
|
if (!cliOptions.attach) {
|
||||||
if (isBridgeRunning()) {
|
if (isBridgeRunning()) {
|
||||||
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
throw Exception("An orphan instance of bridge is already running. Please terminate it and relaunch the application.",
|
||||||
QString(), QString(), tailOfLatestBridgeLog());
|
QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
|
|
||||||
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
// before launching bridge, we remove any trailing service config file, because we need to make sure we get a newly generated one.
|
||||||
@ -345,7 +343,7 @@ int main(int argc, char *argv[]) {
|
|||||||
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
std::unique_ptr<QQmlComponent> rootComponent(createRootQmlComponent(engine));
|
||||||
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
std::unique_ptr<QObject> rootObject(rootComponent->create(engine.rootContext()));
|
||||||
if (!rootObject) {
|
if (!rootObject) {
|
||||||
throw Exception("Could not create root object.");
|
throw Exception("Could not create QML root object.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ProcessMonitor *bridgeMonitor = app().bridgeMonitor();
|
ProcessMonitor *bridgeMonitor = app().bridgeMonitor();
|
||||||
|
|||||||
@ -146,6 +146,7 @@ add_library(bridgepp
|
|||||||
${FOCUS_PROTO_CPP_FILE} ${FOCUS_PROTO_H_FILE} ${FOCUS_GRPC_CPP_FILE} ${FOCUS_GRPC_H_FILE}
|
${FOCUS_PROTO_CPP_FILE} ${FOCUS_PROTO_H_FILE} ${FOCUS_GRPC_CPP_FILE} ${FOCUS_GRPC_H_FILE}
|
||||||
bridgepp/FocusGRPC/FocusGRPCClient.cpp bridgepp/FocusGRPC/FocusGRPCClient.h
|
bridgepp/FocusGRPC/FocusGRPCClient.cpp bridgepp/FocusGRPC/FocusGRPCClient.h
|
||||||
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
|
bridgepp/Log/Log.h bridgepp/Log/Log.cpp
|
||||||
|
bridgepp/Log/LogUtils.h bridgepp/Log/LogUtils.cpp
|
||||||
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
|
bridgepp/ProcessMonitor.cpp bridgepp/ProcessMonitor.h
|
||||||
bridgepp/User/User.cpp bridgepp/User/User.h
|
bridgepp/User/User.cpp bridgepp/User/User.h
|
||||||
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
|
bridgepp/Worker/Worker.h bridgepp/Worker/Overseer.h bridgepp/Worker/Overseer.cpp)
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
namespace bridgepp {
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
@ -44,6 +45,9 @@ public: // member functions
|
|||||||
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
|
QByteArray attachment() const noexcept; ///< Return the attachment for the exception.
|
||||||
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
|
QString detailedWhat() const; ///< Return the detailed description of the message (i.e. including the function name and the details).
|
||||||
|
|
||||||
|
public: // static data members
|
||||||
|
static qsizetype const attachmentMaxLength {25 * 1024}; ///< The maximum length text attachment sent in Sentry reports, in bytes.
|
||||||
|
|
||||||
private: // data members
|
private: // data members
|
||||||
QString const qwhat_; ///< The description of the exception.
|
QString const qwhat_; ///< The description of the exception.
|
||||||
QByteArray const what_; ///< The c-string version of the qwhat message. Stored as a QByteArray for automatic lifetime management.
|
QByteArray const what_; ///< The c-string version of the qwhat message. Stored as a QByteArray for automatic lifetime management.
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
#include "../BridgeUtils.h"
|
#include "../BridgeUtils.h"
|
||||||
#include "../Exception/Exception.h"
|
#include "../Exception/Exception.h"
|
||||||
#include "../ProcessMonitor.h"
|
#include "../ProcessMonitor.h"
|
||||||
|
#include "../Log/LogUtils.h"
|
||||||
|
|
||||||
|
|
||||||
using namespace google::protobuf;
|
using namespace google::protobuf;
|
||||||
@ -70,7 +71,8 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
|
|||||||
bool found = false;
|
bool found = false;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (serverProcess && serverProcess->getStatus().ended) {
|
if (serverProcess && serverProcess->getStatus().ended) {
|
||||||
throw Exception("Bridge application exited before providing a gRPC service configuration file.");
|
throw Exception("Bridge application exited before providing a gRPC service configuration file.", QString(), __FUNCTION__,
|
||||||
|
tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
@ -84,13 +86,20 @@ GRPCConfig GRPCClient::waitAndRetrieveServiceConfig(QString const &configDir, qi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!found) {
|
if (!found) {
|
||||||
throw Exception("Server did not provide gRPC service configuration in time.");
|
throw Exception("Server did not provide gRPC service configuration in time.", QString(), __FUNCTION__, tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
|
|
||||||
GRPCConfig sc;
|
GRPCConfig sc;
|
||||||
QString err;
|
QString err;
|
||||||
if (!sc.load(path, &err)) {
|
if (!sc.load(path, &err)) {
|
||||||
throw Exception("The gRPC service configuration file is invalid.", err);
|
// include the file content in the exception, if any
|
||||||
|
QByteArray array;
|
||||||
|
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
file.readAll();
|
||||||
|
array = array.right(Exception::attachmentMaxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception("The gRPC service configuration file is invalid.", err, __FUNCTION__, array);
|
||||||
}
|
}
|
||||||
|
|
||||||
return sc;
|
return sc;
|
||||||
@ -126,19 +135,20 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
|
|||||||
|
|
||||||
channel_ = CreateCustomChannel(address.toStdString(), grpc::SslCredentials(opts), chanArgs);
|
channel_ = CreateCustomChannel(address.toStdString(), grpc::SslCredentials(opts), chanArgs);
|
||||||
if (!channel_) {
|
if (!channel_) {
|
||||||
throw Exception("Channel creation failed.");
|
throw Exception("gRPC channel creation failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
stub_ = Bridge::NewStub(channel_);
|
stub_ = Bridge::NewStub(channel_);
|
||||||
if (!stub_) {
|
if (!stub_) {
|
||||||
throw Exception("Stub creation failed.");
|
throw Exception("gRPC stub creation failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
QDateTime const giveUpTime = QDateTime::currentDateTime().addMSecs(grpcConnectionWaitTimeoutMs); // if we reach giveUpTime without connecting, we give up
|
QDateTime const giveUpTime = QDateTime::currentDateTime().addMSecs(grpcConnectionWaitTimeoutMs); // if we reach giveUpTime without connecting, we give up
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (serverProcess && serverProcess->getStatus().ended) {
|
if (serverProcess && serverProcess->getStatus().ended) {
|
||||||
throw Exception("Bridge application ended before gRPC connexion could be established.");
|
throw Exception("Bridge application ended before gRPC connexion could be established.", QString(), __FUNCTION__,
|
||||||
|
tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
|
|
||||||
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
this->logInfo(QString("Connection to gRPC server at %1. attempt #%2").arg(address).arg(++i));
|
||||||
@ -148,7 +158,8 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
|
|||||||
} // connection established.
|
} // connection established.
|
||||||
|
|
||||||
if (QDateTime::currentDateTime() > giveUpTime) {
|
if (QDateTime::currentDateTime() > giveUpTime) {
|
||||||
throw Exception("Connection to the RPC server failed.");
|
throw Exception("Connection to the gRPC server failed because of a timeout.", QString(), __FUNCTION__,
|
||||||
|
tailOfLatestBridgeLog());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +191,7 @@ void GRPCClient::connectToServer(QString const &configDir, GRPCConfig const &con
|
|||||||
log_->info("gRPC token was validated");
|
log_->info("gRPC token was validated");
|
||||||
}
|
}
|
||||||
catch (Exception const &e) {
|
catch (Exception const &e) {
|
||||||
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details());
|
throw Exception("Cannot connect to Go backend via gRPC: " + e.qwhat(), e.details(), __FUNCTION__, e.attachment());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge 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.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge 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.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
#include "LogUtils.h"
|
||||||
|
#include <bridgepp/BridgeUtils.h>
|
||||||
|
#include <bridgepp/Exception/Exception.h>
|
||||||
|
|
||||||
|
|
||||||
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \return user logs directory used by bridge.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString userLogsDir() {
|
||||||
|
QString const path = QDir(bridgepp::userDataDir()).absoluteFilePath("logs");
|
||||||
|
QDir().mkpath(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// \brief Return the path of the latest bridge log.
|
||||||
|
/// \return The path of the latest bridge log file.
|
||||||
|
/// \return An empty string if no bridge log file was found.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QString latestBridgeLogPath() {
|
||||||
|
QDir const logsDir(userLogsDir());
|
||||||
|
if (logsDir.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
QFileInfoList files = logsDir.entryInfoList({ "v*.log" }, QDir::Files); // could do sorting, but only by last modification time. we want to sort by creation time.
|
||||||
|
std::sort(files.begin(), files.end(), [](QFileInfo const &lhs, QFileInfo const &rhs) -> bool {
|
||||||
|
return lhs.birthTime() < rhs.birthTime();
|
||||||
|
});
|
||||||
|
return files.back().absoluteFilePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
/// Return the maxSize last bytes of the latest bridge log.
|
||||||
|
//****************************************************************************************************************************************************
|
||||||
|
QByteArray tailOfLatestBridgeLog() {
|
||||||
|
QString path = latestBridgeLogPath();
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(path);
|
||||||
|
return file.open(QIODevice::Text | QIODevice::ReadOnly) ? file.readAll().right(Exception::attachmentMaxLength) : QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace bridgepp
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2023 Proton AG
|
||||||
|
//
|
||||||
|
// This file is part of Proton Mail Bridge.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge 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.
|
||||||
|
//
|
||||||
|
// Proton Mail Bridge 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.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
#ifndef BRIDGE_PP_LOG_UTILS_H
|
||||||
|
#define BRIDGE_PP_LOG_UTILS_H
|
||||||
|
|
||||||
|
|
||||||
|
namespace bridgepp {
|
||||||
|
|
||||||
|
|
||||||
|
QString userLogsDir(); ///< Return the path of the user logs dir.
|
||||||
|
QByteArray tailOfLatestBridgeLog(); ///< Return the last bytes of the last bridge log.
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace bridgepp
|
||||||
|
|
||||||
|
|
||||||
|
#endif //BRIDGE_PP_LOG_UTILS_H
|
||||||
Reference in New Issue
Block a user