refactor: unified exception handling for llvm.
This commit is contained in:
@@ -49,7 +49,7 @@ public:
|
|||||||
HSTR(VarTemplateSpecialization);
|
HSTR(VarTemplateSpecialization);
|
||||||
#undef HSTR
|
#undef HSTR
|
||||||
default:
|
default:
|
||||||
throw EnumTransformException(str, Enum{});
|
throw ConvertEnumException(str, TypeOnly<Enum>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
@@ -87,7 +87,7 @@ public:
|
|||||||
HSTR(VarTemplateSpecialization);
|
HSTR(VarTemplateSpecialization);
|
||||||
#undef HSTR
|
#undef HSTR
|
||||||
default:
|
default:
|
||||||
throw EnumTransformException(m_data, Enum{});
|
throw ConvertEnumException(m_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public:
|
|||||||
add_context("path", list_file_path.string());
|
add_context("path", list_file_path.string());
|
||||||
add_context("current_line", current_line);
|
add_context("current_line", current_line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const {
|
||||||
|
return "exception.dataformat.missingdecltype";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace di::data_format
|
} // namespace di::data_format
|
||||||
|
|||||||
73
src/error.h
73
src/error.h
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <llvm/Support/Error.h>
|
||||||
|
|
||||||
namespace di {
|
namespace di {
|
||||||
|
|
||||||
class BaseException {
|
class BaseException {
|
||||||
@@ -30,7 +32,7 @@ public:
|
|||||||
return static_cast<const Derived*>(this)->category();
|
return static_cast<const Derived*>(this)->category();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string what() const override {
|
std::string what() const final {
|
||||||
// [exception.base] Reason...
|
// [exception.base] Reason...
|
||||||
//
|
//
|
||||||
// Context Information:
|
// Context Information:
|
||||||
@@ -82,6 +84,12 @@ protected:
|
|||||||
m_context_information[key] = std::format("{}", std::forward<T>(value));
|
m_context_information[key] = std::format("{}", std::forward<T>(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <std::integral T>
|
||||||
|
constexpr void add_context_v_hex(const std::string& key, T value) {
|
||||||
|
m_context_information[key] =
|
||||||
|
std::format("{:#x}", std::forward<T>(value));
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_reason;
|
std::string m_reason;
|
||||||
|
|
||||||
@@ -99,26 +107,79 @@ public:
|
|||||||
constexpr std::string category() const { return "exception.unix"; }
|
constexpr std::string category() const { return "exception.unix"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class EnumTransformException : public RuntimeException<EnumTransformException> {
|
class ConvertEnumException : public RuntimeException<ConvertEnumException> {
|
||||||
public:
|
public:
|
||||||
// TODO: compile-time reflection.
|
// TODO: compile-time reflection.
|
||||||
// TODO: remove helper.
|
// TODO: remove helper.
|
||||||
|
|
||||||
template <typename T>
|
template <Enumerate T>
|
||||||
explicit EnumTransformException(int enum_val, T _helper = {})
|
explicit ConvertEnumException(T enum_val)
|
||||||
: RuntimeException("Unable to convert string to enumeration value because "
|
: RuntimeException("Unable to convert string to enumeration value because "
|
||||||
"input value is bad.") {
|
"input value is bad.") {
|
||||||
add_context("enum_type", typeid(T).name());
|
add_context("enum_type", typeid(T).name());
|
||||||
add_context("value", enum_val);
|
add_context("value", underlying_value(enum_val));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
explicit EnumTransformException(std::string_view enum_str, T _helper = {})
|
explicit ConvertEnumException(std::string_view enum_str, TypeOnly<T>)
|
||||||
: RuntimeException("Unable to convert enumeration value to string because "
|
: RuntimeException("Unable to convert enumeration value to string because "
|
||||||
"input value is bad.") {
|
"input value is bad.") {
|
||||||
add_context("enum_type", typeid(T).name());
|
add_context("enum_type", typeid(T).name());
|
||||||
add_context("string", enum_str);
|
add_context("string", enum_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const { return "exception.enumconvert"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class LLVMException : public RuntimeException<LLVMException> {
|
||||||
|
public:
|
||||||
|
explicit LLVMException(
|
||||||
|
std::string_view error_message_di = "",
|
||||||
|
std::string_view error_message_llvm = ""
|
||||||
|
)
|
||||||
|
: RuntimeException("There were some problems when calling LLVM.") {
|
||||||
|
if (!error_message_di.empty())
|
||||||
|
add_context("error_message_di", error_message_di);
|
||||||
|
if (!error_message_llvm.empty())
|
||||||
|
add_context("error_message_llvm", error_message_llvm);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const { return "exception.llvm"; }
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr void check_llvm_result(Error err, std::string_view msg = "") {
|
||||||
|
if (err) {
|
||||||
|
std::string err_detail;
|
||||||
|
raw_string_ostream os(err_detail);
|
||||||
|
os << err;
|
||||||
|
throw LLVMException(msg, err_detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr T
|
||||||
|
check_llvm_result(Expected<T> val_or_err, std::string_view msg = "") {
|
||||||
|
if (val_or_err) return std::move(*val_or_err);
|
||||||
|
else {
|
||||||
|
std::string err_detail;
|
||||||
|
raw_string_ostream os(err_detail);
|
||||||
|
auto err = val_or_err.takeError();
|
||||||
|
os << err;
|
||||||
|
throw LLVMException(msg, err_detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr T&
|
||||||
|
check_llvm_result(Expected<T&> val_or_err, std::string_view msg = "") {
|
||||||
|
if (val_or_err) return *val_or_err;
|
||||||
|
else {
|
||||||
|
std::string err_detail;
|
||||||
|
raw_string_ostream os(err_detail);
|
||||||
|
auto err = val_or_err.takeError();
|
||||||
|
os << err;
|
||||||
|
throw LLVMException(msg, err_detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace di
|
} // namespace di
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ constexpr auto static_unique_ptr_cast(std::unique_ptr<From>&& F) {
|
|||||||
return std::unique_ptr<To>(static_cast<To*>(F.release()));
|
return std::unique_ptr<To>(static_cast<To*>(F.release()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
concept Enumerate = std::is_enum_v<T>;
|
||||||
|
|
||||||
|
template <Enumerate T>
|
||||||
|
constexpr auto underlying_value(T v) {
|
||||||
|
return static_cast<std::underlying_type_t<decltype(v)>>(v);
|
||||||
|
}
|
||||||
|
|
||||||
// From:
|
// From:
|
||||||
// https://stackoverflow.com/questions/7110301/generic-hash-for-tuples-in-unordered-map-unordered-set
|
// https://stackoverflow.com/questions/7110301/generic-hash-for-tuples-in-unordered-map-unordered-set
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
#include "object_file/coff.h"
|
#include "object_file/coff.h"
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
namespace di::object_file {
|
namespace di::object_file {
|
||||||
|
|
||||||
COFF::COFF(const fs::path& path) {
|
COFF::COFF(const fs::path& path) {
|
||||||
using namespace object;
|
using namespace object;
|
||||||
|
|
||||||
auto obj_or_err = ObjectFile::createObjectFile(path.string());
|
auto file = check_llvm_result(ObjectFile::createObjectFile(path.string()));
|
||||||
if (!obj_or_err) {
|
|
||||||
throw std::runtime_error("Failed to create object file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isa<COFFObjectFile>(obj_or_err->getBinary())) {
|
if (!isa<COFFObjectFile>(file.getBinary()))
|
||||||
throw std::runtime_error("Is not a valid PE file.");
|
throw UnexceptObjectException(path, TypeOnly<COFFObjectFile>{});
|
||||||
}
|
|
||||||
|
|
||||||
auto bin = obj_or_err->takeBinary();
|
auto bin = file.takeBinary();
|
||||||
|
|
||||||
m_owning_binary = object::OwningBinary(
|
m_owning_binary = object::OwningBinary(
|
||||||
static_unique_ptr_cast<COFFObjectFile>(std::move(bin.first)),
|
static_unique_ptr_cast<COFFObjectFile>(std::move(bin.first)),
|
||||||
@@ -28,11 +25,13 @@ codeview::PDB70DebugInfo COFF::get_debug_info() const {
|
|||||||
|
|
||||||
if (get_owning_coff().getDebugPDBInfo(debug_info, pdb_file_name)
|
if (get_owning_coff().getDebugPDBInfo(debug_info, pdb_file_name)
|
||||||
|| !debug_info) {
|
|| !debug_info) {
|
||||||
throw std::runtime_error("Failed to get pdb info from coff file.");
|
throw MissingPDBInfoException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debug_info->Signature.CVSignature != OMF::Signature::PDB70) {
|
|
||||||
throw std::runtime_error("Unsupported PDB format.");
|
if (auto signature = debug_info->Signature.CVSignature;
|
||||||
|
signature != OMF::Signature::PDB70) {
|
||||||
|
throw UnsupportPDBFormatException(signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
return debug_info->PDB70;
|
return debug_info->PDB70;
|
||||||
@@ -50,7 +49,7 @@ size_t COFF::get_section_index(size_t offset) const {
|
|||||||
}
|
}
|
||||||
current_index++;
|
current_index++;
|
||||||
}
|
}
|
||||||
throw std::runtime_error("Offset is not in any section.");
|
throw SectionNotFoundException(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
object::coff_section* COFF::get_section_table() {
|
object::coff_section* COFF::get_section_table() {
|
||||||
|
|||||||
@@ -20,4 +20,52 @@ private:
|
|||||||
object::OwningBinary<object::COFFObjectFile> m_owning_binary;
|
object::OwningBinary<object::COFFObjectFile> m_owning_binary;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class UnexceptObjectException : public LLVMException {
|
||||||
|
public:
|
||||||
|
template <typename T>
|
||||||
|
explicit UnexceptObjectException(const fs::path& path, TypeOnly<T>)
|
||||||
|
: LLVMException("Unexpected ObjectFile!") {
|
||||||
|
add_context("path", path.string());
|
||||||
|
add_context("excepted", typeid(T).name());
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const {
|
||||||
|
return "exception.llvm.invalidobject";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MissingPDBInfoException : public LLVMException {
|
||||||
|
public:
|
||||||
|
explicit MissingPDBInfoException()
|
||||||
|
: LLVMException("No PDB Info found in COFF file.") {}
|
||||||
|
|
||||||
|
constexpr std::string category() const {
|
||||||
|
return "exception.llvm.missingpdbinfo";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class UnsupportPDBFormatException : public LLVMException {
|
||||||
|
public:
|
||||||
|
explicit UnsupportPDBFormatException(uint32_t signature)
|
||||||
|
: LLVMException("Unsupported PDB file format.") {
|
||||||
|
add_context_v_hex("signature", signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const {
|
||||||
|
return "exception.llvm.missingpdbinfo";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class SectionNotFoundException : public LLVMException {
|
||||||
|
public:
|
||||||
|
explicit SectionNotFoundException(size_t offset)
|
||||||
|
: LLVMException("The offset is not within any section.") {
|
||||||
|
add_context_v_hex("offset", offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string category() const {
|
||||||
|
return "exception.llvm.sectionnotfound";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace di::object_file
|
} // namespace di::object_file
|
||||||
|
|||||||
@@ -22,58 +22,38 @@ using namespace llvm::codeview;
|
|||||||
namespace di::object_file {
|
namespace di::object_file {
|
||||||
|
|
||||||
void PDB::read(const fs::path& path) {
|
void PDB::read(const fs::path& path) {
|
||||||
if (loadDataForPDB(PDB_ReaderType::Native, path.string(), m_session)) {
|
check_llvm_result(
|
||||||
throw std::runtime_error("Failed to load PDB.");
|
loadDataForPDB(PDB_ReaderType::Native, path.string(), m_session)
|
||||||
}
|
);
|
||||||
|
|
||||||
std::unique_ptr<IPDBSession> pdb_session;
|
auto& pdb_file = get_native_session().getPDBFile();
|
||||||
if (llvm::pdb::loadDataForPDB(
|
|
||||||
PDB_ReaderType::Native,
|
|
||||||
path.string(),
|
|
||||||
pdb_session
|
|
||||||
)) {
|
|
||||||
throw std::runtime_error("Failed to load PDB.");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto native_session = static_cast<NativeSession*>(pdb_session.get());
|
|
||||||
auto& pdb_file = native_session->getPDBFile();
|
|
||||||
|
|
||||||
SmallVector<codeview::TypeIndex, 128> type_map;
|
SmallVector<codeview::TypeIndex, 128> type_map;
|
||||||
SmallVector<codeview::TypeIndex, 128> id_map;
|
SmallVector<codeview::TypeIndex, 128> id_map;
|
||||||
|
|
||||||
if (auto tpi_stream = pdb_file.getPDBTpiStream()) {
|
auto& tpi_stream = check_llvm_result(pdb_file.getPDBTpiStream());
|
||||||
if (codeview::mergeTypeRecords(
|
|
||||||
*m_storaged_Tpi,
|
|
||||||
type_map,
|
|
||||||
(*tpi_stream).typeArray()
|
|
||||||
)) {
|
|
||||||
throw std::runtime_error("Failed to merge type record.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw std::runtime_error("TPI is not valid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto ipi_stream = pdb_file.getPDBIpiStream()) {
|
check_llvm_result(codeview::mergeTypeRecords(
|
||||||
if (codeview::mergeIdRecords(
|
*m_storaged_Tpi,
|
||||||
*m_storaged_Ipi,
|
type_map,
|
||||||
type_map,
|
tpi_stream.typeArray()
|
||||||
id_map,
|
));
|
||||||
(*ipi_stream).typeArray()
|
|
||||||
)) {
|
auto& ipi_stream = check_llvm_result(pdb_file.getPDBIpiStream());
|
||||||
throw std::runtime_error("Failed to merge id record.");
|
|
||||||
}
|
check_llvm_result(codeview::mergeIdRecords(
|
||||||
} else {
|
*m_storaged_Ipi,
|
||||||
throw std::runtime_error("IPI is not valid.");
|
type_map,
|
||||||
}
|
id_map,
|
||||||
|
ipi_stream.typeArray()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PDB::_write(const fs::path& path) {
|
void PDB::_write(const fs::path& path) {
|
||||||
build();
|
build();
|
||||||
|
|
||||||
GUID out_guid;
|
GUID out_guid;
|
||||||
if (m_builder->commit(path.string(), &out_guid)) {
|
check_llvm_result(m_builder->commit(path.string(), &out_guid));
|
||||||
throw std::runtime_error("Failed to create pdb!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeSession& PDB::get_native_session() {
|
NativeSession& PDB::get_native_session() {
|
||||||
@@ -85,42 +65,28 @@ pdb::PDBFile& PDB::get_pdb_file() { return get_native_session().getPDBFile(); }
|
|||||||
void PDB::_for_each_public(const for_each_symbol_callback_t& callback) {
|
void PDB::_for_each_public(const for_each_symbol_callback_t& callback) {
|
||||||
using namespace codeview;
|
using namespace codeview;
|
||||||
auto& file = get_pdb_file();
|
auto& file = get_pdb_file();
|
||||||
auto publics_stream = file.getPDBPublicsStream();
|
auto& publics_stream = check_llvm_result(file.getPDBPublicsStream());
|
||||||
if (!publics_stream) {
|
auto& symbol_stream = check_llvm_result(file.getPDBSymbolStream());
|
||||||
throw std::runtime_error("Failed to get public stream from PDB.");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto publics_symbol_stream = file.getPDBSymbolStream();
|
auto raw_stream = symbol_stream.getSymbolArray().getUnderlyingStream();
|
||||||
if (!publics_symbol_stream) {
|
for (auto offset : publics_stream.getPublicsTable()) {
|
||||||
throw std::runtime_error("Failed to get symbol stream from PDB.");
|
auto cv_symbol = readSymbolFromStream(raw_stream, offset);
|
||||||
}
|
auto public_sym32 = check_llvm_result(
|
||||||
|
SymbolDeserializer::deserializeAs<PublicSym32>(cv_symbol.get()),
|
||||||
auto publics_symbols =
|
"Unsupported symbol type."
|
||||||
publics_symbol_stream->getSymbolArray().getUnderlyingStream();
|
);
|
||||||
for (auto offset : publics_stream->getPublicsTable()) {
|
callback(public_sym32);
|
||||||
auto cv_symbol = readSymbolFromStream(publics_symbols, offset);
|
|
||||||
auto public_sym32 =
|
|
||||||
SymbolDeserializer::deserializeAs<PublicSym32>(cv_symbol.get());
|
|
||||||
if (!public_sym32) {
|
|
||||||
throw std::runtime_error("Unsupported symbol type.");
|
|
||||||
}
|
|
||||||
callback(*public_sym32);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PDB::build() {
|
void PDB::build() {
|
||||||
constexpr auto block_size = 4096;
|
|
||||||
|
|
||||||
m_builder.reset(new PDBFileBuilder{m_Alloc});
|
m_builder.reset(new PDBFileBuilder{m_Alloc});
|
||||||
|
|
||||||
if (m_builder->initialize(block_size)) {
|
constexpr auto block_size = 4096;
|
||||||
throw std::runtime_error("Failed to initialize pdb file builder.");
|
check_llvm_result(m_builder->initialize(block_size));
|
||||||
}
|
|
||||||
|
|
||||||
for (uint32_t idx = 0; idx < pdb::kSpecialStreamCount; ++idx) {
|
for (uint32_t idx = 0; idx < pdb::kSpecialStreamCount; ++idx) {
|
||||||
if (!m_builder->getMsfBuilder().addStream(0)) {
|
check_llvm_result(m_builder->getMsfBuilder().addStream(0));
|
||||||
throw std::runtime_error("Failed to add initial stream.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// INFO
|
// INFO
|
||||||
@@ -163,9 +129,9 @@ void PDB::build() {
|
|||||||
Dbi.createSectionMap(section_table_ref);
|
Dbi.createSectionMap(section_table_ref);
|
||||||
|
|
||||||
// Add COFF section header stream.
|
// Add COFF section header stream.
|
||||||
if (Dbi.addDbgStream(DbgHeaderType::SectionHdr, section_data_ref)) {
|
check_llvm_result(
|
||||||
throw std::runtime_error("Failed to add dbg stream.");
|
Dbi.addDbgStream(DbgHeaderType::SectionHdr, section_data_ref)
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TPI & IPI
|
// TPI & IPI
|
||||||
@@ -198,17 +164,15 @@ void PDB::build() {
|
|||||||
|
|
||||||
auto section_index =
|
auto section_index =
|
||||||
m_owning_coff->get_section_index(entity.m_rva - m_image_base);
|
m_owning_coff->get_section_index(entity.m_rva - m_image_base);
|
||||||
auto section_or_err =
|
auto section = check_llvm_result(
|
||||||
m_owning_coff->get_owning_coff().getSection(section_index + 1);
|
m_owning_coff->get_owning_coff().getSection(section_index + 1)
|
||||||
if (!section_or_err) {
|
);
|
||||||
throw std::runtime_error("Invalid section.");
|
|
||||||
}
|
|
||||||
|
|
||||||
symbol.Name = strdup(entity.m_symbol_name.c_str());
|
symbol.Name = strdup(entity.m_symbol_name.c_str());
|
||||||
symbol.NameLen = entity.m_symbol_name.size();
|
symbol.NameLen = entity.m_symbol_name.size();
|
||||||
symbol.Segment = section_index + 1;
|
symbol.Segment = section_index + 1;
|
||||||
symbol.Offset = entity.m_rva - m_image_base
|
symbol.Offset =
|
||||||
- section_or_err.get()->VirtualAddress;
|
entity.m_rva - m_image_base - section->VirtualAddress;
|
||||||
if (entity.m_is_function) symbol.setFlags(PublicSymFlags::Function);
|
if (entity.m_is_function) symbol.setFlags(PublicSymFlags::Function);
|
||||||
|
|
||||||
publics.emplace_back(symbol);
|
publics.emplace_back(symbol);
|
||||||
|
|||||||
@@ -10,3 +10,7 @@ using hash_t = uint64_t;
|
|||||||
} // namespace di
|
} // namespace di
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
// shit!
|
||||||
|
template <typename T>
|
||||||
|
struct TypeOnly {};
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ set_warnings('all')
|
|||||||
|
|
||||||
add_includedirs('src')
|
add_includedirs('src')
|
||||||
|
|
||||||
|
-- workaround to fix std::stacktrace link problem
|
||||||
|
-- for gcc == 14
|
||||||
|
-- see https://gcc.gnu.org/onlinedocs/gcc-14.2.0/libstdc++/manual/manual/using.html
|
||||||
|
-- for gcc == 13
|
||||||
|
-- see https://gcc.gnu.org/onlinedocs/gcc-13.2.0/libstdc++/manual/manual/using.html
|
||||||
|
if is_plat('linux') then
|
||||||
|
add_links('stdc++exp')
|
||||||
|
end
|
||||||
|
|
||||||
if is_mode('debug') then
|
if is_mode('debug') then
|
||||||
add_defines('DI_DEBUG')
|
add_defines('DI_DEBUG')
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user