mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-19 05:45:05 +01:00
multiprocess: Add Ipc interface implementation
This commit is contained in:
parent
745c9cebd5
commit
10afdf0280
@ -74,6 +74,7 @@ EXTRA_LIBRARIES += \
|
||||
$(LIBBITCOIN_CONSENSUS) \
|
||||
$(LIBBITCOIN_SERVER) \
|
||||
$(LIBBITCOIN_CLI) \
|
||||
$(LIBBITCOIN_IPC) \
|
||||
$(LIBBITCOIN_WALLET) \
|
||||
$(LIBBITCOIN_WALLET_TOOL) \
|
||||
$(LIBBITCOIN_ZMQ)
|
||||
@ -301,6 +302,8 @@ obj/build.h: FORCE
|
||||
"$(abs_top_srcdir)"
|
||||
libbitcoin_util_a-clientversion.$(OBJEXT): obj/build.h
|
||||
|
||||
ipc/capnp/libbitcoin_ipc_a-ipc.$(OBJEXT): $(libbitcoin_ipc_mpgen_input:=.h)
|
||||
|
||||
# server: shared between bitcoind and bitcoin-qt
|
||||
# Contains code accessing mempool and chain state that is meant to be separated
|
||||
# from wallet and gui code (see node/README.md). Shared code should go in
|
||||
@ -647,7 +650,7 @@ bitcoin_node_SOURCES = $(bitcoin_daemon_sources)
|
||||
bitcoin_node_CPPFLAGS = $(bitcoin_bin_cppflags)
|
||||
bitcoin_node_CXXFLAGS = $(bitcoin_bin_cxxflags)
|
||||
bitcoin_node_LDFLAGS = $(bitcoin_bin_ldflags)
|
||||
bitcoin_node_LDADD = $(LIBBITCOIN_SERVER) $(bitcoin_bin_ldadd)
|
||||
bitcoin_node_LDADD = $(LIBBITCOIN_SERVER) $(bitcoin_bin_ldadd) $(LIBBITCOIN_IPC) $(LIBMULTIPROCESS_LIBS)
|
||||
|
||||
# bitcoin-cli binary #
|
||||
bitcoin_cli_SOURCES = bitcoin-cli.cpp
|
||||
@ -811,6 +814,38 @@ if HARDEN
|
||||
$(AM_V_at) OBJDUMP=$(OBJDUMP) OTOOL=$(OTOOL) $(PYTHON) $(top_srcdir)/contrib/devtools/security-check.py $(bin_PROGRAMS)
|
||||
endif
|
||||
|
||||
libbitcoin_ipc_mpgen_input = \
|
||||
ipc/capnp/init.capnp
|
||||
EXTRA_DIST += $(libbitcoin_ipc_mpgen_input)
|
||||
%.capnp:
|
||||
|
||||
if BUILD_MULTIPROCESS
|
||||
LIBBITCOIN_IPC=libbitcoin_ipc.a
|
||||
libbitcoin_ipc_a_SOURCES = \
|
||||
ipc/capnp/init-types.h \
|
||||
ipc/capnp/protocol.cpp \
|
||||
ipc/capnp/protocol.h \
|
||||
ipc/exception.h \
|
||||
ipc/interfaces.cpp \
|
||||
ipc/process.cpp \
|
||||
ipc/process.h \
|
||||
ipc/protocol.h
|
||||
libbitcoin_ipc_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES)
|
||||
libbitcoin_ipc_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS) $(LIBMULTIPROCESS_CFLAGS)
|
||||
|
||||
include $(MPGEN_PREFIX)/include/mpgen.mk
|
||||
libbitcoin_ipc_mpgen_output = \
|
||||
$(libbitcoin_ipc_mpgen_input:=.c++) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.h) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.proxy-client.c++) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.proxy-server.c++) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.proxy-types.c++) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.proxy-types.h) \
|
||||
$(libbitcoin_ipc_mpgen_input:=.proxy.h)
|
||||
nodist_libbitcoin_ipc_a_SOURCES = $(libbitcoin_ipc_mpgen_output)
|
||||
CLEANFILES += $(libbitcoin_ipc_mpgen_output)
|
||||
endif
|
||||
|
||||
if EMBEDDED_LEVELDB
|
||||
include Makefile.crc32c.include
|
||||
include Makefile.leveldb.include
|
||||
|
2
src/ipc/capnp/.gitignore
vendored
Normal file
2
src/ipc/capnp/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# capnp generated files
|
||||
*.capnp.*
|
7
src/ipc/capnp/init-types.h
Normal file
7
src/ipc/capnp/init-types.h
Normal file
@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_CAPNP_INIT_TYPES_H
|
||||
#define BITCOIN_IPC_CAPNP_INIT_TYPES_H
|
||||
#endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H
|
16
src/ipc/capnp/init.capnp
Normal file
16
src/ipc/capnp/init.capnp
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2021 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@0xf2c5cfa319406aa6;
|
||||
|
||||
using Cxx = import "/capnp/c++.capnp";
|
||||
$Cxx.namespace("ipc::capnp::messages");
|
||||
|
||||
using Proxy = import "/mp/proxy.capnp";
|
||||
$Proxy.include("interfaces/init.h");
|
||||
$Proxy.includeTypes("ipc/capnp/init-types.h");
|
||||
|
||||
interface Init $Proxy.wrap("interfaces::Init") {
|
||||
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
|
||||
}
|
90
src/ipc/capnp/protocol.cpp
Normal file
90
src/ipc/capnp/protocol.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/init.h>
|
||||
#include <ipc/capnp/init.capnp.h>
|
||||
#include <ipc/capnp/init.capnp.proxy.h>
|
||||
#include <ipc/capnp/protocol.h>
|
||||
#include <ipc/exception.h>
|
||||
#include <ipc/protocol.h>
|
||||
#include <kj/async.h>
|
||||
#include <logging.h>
|
||||
#include <mp/proxy-io.h>
|
||||
#include <mp/proxy-types.h>
|
||||
#include <mp/util.h>
|
||||
#include <util/threadnames.h>
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ipc {
|
||||
namespace capnp {
|
||||
namespace {
|
||||
void IpcLogFn(bool raise, std::string message)
|
||||
{
|
||||
LogPrint(BCLog::IPC, "%s\n", message);
|
||||
if (raise) throw Exception(message);
|
||||
}
|
||||
|
||||
class CapnpProtocol : public Protocol
|
||||
{
|
||||
public:
|
||||
~CapnpProtocol() noexcept(true)
|
||||
{
|
||||
if (m_loop) {
|
||||
std::unique_lock<std::mutex> lock(m_loop->m_mutex);
|
||||
m_loop->removeClient(lock);
|
||||
}
|
||||
if (m_loop_thread.joinable()) m_loop_thread.join();
|
||||
assert(!m_loop);
|
||||
};
|
||||
std::unique_ptr<interfaces::Init> connect(int fd, const char* exe_name) override
|
||||
{
|
||||
startLoop(exe_name);
|
||||
return mp::ConnectStream<messages::Init>(*m_loop, fd);
|
||||
}
|
||||
void serve(int fd, const char* exe_name, interfaces::Init& init) override
|
||||
{
|
||||
assert(!m_loop);
|
||||
mp::g_thread_context.thread_name = mp::ThreadName(exe_name);
|
||||
m_loop.emplace(exe_name, &IpcLogFn, nullptr);
|
||||
mp::ServeStream<messages::Init>(*m_loop, fd, init);
|
||||
m_loop->loop();
|
||||
m_loop.reset();
|
||||
}
|
||||
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
|
||||
{
|
||||
mp::ProxyTypeRegister::types().at(type)(iface).cleanup.emplace_back(std::move(cleanup));
|
||||
}
|
||||
void startLoop(const char* exe_name)
|
||||
{
|
||||
if (m_loop) return;
|
||||
std::promise<void> promise;
|
||||
m_loop_thread = std::thread([&] {
|
||||
util::ThreadRename("capnp-loop");
|
||||
m_loop.emplace(exe_name, &IpcLogFn, nullptr);
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_loop->m_mutex);
|
||||
m_loop->addClient(lock);
|
||||
}
|
||||
promise.set_value();
|
||||
m_loop->loop();
|
||||
m_loop.reset();
|
||||
});
|
||||
promise.get_future().wait();
|
||||
}
|
||||
std::thread m_loop_thread;
|
||||
std::optional<mp::EventLoop> m_loop;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Protocol> MakeCapnpProtocol() { return std::make_unique<CapnpProtocol>(); }
|
||||
} // namespace capnp
|
||||
} // namespace ipc
|
17
src/ipc/capnp/protocol.h
Normal file
17
src/ipc/capnp/protocol.h
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_CAPNP_PROTOCOL_H
|
||||
#define BITCOIN_IPC_CAPNP_PROTOCOL_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace ipc {
|
||||
class Protocol;
|
||||
namespace capnp {
|
||||
std::unique_ptr<Protocol> MakeCapnpProtocol();
|
||||
} // namespace capnp
|
||||
} // namespace ipc
|
||||
|
||||
#endif // BITCOIN_IPC_CAPNP_PROTOCOL_H
|
20
src/ipc/exception.h
Normal file
20
src/ipc/exception.h
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_EXCEPTION_H
|
||||
#define BITCOIN_IPC_EXCEPTION_H
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace ipc {
|
||||
//! Exception class thrown when a call to remote method fails due to an IPC
|
||||
//! error, like a socket getting disconnected.
|
||||
class Exception : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
} // namespace ipc
|
||||
|
||||
#endif // BITCOIN_IPC_EXCEPTION_H
|
77
src/ipc/interfaces.cpp
Normal file
77
src/ipc/interfaces.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <fs.h>
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <ipc/capnp/protocol.h>
|
||||
#include <ipc/process.h>
|
||||
#include <ipc/protocol.h>
|
||||
#include <logging.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/system.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ipc {
|
||||
namespace {
|
||||
class IpcImpl : public interfaces::Ipc
|
||||
{
|
||||
public:
|
||||
IpcImpl(const char* exe_name, const char* process_argv0, interfaces::Init& init)
|
||||
: m_exe_name(exe_name), m_process_argv0(process_argv0), m_init(init),
|
||||
m_protocol(ipc::capnp::MakeCapnpProtocol()), m_process(ipc::MakeProcess())
|
||||
{
|
||||
}
|
||||
std::unique_ptr<interfaces::Init> spawnProcess(const char* new_exe_name) override
|
||||
{
|
||||
int pid;
|
||||
int fd = m_process->spawn(new_exe_name, m_process_argv0, pid);
|
||||
LogPrint(::BCLog::IPC, "Process %s pid %i launched\n", new_exe_name, pid);
|
||||
auto init = m_protocol->connect(fd, m_exe_name);
|
||||
Ipc::addCleanup(*init, [this, new_exe_name, pid] {
|
||||
int status = m_process->waitSpawned(pid);
|
||||
LogPrint(::BCLog::IPC, "Process %s pid %i exited with status %i\n", new_exe_name, pid, status);
|
||||
});
|
||||
return init;
|
||||
}
|
||||
bool startSpawnedProcess(int argc, char* argv[], int& exit_status) override
|
||||
{
|
||||
exit_status = EXIT_FAILURE;
|
||||
int32_t fd = -1;
|
||||
if (!m_process->checkSpawned(argc, argv, fd)) {
|
||||
return false;
|
||||
}
|
||||
m_protocol->serve(fd, m_exe_name, m_init);
|
||||
exit_status = EXIT_SUCCESS;
|
||||
return true;
|
||||
}
|
||||
void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) override
|
||||
{
|
||||
m_protocol->addCleanup(type, iface, std::move(cleanup));
|
||||
}
|
||||
const char* m_exe_name;
|
||||
const char* m_process_argv0;
|
||||
interfaces::Init& m_init;
|
||||
std::unique_ptr<Protocol> m_protocol;
|
||||
std::unique_ptr<Process> m_process;
|
||||
};
|
||||
} // namespace
|
||||
} // namespace ipc
|
||||
|
||||
namespace interfaces {
|
||||
std::unique_ptr<Ipc> MakeIpc(const char* exe_name, const char* process_argv0, Init& init)
|
||||
{
|
||||
return std::make_unique<ipc::IpcImpl>(exe_name, process_argv0, init);
|
||||
}
|
||||
} // namespace interfaces
|
61
src/ipc/process.cpp
Normal file
61
src/ipc/process.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <fs.h>
|
||||
#include <ipc/process.h>
|
||||
#include <ipc/protocol.h>
|
||||
#include <mp/util.h>
|
||||
#include <tinyformat.h>
|
||||
#include <util/strencodings.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <system_error>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ipc {
|
||||
namespace {
|
||||
class ProcessImpl : public Process
|
||||
{
|
||||
public:
|
||||
int spawn(const std::string& new_exe_name, const fs::path& argv0_path, int& pid) override
|
||||
{
|
||||
return mp::SpawnProcess(pid, [&](int fd) {
|
||||
fs::path path = argv0_path;
|
||||
path.remove_filename();
|
||||
path.append(new_exe_name);
|
||||
return std::vector<std::string>{path.string(), "-ipcfd", strprintf("%i", fd)};
|
||||
});
|
||||
}
|
||||
int waitSpawned(int pid) override { return mp::WaitProcess(pid); }
|
||||
bool checkSpawned(int argc, char* argv[], int& fd) override
|
||||
{
|
||||
// If this process was not started with a single -ipcfd argument, it is
|
||||
// not a process spawned by the spawn() call above, so return false and
|
||||
// do not try to serve requests.
|
||||
if (argc != 3 || strcmp(argv[1], "-ipcfd") != 0) {
|
||||
return false;
|
||||
}
|
||||
// If a single -ipcfd argument was provided, return true and get the
|
||||
// file descriptor so Protocol::serve() can be called to handle
|
||||
// requests from the parent process. The -ipcfd argument is not valid
|
||||
// in combination with other arguments because the parent process
|
||||
// should be able to control the child process through the IPC protocol
|
||||
// without passing information out of band.
|
||||
if (!ParseInt32(argv[2], &fd)) {
|
||||
throw std::runtime_error(strprintf("Invalid -ipcfd number '%s'", argv[2]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Process> MakeProcess() { return std::make_unique<ProcessImpl>(); }
|
||||
} // namespace ipc
|
42
src/ipc/process.h
Normal file
42
src/ipc/process.h
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_PROCESS_H
|
||||
#define BITCOIN_IPC_PROCESS_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace ipc {
|
||||
class Protocol;
|
||||
|
||||
//! IPC process interface for spawning bitcoin processes and serving requests
|
||||
//! in processes that have been spawned.
|
||||
//!
|
||||
//! There will be different implementations of this interface depending on the
|
||||
//! platform (e.g. unix, windows).
|
||||
class Process
|
||||
{
|
||||
public:
|
||||
virtual ~Process() = default;
|
||||
|
||||
//! Spawn process and return socket file descriptor for communicating with
|
||||
//! it.
|
||||
virtual int spawn(const std::string& new_exe_name, const fs::path& argv0_path, int& pid) = 0;
|
||||
|
||||
//! Wait for spawned process to exit and return its exit code.
|
||||
virtual int waitSpawned(int pid) = 0;
|
||||
|
||||
//! Parse command line and determine if current process is a spawned child
|
||||
//! process. If so, return true and a file descriptor for communicating
|
||||
//! with the parent process.
|
||||
virtual bool checkSpawned(int argc, char* argv[], int& fd) = 0;
|
||||
};
|
||||
|
||||
//! Constructor for Process interface. Implementation will vary depending on
|
||||
//! the platform (unix or windows).
|
||||
std::unique_ptr<Process> MakeProcess();
|
||||
} // namespace ipc
|
||||
|
||||
#endif // BITCOIN_IPC_PROCESS_H
|
39
src/ipc/protocol.h
Normal file
39
src/ipc/protocol.h
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2021 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_PROTOCOL_H
|
||||
#define BITCOIN_IPC_PROTOCOL_H
|
||||
|
||||
#include <interfaces/init.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <typeindex>
|
||||
|
||||
namespace ipc {
|
||||
//! IPC protocol interface for calling IPC methods over sockets.
|
||||
//!
|
||||
//! There may be different implementations of this interface for different IPC
|
||||
//! protocols (e.g. Cap'n Proto, gRPC, JSON-RPC, or custom protocols).
|
||||
class Protocol
|
||||
{
|
||||
public:
|
||||
virtual ~Protocol() = default;
|
||||
|
||||
//! Return Init interface that forwards requests over given socket descriptor.
|
||||
//! Socket communication is handled on a background thread.
|
||||
virtual std::unique_ptr<interfaces::Init> connect(int fd, const char* exe_name) = 0;
|
||||
|
||||
//! Handle requests on provided socket descriptor, forwarding them to the
|
||||
//! provided Init interface. Socket communication is handled on the
|
||||
//! current thread, and this call blocks until the socket is closed.
|
||||
virtual void serve(int fd, const char* exe_name, interfaces::Init& init) = 0;
|
||||
|
||||
//! Add cleanup callback to interface that will run when the interface is
|
||||
//! deleted.
|
||||
virtual void addCleanup(std::type_index type, void* iface, std::function<void()> cleanup) = 0;
|
||||
};
|
||||
} // namespace ipc
|
||||
|
||||
#endif // BITCOIN_IPC_PROTOCOL_H
|
@ -157,6 +157,7 @@ const CLogCategoryDesc LogCategories[] =
|
||||
{BCLog::LEVELDB, "leveldb"},
|
||||
{BCLog::VALIDATION, "validation"},
|
||||
{BCLog::I2P, "i2p"},
|
||||
{BCLog::IPC, "ipc"},
|
||||
{BCLog::ALL, "1"},
|
||||
{BCLog::ALL, "all"},
|
||||
};
|
||||
|
@ -58,6 +58,7 @@ namespace BCLog {
|
||||
LEVELDB = (1 << 20),
|
||||
VALIDATION = (1 << 21),
|
||||
I2P = (1 << 22),
|
||||
IPC = (1 << 23),
|
||||
ALL = ~(uint32_t)0,
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ REGEXP_EXCLUDE_FILES_WITH_PREFIX="src/(crypto/ctaes/|leveldb/|crc32c/|secp256k1/
|
||||
EXIT_CODE=0
|
||||
for HEADER_FILE in $(git ls-files -- "*.h" | grep -vE "^${REGEXP_EXCLUDE_FILES_WITH_PREFIX}")
|
||||
do
|
||||
HEADER_ID_BASE=$(cut -f2- -d/ <<< "${HEADER_FILE}" | sed "s/\.h$//g" | tr / _ | tr "[:lower:]" "[:upper:]")
|
||||
HEADER_ID_BASE=$(cut -f2- -d/ <<< "${HEADER_FILE}" | sed "s/\.h$//g" | tr / _ | tr - _ | tr "[:lower:]" "[:upper:]")
|
||||
HEADER_ID="${HEADER_ID_PREFIX}${HEADER_ID_BASE}${HEADER_ID_SUFFIX}"
|
||||
if [[ $(grep -cE "^#(ifndef|define) ${HEADER_ID}" "${HEADER_FILE}") != 2 ]]; then
|
||||
echo "${HEADER_FILE} seems to be missing the expected include guard:"
|
||||
|
Loading…
Reference in New Issue
Block a user