From 932fa94af747a59fa6f69aab6dff1fd9a802def4 Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 16:01:31 -0500 Subject: handle_table: Remove ConvertSessionToDomain. --- src/core/hle/kernel/handle_table.cpp | 10 ---------- src/core/hle/kernel/handle_table.h | 7 ------- 2 files changed, 17 deletions(-) (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/handle_table.cpp b/src/core/hle/kernel/handle_table.cpp index 74d3d0514..3beb55753 100644 --- a/src/core/hle/kernel/handle_table.cpp +++ b/src/core/hle/kernel/handle_table.cpp @@ -5,12 +5,10 @@ #include #include "common/assert.h" #include "common/logging/log.h" -#include "core/hle/kernel/client_session.h" #include "core/hle/kernel/errors.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/kernel.h" #include "core/hle/kernel/process.h" -#include "core/hle/kernel/session.h" #include "core/hle/kernel/thread.h" namespace Kernel { @@ -55,14 +53,6 @@ ResultVal HandleTable::Duplicate(Handle handle) { return Create(std::move(object)); } -void HandleTable::ConvertSessionToDomain(const Session& session, SharedPtr domain) { - for (auto& object : objects) { - if (DynamicObjectCast(object) == session.client) { - object = domain; - } - } -} - ResultCode HandleTable::Close(Handle handle) { if (!IsValid(handle)) return ERR_INVALID_HANDLE; diff --git a/src/core/hle/kernel/handle_table.h b/src/core/hle/kernel/handle_table.h index 935cc22b5..ba968c666 100644 --- a/src/core/hle/kernel/handle_table.h +++ b/src/core/hle/kernel/handle_table.h @@ -17,8 +17,6 @@ enum KernelHandle : Handle { CurrentProcess = 0xFFFF8001, }; -class Session; - /** * This class allows the creation of Handles, which are references to objects that can be tested * for validity and looked up. Here they are used to pass references to kernel objects to/from the @@ -61,11 +59,6 @@ public: */ ResultVal Duplicate(Handle handle); - /** - * Convert all handles of the specified Session to the specified Domain. - */ - void ConvertSessionToDomain(const Session& session, SharedPtr domain); - /** * Closes a handle, removing it from the table and decreasing the object's ref-count. * @return `RESULT_SUCCESS` or one of the following errors: -- cgit v1.2.3 From 67758857e4203cebc9a5815c6b0293c0aad0e598 Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 16:13:19 -0500 Subject: hle: Remove Domain and SyncObject kernel objects. --- src/core/hle/kernel/client_session.h | 3 +- src/core/hle/kernel/domain.cpp | 63 ------------------------------------ src/core/hle/kernel/domain.h | 45 -------------------------- src/core/hle/kernel/hle_ipc.cpp | 1 - src/core/hle/kernel/kernel.h | 16 --------- src/core/hle/kernel/svc.cpp | 3 +- src/core/hle/kernel/sync_object.h | 35 -------------------- 7 files changed, 2 insertions(+), 164 deletions(-) delete mode 100644 src/core/hle/kernel/domain.cpp delete mode 100644 src/core/hle/kernel/domain.h delete mode 100644 src/core/hle/kernel/sync_object.h (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/client_session.h b/src/core/hle/kernel/client_session.h index d6ab4f893..f2765cc1e 100644 --- a/src/core/hle/kernel/client_session.h +++ b/src/core/hle/kernel/client_session.h @@ -7,7 +7,6 @@ #include #include #include "common/common_types.h" -#include "core/hle/kernel/sync_object.h" #include "core/hle/result.h" namespace Kernel { @@ -16,7 +15,7 @@ class ServerSession; class Session; class Thread; -class ClientSession final : public SyncObject { +class ClientSession final : public Object { public: friend class ServerSession; diff --git a/src/core/hle/kernel/domain.cpp b/src/core/hle/kernel/domain.cpp deleted file mode 100644 index 5035e9c08..000000000 --- a/src/core/hle/kernel/domain.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2018 yuzu emulator team -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include "common/logging/log.h" -#include "core/hle/ipc_helpers.h" -#include "core/hle/kernel/client_port.h" -#include "core/hle/kernel/domain.h" -#include "core/hle/kernel/handle_table.h" -#include "core/hle/kernel/hle_ipc.h" -#include "core/hle/kernel/process.h" -#include "core/hle/kernel/session.h" -#include "core/hle/kernel/thread.h" - -namespace Kernel { - -ResultVal> Domain::Create(std::string name) { - SharedPtr domain(new Domain); - domain->name = std::move(name); - return MakeResult(std::move(domain)); -} - -ResultVal> Domain::CreateFromSession(const Session& session) { - auto res = Create(session.port->GetName() + "_Domain"); - auto& domain = res.Unwrap(); - domain->request_handlers.push_back(std::move(session.server->hle_handler)); - Kernel::g_handle_table.ConvertSessionToDomain(session, domain); - return res; -} - -ResultCode Domain::SendSyncRequest(SharedPtr thread) { - Kernel::HLERequestContext context(this); - u32* cmd_buf = (u32*)Memory::GetPointer(Kernel::GetCurrentThread()->GetTLSAddress()); - context.PopulateFromIncomingCommandBuffer(cmd_buf, *Kernel::g_current_process, - Kernel::g_handle_table); - - auto& domain_message_header = context.GetDomainMessageHeader(); - if (domain_message_header) { - // If there is a DomainMessageHeader, then this is CommandType "Request" - const u32 object_id{context.GetDomainMessageHeader()->object_id}; - switch (domain_message_header->command) { - case IPC::DomainMessageHeader::CommandType::SendMessage: - return request_handlers[object_id - 1]->HandleSyncRequest(context); - - case IPC::DomainMessageHeader::CommandType::CloseVirtualHandle: { - LOG_DEBUG(IPC, "CloseVirtualHandle, object_id=0x%08X", object_id); - - request_handlers[object_id - 1] = nullptr; - - IPC::RequestBuilder rb{context, 2}; - rb.Push(RESULT_SUCCESS); - - return RESULT_SUCCESS; - } - } - - LOG_CRITICAL(IPC, "Unknown domain command=%d", domain_message_header->command.Value()); - UNIMPLEMENTED(); - } - return request_handlers.front()->HandleSyncRequest(context); -} - -} // namespace Kernel diff --git a/src/core/hle/kernel/domain.h b/src/core/hle/kernel/domain.h deleted file mode 100644 index 3fec3b0b2..000000000 --- a/src/core/hle/kernel/domain.h +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2018 yuzu emulator team -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include "core/hle/kernel/sync_object.h" -#include "core/hle/result.h" - -namespace Kernel { - -class Session; -class SessionRequestHandler; - -class Domain final : public SyncObject { -public: - std::string GetTypeName() const override { - return "Domain"; - } - - static const HandleType HANDLE_TYPE = HandleType::Domain; - HandleType GetHandleType() const override { - return HANDLE_TYPE; - } - - static ResultVal> CreateFromSession(const Session& server); - - ResultCode SendSyncRequest(SharedPtr thread) override; - - /// The name of this domain (optional) - std::string name; - - std::vector> request_handlers; - -private: - Domain() = default; - ~Domain() override = default; - - static ResultVal> Create(std::string name = "Unknown"); -}; - -} // namespace Kernel diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index ecf32c18a..2cd6de12e 100644 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -7,7 +7,6 @@ #include "common/common_funcs.h" #include "common/common_types.h" #include "core/hle/ipc_helpers.h" -#include "core/hle/kernel/domain.h" #include "core/hle/kernel/handle_table.h" #include "core/hle/kernel/hle_ipc.h" #include "core/hle/kernel/kernel.h" diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h index 4d9549e45..c77e58f3c 100644 --- a/src/core/hle/kernel/kernel.h +++ b/src/core/hle/kernel/kernel.h @@ -31,7 +31,6 @@ enum class HandleType : u32 { ServerPort, ClientSession, ServerSession, - Domain, }; enum { @@ -84,27 +83,12 @@ public: case HandleType::CodeSet: case HandleType::ClientPort: case HandleType::ClientSession: - case HandleType::Domain: return false; } UNREACHABLE(); } - /** - * Check if svcSendSyncRequest can be called on the object - * @return True svcSendSyncRequest can be called on the object, otherwise false - */ - bool IsSyncable() const { - switch (GetHandleType()) { - case HandleType::ClientSession: - case HandleType::Domain: - return true; - } - - UNREACHABLE(); - } - public: static unsigned int next_object_id; diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index 516309036..4c0276cf0 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -20,7 +20,6 @@ #include "core/hle/kernel/shared_memory.h" #include "core/hle/kernel/svc.h" #include "core/hle/kernel/svc_wrap.h" -#include "core/hle/kernel/sync_object.h" #include "core/hle/kernel/thread.h" #include "core/hle/lock.h" #include "core/hle/result.h" @@ -87,7 +86,7 @@ static ResultCode ConnectToNamedPort(Handle* out_handle, VAddr port_name_address /// Makes a blocking IPC call to an OS service. static ResultCode SendSyncRequest(Handle handle) { - SharedPtr session = g_handle_table.Get(handle); + SharedPtr session = g_handle_table.Get(handle); if (!session) { LOG_ERROR(Kernel_SVC, "called with invalid handle=0x%08X", handle); return ERR_INVALID_HANDLE; diff --git a/src/core/hle/kernel/sync_object.h b/src/core/hle/kernel/sync_object.h deleted file mode 100644 index f2befa2ea..000000000 --- a/src/core/hle/kernel/sync_object.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2018 yuzu emulator team -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "core/hle/kernel/kernel.h" -#include "core/hle/result.h" - -namespace Kernel { - -class Thread; - -/// Class that represents a Kernel object that svcSendSyncRequest can be called on -class SyncObject : public Object { -public: - /** - * Handle a sync request from the emulated application. - * @param thread Thread that initiated the request. - * @returns ResultCode from the operation. - */ - virtual ResultCode SendSyncRequest(SharedPtr thread) = 0; -}; - -// Specialization of DynamicObjectCast for SyncObjects -template <> -inline SharedPtr DynamicObjectCast(SharedPtr object) { - if (object != nullptr && object->IsSyncable()) { - return boost::static_pointer_cast(std::move(object)); - } - return nullptr; -} - -} // namespace Kernel -- cgit v1.2.3 From 27bad0598a3ddce0417388c3945368200150d413 Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 18:03:09 -0500 Subject: hle: Integrate Domain handling into ServerSession. --- src/core/hle/kernel/client_session.h | 3 ++- src/core/hle/kernel/hle_ipc.cpp | 10 +++----- src/core/hle/kernel/hle_ipc.h | 16 ++---------- src/core/hle/kernel/server_session.cpp | 47 ++++++++++++++++++++++++++++++---- src/core/hle/kernel/server_session.h | 18 ++++++++++++- 5 files changed, 66 insertions(+), 28 deletions(-) (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/client_session.h b/src/core/hle/kernel/client_session.h index f2765cc1e..2258f95bc 100644 --- a/src/core/hle/kernel/client_session.h +++ b/src/core/hle/kernel/client_session.h @@ -7,6 +7,7 @@ #include #include #include "common/common_types.h" +#include "core/hle/kernel/kernel.h" #include "core/hle/result.h" namespace Kernel { @@ -32,7 +33,7 @@ public: return HANDLE_TYPE; } - ResultCode SendSyncRequest(SharedPtr thread) override; + ResultCode SendSyncRequest(SharedPtr thread); std::string name; ///< Name of client port (optional) diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp index 2cd6de12e..db104e8a2 100644 --- a/src/core/hle/kernel/hle_ipc.cpp +++ b/src/core/hle/kernel/hle_ipc.cpp @@ -25,10 +25,6 @@ void SessionRequestHandler::ClientDisconnected(SharedPtr server_s boost::range::remove_erase(connected_sessions, server_session); } -HLERequestContext::HLERequestContext(SharedPtr domain) : domain(std::move(domain)) { - cmd_buf[0] = 0; -} - HLERequestContext::HLERequestContext(SharedPtr server_session) : server_session(std::move(server_session)) { cmd_buf[0] = 0; @@ -86,7 +82,7 @@ void HLERequestContext::ParseCommandBuffer(u32_le* src_cmdbuf, bool incoming) { // Padding to align to 16 bytes rp.AlignWithPadding(); - if (IsDomain() && (command_header->type == IPC::CommandType::Request || !incoming)) { + if (Session()->IsDomain() && (command_header->type == IPC::CommandType::Request || !incoming)) { // If this is an incoming message, only CommandType "Request" has a domain header // All outgoing domain messages have the domain header domain_message_header = @@ -199,12 +195,12 @@ ResultCode HLERequestContext::WriteToOutgoingCommandBuffer(u32_le* dst_cmdbuf, P // TODO(Subv): Translate the X/A/B/W buffers. - if (IsDomain()) { + if (Session()->IsDomain()) { ASSERT(domain_message_header->num_objects == domain_objects.size()); // Write the domain objects to the command buffer, these go after the raw untranslated data. // TODO(Subv): This completely ignores C buffers. size_t domain_offset = size - domain_message_header->num_objects; - auto& request_handlers = domain->request_handlers; + auto& request_handlers = server_session->domain_request_handlers; for (auto& object : domain_objects) { request_handlers.emplace_back(object); diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 80fa48d7f..71e5609b8 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -86,7 +86,6 @@ protected: */ class HLERequestContext { public: - HLERequestContext(SharedPtr domain); HLERequestContext(SharedPtr session); ~HLERequestContext(); @@ -95,18 +94,11 @@ public: return cmd_buf.data(); } - /** - * Returns the domain through which this request was made. - */ - const SharedPtr& Domain() const { - return domain; - } - /** * Returns the session through which this request was made. This can be used as a map key to * access per-client data on services. */ - const SharedPtr& ServerSession() const { + const SharedPtr& Session() const { return server_session; } @@ -151,10 +143,6 @@ public: return domain_message_header; } - bool IsDomain() const { - return domain != nullptr; - } - template SharedPtr GetCopyObject(size_t index) { ASSERT(index < copy_objects.size()); @@ -189,7 +177,6 @@ public: private: std::array cmd_buf; - SharedPtr domain; SharedPtr server_session; // TODO(yuriks): Check common usage of this and optimize size accordingly boost::container::small_vector, 8> move_objects; @@ -209,6 +196,7 @@ private: unsigned data_payload_offset{}; unsigned buffer_c_offset{}; u32_le command{}; + bool is_domain{}; }; } // namespace Kernel diff --git a/src/core/hle/kernel/server_session.cpp b/src/core/hle/kernel/server_session.cpp index 09d02a691..b79bf7bab 100644 --- a/src/core/hle/kernel/server_session.cpp +++ b/src/core/hle/kernel/server_session.cpp @@ -4,6 +4,7 @@ #include +#include "core/hle/ipc_helpers.h" #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/client_session.h" #include "core/hle/kernel/handle_table.h" @@ -61,6 +62,38 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { // from its ClientSession, so wake up any threads that may be waiting on a svcReplyAndReceive or // similar. + Kernel::HLERequestContext context(this); + u32* cmd_buf = (u32*)Memory::GetPointer(thread->GetTLSAddress()); + context.PopulateFromIncomingCommandBuffer(cmd_buf, *Kernel::g_current_process, + Kernel::g_handle_table); + + // If the session has been converted to a domain, handle the doomain request + if (IsDomain()) { + auto& domain_message_header = context.GetDomainMessageHeader(); + if (domain_message_header) { + // If there is a DomainMessageHeader, then this is CommandType "Request" + const u32 object_id{context.GetDomainMessageHeader()->object_id}; + switch (domain_message_header->command) { + case IPC::DomainMessageHeader::CommandType::SendMessage: + return domain_request_handlers[object_id - 1]->HandleSyncRequest(context); + + case IPC::DomainMessageHeader::CommandType::CloseVirtualHandle: { + LOG_DEBUG(IPC, "CloseVirtualHandle, object_id=0x%08X", object_id); + + domain_request_handlers[object_id - 1] = nullptr; + + IPC::RequestBuilder rb{context, 2}; + rb.Push(RESULT_SUCCESS); + return RESULT_SUCCESS; + } + } + + LOG_CRITICAL(IPC, "Unknown domain command=%d", domain_message_header->command.Value()); + UNIMPLEMENTED(); + } + return domain_request_handlers.front()->HandleSyncRequest(context); + } + // If this ServerSession has an associated HLE handler, forward the request to it. ResultCode result{RESULT_SUCCESS}; if (hle_handler != nullptr) { @@ -69,11 +102,6 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { if (translate_result.IsError()) return translate_result; - Kernel::HLERequestContext context(this); - u32* cmd_buf = (u32*)Memory::GetPointer(Kernel::GetCurrentThread()->GetTLSAddress()); - context.PopulateFromIncomingCommandBuffer(cmd_buf, *Kernel::g_current_process, - Kernel::g_handle_table); - result = hle_handler->HandleSyncRequest(context); } else { // Add the thread to the list of threads that have issued a sync request with this @@ -84,6 +112,15 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { // If this ServerSession does not have an HLE implementation, just wake up the threads waiting // on it. WakeupAllWaitingThreads(); + + // Handle scenario when ConvertToDomain command was issued, as we must do the conversion at the + // end of the command such that only commands following this one are handled as domains + if (convert_to_domain) { + ASSERT_MSG(domain_request_handlers.empty(), "already a domain"); + domain_request_handlers.push_back(std::move(hle_handler)); + convert_to_domain = false; + } + return result; } diff --git a/src/core/hle/kernel/server_session.h b/src/core/hle/kernel/server_session.h index 6ff4ef8c1..144692106 100644 --- a/src/core/hle/kernel/server_session.h +++ b/src/core/hle/kernel/server_session.h @@ -79,7 +79,10 @@ public: std::string name; ///< The name of this session (optional) std::shared_ptr parent; ///< The parent session, which links to the client endpoint. std::shared_ptr - hle_handler; ///< This session's HLE request handler (optional) + hle_handler; ///< This session's HLE request handler (applicable when not a domain) + + /// This is the list of domain request handlers (after conversion to a domain) + std::vector> domain_request_handlers; /// List of threads that are pending a response after a sync request. This list is processed in /// a LIFO manner, thus, the last request will be dispatched first. @@ -91,6 +94,16 @@ public: /// TODO(Subv): Find a better name for this. SharedPtr currently_handling; + /// Returns true if the session has been converted to a domain, otherwise False + bool IsDomain() const { + return !domain_request_handlers.empty(); + } + + /// Converts the session to a domain at the end of the current command + void ConvertToDomain() { + convert_to_domain = true; + } + private: ServerSession(); ~ServerSession() override; @@ -102,6 +115,9 @@ private: * @return The created server session */ static ResultVal> Create(std::string name = "Unknown"); + + /// When set to True, converts the session to a domain at the end of the command + bool convert_to_domain{}; }; /** -- cgit v1.2.3 From f0035420d729afec924390dc58760082581c380b Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 18:58:25 -0500 Subject: ipc_helpers: Make interface domain agnostic and add header validation. --- src/core/hle/kernel/hle_ipc.h | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h index 71e5609b8..da8335b35 100644 --- a/src/core/hle/kernel/hle_ipc.h +++ b/src/core/hle/kernel/hle_ipc.h @@ -175,6 +175,18 @@ public: domain_objects.clear(); } + size_t NumMoveObjects() const { + return move_objects.size(); + } + + size_t NumCopyObjects() const { + return copy_objects.size(); + } + + size_t NumDomainObjects() const { + return domain_objects.size(); + } + private: std::array cmd_buf; SharedPtr server_session; @@ -196,7 +208,6 @@ private: unsigned data_payload_offset{}; unsigned buffer_c_offset{}; u32_le command{}; - bool is_domain{}; }; } // namespace Kernel -- cgit v1.2.3 From 1b1d399e5f9018e7e4c5da9dfa0932eacfc392e9 Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 19:52:18 -0500 Subject: hle: Rename RequestBuilder to ResponseBuilder. --- src/core/hle/kernel/server_session.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/server_session.cpp b/src/core/hle/kernel/server_session.cpp index b79bf7bab..7f215d0bb 100644 --- a/src/core/hle/kernel/server_session.cpp +++ b/src/core/hle/kernel/server_session.cpp @@ -82,7 +82,7 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { domain_request_handlers[object_id - 1] = nullptr; - IPC::RequestBuilder rb{context, 2}; + IPC::ResponseBuilder rb{context, 2}; rb.Push(RESULT_SUCCESS); return RESULT_SUCCESS; } -- cgit v1.2.3 From 7588b24f46b52c80018153a37fed214d9856f0f2 Mon Sep 17 00:00:00 2001 From: bunnei Date: Tue, 23 Jan 2018 22:00:17 -0500 Subject: server_session: Fix scenario where all domain handlers are closed. --- src/core/hle/kernel/server_session.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/core/hle/kernel') diff --git a/src/core/hle/kernel/server_session.cpp b/src/core/hle/kernel/server_session.cpp index 7f215d0bb..54481f7f1 100644 --- a/src/core/hle/kernel/server_session.cpp +++ b/src/core/hle/kernel/server_session.cpp @@ -89,9 +89,9 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { } LOG_CRITICAL(IPC, "Unknown domain command=%d", domain_message_header->command.Value()); - UNIMPLEMENTED(); + ASSERT(false); } - return domain_request_handlers.front()->HandleSyncRequest(context); + // If there is no domain header, the regular session handler is used } // If this ServerSession has an associated HLE handler, forward the request to it. @@ -117,7 +117,7 @@ ResultCode ServerSession::HandleSyncRequest(SharedPtr thread) { // end of the command such that only commands following this one are handled as domains if (convert_to_domain) { ASSERT_MSG(domain_request_handlers.empty(), "already a domain"); - domain_request_handlers.push_back(std::move(hle_handler)); + domain_request_handlers = {hle_handler}; convert_to_domain = false; } -- cgit v1.2.3