mirror of
https://github.com/PurpleI2P/i2pd.git
synced 2025-01-22 21:37:17 +01:00
move thread naming to util
Signed-off-by: R4SAS <r4sas@i2pmail.org>
This commit is contained in:
parent
aace200899
commit
3100d4f902
|
@ -9,7 +9,6 @@
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <pthread.h>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
|
@ -1313,7 +1312,8 @@ namespace http {
|
||||||
|
|
||||||
void HTTPServer::Run ()
|
void HTTPServer::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Webconsole");
|
i2p::util::SetThreadName("Webconsole");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||||
#include <boost/property_tree/ini_parser.hpp>
|
#include <boost/property_tree/ini_parser.hpp>
|
||||||
#include <boost/property_tree/json_parser.hpp>
|
#include <boost/property_tree/json_parser.hpp>
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
#include "Crypto.h"
|
#include "Crypto.h"
|
||||||
#include "FS.h"
|
#include "FS.h"
|
||||||
|
@ -132,7 +131,8 @@ namespace client
|
||||||
|
|
||||||
void I2PControlService::Run ()
|
void I2PControlService::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "I2PC");
|
i2p::util::SetThreadName("I2PC");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#ifdef USE_UPNP
|
#ifdef USE_UPNP
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
#include <boost/thread/thread.hpp>
|
#include <boost/thread/thread.hpp>
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
|
@ -61,7 +60,8 @@ namespace transport
|
||||||
|
|
||||||
void UPnP::Run ()
|
void UPnP::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "UPnP");
|
i2p::util::SetThreadName("UPnP");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include <pthread.h>
|
#include "util.h"
|
||||||
|
|
||||||
//for std::transform
|
//for std::transform
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
@ -180,7 +180,8 @@ namespace log {
|
||||||
|
|
||||||
void Log::Run ()
|
void Log::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Logging");
|
i2p::util::SetThreadName("Logging");
|
||||||
|
|
||||||
Reopen ();
|
Reopen ();
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <pthread.h>
|
|
||||||
|
|
||||||
#include "I2PEndian.h"
|
#include "I2PEndian.h"
|
||||||
#include "Base.h"
|
#include "Base.h"
|
||||||
|
@ -27,6 +26,7 @@
|
||||||
#include "ECIESX25519AEADRatchetSession.h"
|
#include "ECIESX25519AEADRatchetSession.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "NetDb.hpp"
|
#include "NetDb.hpp"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
using namespace i2p::transport;
|
using namespace i2p::transport;
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ namespace data
|
||||||
|
|
||||||
void NetDb::Run ()
|
void NetDb::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "NetDB");
|
i2p::util::SetThreadName("NetDB");
|
||||||
|
|
||||||
uint32_t lastSave = 0, lastPublish = 0, lastExploratory = 0, lastManageRequest = 0, lastDestinationCleanup = 0;
|
uint32_t lastSave = 0, lastPublish = 0, lastExploratory = 0, lastManageRequest = 0, lastDestinationCleanup = 0;
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
|
@ -116,7 +116,7 @@ namespace data
|
||||||
break;
|
break;
|
||||||
case eI2NPDeliveryStatus:
|
case eI2NPDeliveryStatus:
|
||||||
HandleDeliveryStatusMsg (msg);
|
HandleDeliveryStatusMsg (msg);
|
||||||
break;
|
break;
|
||||||
case eI2NPDummyMsg:
|
case eI2NPDummyMsg:
|
||||||
// plain RouterInfo from NTCP2 with flags for now
|
// plain RouterInfo from NTCP2 with flags for now
|
||||||
HandleNTCP2RouterInfoMsg (msg);
|
HandleNTCP2RouterInfoMsg (msg);
|
||||||
|
@ -158,12 +158,12 @@ namespace data
|
||||||
if (!m_HiddenMode && i2p::transport::transports.IsOnline ())
|
if (!m_HiddenMode && i2p::transport::transports.IsOnline ())
|
||||||
{
|
{
|
||||||
bool publish = false;
|
bool publish = false;
|
||||||
if (m_PublishReplyToken)
|
if (m_PublishReplyToken)
|
||||||
{
|
{
|
||||||
if (ts - lastPublish >= NETDB_PUBLISH_CONFIRMATION_TIMEOUT) publish = true;
|
if (ts - lastPublish >= NETDB_PUBLISH_CONFIRMATION_TIMEOUT) publish = true;
|
||||||
}
|
}
|
||||||
else if (i2p::context.GetLastUpdateTime () > lastPublish ||
|
else if (i2p::context.GetLastUpdateTime () > lastPublish ||
|
||||||
ts - lastPublish >= NETDB_PUBLISH_INTERVAL) publish = true;
|
ts - lastPublish >= NETDB_PUBLISH_INTERVAL) publish = true;
|
||||||
if (publish) // update timestamp and publish
|
if (publish) // update timestamp and publish
|
||||||
{
|
{
|
||||||
i2p::context.UpdateTimestamp (ts);
|
i2p::context.UpdateTimestamp (ts);
|
||||||
|
|
|
@ -7,12 +7,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <pthread.h>
|
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "Timestamp.h"
|
#include "Timestamp.h"
|
||||||
#include "RouterContext.h"
|
#include "RouterContext.h"
|
||||||
#include "NetDb.hpp"
|
#include "NetDb.hpp"
|
||||||
#include "SSU.h"
|
#include "SSU.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <boost/winapi/error_codes.hpp>
|
#include <boost/winapi/error_codes.hpp>
|
||||||
|
@ -143,7 +143,7 @@ namespace transport
|
||||||
|
|
||||||
void SSUServer::Run ()
|
void SSUServer::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "SSU");
|
i2p::util::SetThreadName("SSU");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
|
@ -160,7 +160,7 @@ namespace transport
|
||||||
|
|
||||||
void SSUServer::RunReceivers ()
|
void SSUServer::RunReceivers ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "SSUv4");
|
i2p::util::SetThreadName("SSUv4");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
|
@ -184,7 +184,7 @@ namespace transport
|
||||||
|
|
||||||
void SSUServer::RunReceiversV6 ()
|
void SSUServer::RunReceiversV6 ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "SSUv6");
|
i2p::util::SetThreadName("SSUv6");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
|
|
|
@ -14,11 +14,11 @@
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
#include <pthread.h>
|
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "I2PEndian.h"
|
#include "I2PEndian.h"
|
||||||
#include "Timestamp.h"
|
#include "Timestamp.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#ifndef _WIN64
|
#ifndef _WIN64
|
||||||
|
@ -149,7 +149,8 @@ namespace util
|
||||||
|
|
||||||
void NTPTimeSync::Run ()
|
void NTPTimeSync::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Timesync");
|
i2p::util::SetThreadName("Timesync");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* See full license text in LICENSE file at top of project tree
|
* See full license text in LICENSE file at top of project tree
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <pthread.h>
|
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "Crypto.h"
|
#include "Crypto.h"
|
||||||
#include "RouterContext.h"
|
#include "RouterContext.h"
|
||||||
|
@ -15,6 +14,7 @@
|
||||||
#include "Transports.h"
|
#include "Transports.h"
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "HTTP.h"
|
#include "HTTP.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
using namespace i2p::data;
|
using namespace i2p::data;
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ namespace transport
|
||||||
template<typename Keys>
|
template<typename Keys>
|
||||||
void EphemeralKeysSupplier<Keys>::Run ()
|
void EphemeralKeysSupplier<Keys>::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Ephemerals");
|
i2p::util::SetThreadName("Ephemerals");
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
|
@ -275,7 +275,7 @@ namespace transport
|
||||||
|
|
||||||
void Transports::Run ()
|
void Transports::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Transports");
|
i2p::util::SetThreadName("Transports");
|
||||||
|
|
||||||
while (m_IsRunning && m_Service)
|
while (m_IsRunning && m_Service)
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
#include "I2PEndian.h"
|
#include "I2PEndian.h"
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <pthread.h>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "Crypto.h"
|
#include "Crypto.h"
|
||||||
|
@ -23,6 +22,7 @@
|
||||||
#include "Config.h"
|
#include "Config.h"
|
||||||
#include "Tunnel.h"
|
#include "Tunnel.h"
|
||||||
#include "TunnelPool.h"
|
#include "TunnelPool.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
namespace i2p
|
namespace i2p
|
||||||
{
|
{
|
||||||
|
@ -473,7 +473,7 @@ namespace tunnel
|
||||||
|
|
||||||
void Tunnels::Run ()
|
void Tunnels::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), "Tunnels");
|
i2p::util::SetThreadName("Tunnels");
|
||||||
std::this_thread::sleep_for (std::chrono::seconds(1)); // wait for other parts are ready
|
std::this_thread::sleep_for (std::chrono::seconds(1)); // wait for other parts are ready
|
||||||
|
|
||||||
uint64_t lastTs = 0, lastPoolsTs = 0;
|
uint64_t lastTs = 0, lastPoolsTs = 0;
|
||||||
|
|
|
@ -13,6 +13,15 @@
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
|
|
||||||
|
#if not defined (__FreeBSD__)
|
||||||
|
#include <pthread.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(__OpenBSD__) || defined(__FreeBSD__)
|
||||||
|
#include <pthread_np.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
@ -32,7 +41,7 @@
|
||||||
|
|
||||||
// inet_pton exists Windows since Vista, but XP doesn't have that function!
|
// inet_pton exists Windows since Vista, but XP doesn't have that function!
|
||||||
// This function was written by Petar Korponai?. See http://stackoverflow.com/questions/15660203/inet-pton-identifier-not-found
|
// This function was written by Petar Korponai?. See http://stackoverflow.com/questions/15660203/inet-pton-identifier-not-found
|
||||||
int inet_pton_xp(int af, const char *src, void *dst)
|
int inet_pton_xp (int af, const char *src, void *dst)
|
||||||
{
|
{
|
||||||
struct sockaddr_storage ss;
|
struct sockaddr_storage ss;
|
||||||
int size = sizeof (ss);
|
int size = sizeof (ss);
|
||||||
|
@ -94,7 +103,8 @@ namespace util
|
||||||
|
|
||||||
void RunnableService::Run ()
|
void RunnableService::Run ()
|
||||||
{
|
{
|
||||||
pthread_setname_np(pthread_self(), m_Name.c_str());
|
SetThreadName(m_Name.c_str());
|
||||||
|
|
||||||
while (m_IsRunning)
|
while (m_IsRunning)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -108,10 +118,20 @@ namespace util
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetThreadName (const char *name) {
|
||||||
|
#if defined (__APPLE__)
|
||||||
|
pthread_setname_np(name);
|
||||||
|
#elif defined(__FreeBSD__)
|
||||||
|
pthread_set_name_np(pthread_self(), name)
|
||||||
|
#else
|
||||||
|
pthread_setname_np(pthread_self(), name);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
namespace net
|
namespace net
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
bool IsWindowsXPorLater()
|
bool IsWindowsXPorLater ()
|
||||||
{
|
{
|
||||||
static bool isRequested = false;
|
static bool isRequested = false;
|
||||||
static bool isXP = false;
|
static bool isXP = false;
|
||||||
|
@ -130,7 +150,7 @@ namespace net
|
||||||
return isXP;
|
return isXP;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GetMTUWindowsIpv4(sockaddr_in inputAddress, int fallback)
|
int GetMTUWindowsIpv4 (sockaddr_in inputAddress, int fallback)
|
||||||
{
|
{
|
||||||
ULONG outBufLen = 0;
|
ULONG outBufLen = 0;
|
||||||
PIP_ADAPTER_ADDRESSES pAddresses = nullptr;
|
PIP_ADAPTER_ADDRESSES pAddresses = nullptr;
|
||||||
|
@ -184,7 +204,7 @@ namespace net
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GetMTUWindowsIpv6(sockaddr_in6 inputAddress, int fallback)
|
int GetMTUWindowsIpv6 (sockaddr_in6 inputAddress, int fallback)
|
||||||
{
|
{
|
||||||
ULONG outBufLen = 0;
|
ULONG outBufLen = 0;
|
||||||
PIP_ADAPTER_ADDRESSES pAddresses = nullptr;
|
PIP_ADAPTER_ADDRESSES pAddresses = nullptr;
|
||||||
|
@ -249,7 +269,7 @@ namespace net
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
int GetMTUWindows(const boost::asio::ip::address& localAddress, int fallback)
|
int GetMTUWindows (const boost::asio::ip::address& localAddress, int fallback)
|
||||||
{
|
{
|
||||||
#ifdef UNICODE
|
#ifdef UNICODE
|
||||||
string localAddress_temporary = localAddress.to_string();
|
string localAddress_temporary = localAddress.to_string();
|
||||||
|
@ -281,7 +301,7 @@ namespace net
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else // assume unix
|
#else // assume unix
|
||||||
int GetMTUUnix(const boost::asio::ip::address& localAddress, int fallback)
|
int GetMTUUnix (const boost::asio::ip::address& localAddress, int fallback)
|
||||||
{
|
{
|
||||||
ifaddrs* ifaddr, *ifa = nullptr;
|
ifaddrs* ifaddr, *ifa = nullptr;
|
||||||
if(getifaddrs(&ifaddr) == -1)
|
if(getifaddrs(&ifaddr) == -1)
|
||||||
|
@ -336,7 +356,7 @@ namespace net
|
||||||
}
|
}
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
int GetMTU(const boost::asio::ip::address& localAddress)
|
int GetMTU (const boost::asio::ip::address& localAddress)
|
||||||
{
|
{
|
||||||
int fallback = localAddress.is_v6 () ? 1280 : 620; // fallback MTU
|
int fallback = localAddress.is_v6 () ? 1280 : 620; // fallback MTU
|
||||||
|
|
||||||
|
@ -348,7 +368,7 @@ namespace net
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
const boost::asio::ip::address GetInterfaceAddress(const std::string & ifname, bool ipv6)
|
const boost::asio::ip::address GetInterfaceAddress (const std::string & ifname, bool ipv6)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
LogPrint(eLogError, "NetIface: cannot get address by interface name, not implemented on WIN32");
|
LogPrint(eLogError, "NetIface: cannot get address by interface name, not implemented on WIN32");
|
||||||
|
@ -396,7 +416,7 @@ namespace net
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsInReservedRange(const boost::asio::ip::address& host) {
|
bool IsInReservedRange (const boost::asio::ip::address& host) {
|
||||||
// https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
// https://en.wikipedia.org/wiki/Reserved_IP_addresses
|
||||||
if(host.is_v4())
|
if(host.is_v4())
|
||||||
{
|
{
|
||||||
|
|
|
@ -168,11 +168,13 @@ namespace util
|
||||||
boost::asio::io_service::work m_Work;
|
boost::asio::io_service::work m_Work;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void SetThreadName (const char *name);
|
||||||
|
|
||||||
namespace net
|
namespace net
|
||||||
{
|
{
|
||||||
int GetMTU (const boost::asio::ip::address& localAddress);
|
int GetMTU (const boost::asio::ip::address& localAddress);
|
||||||
const boost::asio::ip::address GetInterfaceAddress(const std::string & ifname, bool ipv6=false);
|
const boost::asio::ip::address GetInterfaceAddress (const std::string & ifname, bool ipv6=false);
|
||||||
bool IsInReservedRange(const boost::asio::ip::address& host);
|
bool IsInReservedRange (const boost::asio::ip::address& host);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <pthread.h>
|
|
||||||
#include "Base.h"
|
#include "Base.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
#include "Destination.h"
|
#include "Destination.h"
|
||||||
#include "ClientContext.h"
|
#include "ClientContext.h"
|
||||||
#include "I2PTunnel.h"
|
#include "I2PTunnel.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
namespace i2p
|
namespace i2p
|
||||||
{
|
{
|
||||||
|
@ -941,8 +941,8 @@ namespace client
|
||||||
}
|
}
|
||||||
|
|
||||||
void I2PUDPClientTunnel::TryResolving() {
|
void I2PUDPClientTunnel::TryResolving() {
|
||||||
|
i2p::util::SetThreadName("UDP Resolver");
|
||||||
LogPrint(eLogInfo, "UDP Tunnel: Trying to resolve ", m_RemoteDest);
|
LogPrint(eLogInfo, "UDP Tunnel: Trying to resolve ", m_RemoteDest);
|
||||||
pthread_setname_np(pthread_self(), "UDP Resolver");
|
|
||||||
|
|
||||||
std::shared_ptr<const Address> addr;
|
std::shared_ptr<const Address> addr;
|
||||||
while(!(addr = context.GetAddressBook().GetAddress(m_RemoteDest)) && !m_cancel_resolve)
|
while(!(addr = context.GetAddressBook().GetAddress(m_RemoteDest)) && !m_cancel_resolve)
|
||||||
|
|
Loading…
Reference in a new issue