IPC: add support for multiple backends to QSystemSemaphore
Simultaneously. Change-Id: If4c23ea3719947d790d4fffd17152a29e6c217d3 Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io> Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
parent
968d9584ff
commit
32a06e9830
@ -1139,7 +1139,13 @@ qt_internal_extend_target(Core CONDITION UNIX
|
||||
SOURCES
|
||||
ipc/qsharedmemory_posix.cpp
|
||||
ipc/qsharedmemory_systemv.cpp
|
||||
)
|
||||
qt_internal_extend_target(Core CONDITION QT_FEATURE_posix_sem
|
||||
SOURCES
|
||||
ipc/qsystemsemaphore_posix.cpp
|
||||
)
|
||||
qt_internal_extend_target(Core CONDITION QT_FEATURE_sysv_sem
|
||||
SOURCES
|
||||
ipc/qsystemsemaphore_systemv.cpp
|
||||
)
|
||||
|
||||
|
@ -3,14 +3,33 @@
|
||||
|
||||
#include "qsystemsemaphore.h"
|
||||
#include "qsystemsemaphore_p.h"
|
||||
#include <qglobal.h>
|
||||
|
||||
#if QT_CONFIG(systemsemaphore)
|
||||
#include <memory>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace QtIpcCommon;
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
#if QT_CONFIG(systemsemaphore)
|
||||
#if __cplusplus >= 202002L
|
||||
using std::construct_at;
|
||||
#else
|
||||
template <typename T> static void construct_at(T *ptr)
|
||||
{
|
||||
new (ptr) T;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline void QSystemSemaphorePrivate::constructBackend()
|
||||
{
|
||||
visit([](auto p) { construct_at(p); });
|
||||
}
|
||||
|
||||
inline void QSystemSemaphorePrivate::destructBackend()
|
||||
{
|
||||
visit([](auto p) { std::destroy_at(p); });
|
||||
}
|
||||
|
||||
/*!
|
||||
\class QSystemSemaphore
|
||||
@ -113,7 +132,7 @@ QSystemSemaphore::QSystemSemaphore(const QString &key, int initialValue, AccessM
|
||||
\sa acquire(), key()
|
||||
*/
|
||||
QSystemSemaphore::QSystemSemaphore(const QNativeIpcKey &key, int initialValue, AccessMode mode)
|
||||
: d(new QSystemSemaphorePrivate)
|
||||
: d(new QSystemSemaphorePrivate(key.type()))
|
||||
{
|
||||
setNativeKey(key, initialValue, mode);
|
||||
}
|
||||
@ -187,7 +206,15 @@ void QSystemSemaphore::setNativeKey(const QNativeIpcKey &key, int initialValue,
|
||||
|
||||
d->clearError();
|
||||
d->cleanHandle();
|
||||
d->nativeKey = key;
|
||||
if (key.type() == d->nativeKey.type()) {
|
||||
// we can reuse the backend
|
||||
d->nativeKey = key;
|
||||
} else {
|
||||
// we must recreate the backend
|
||||
d->destructBackend();
|
||||
d->nativeKey = key;
|
||||
d->constructBackend();
|
||||
}
|
||||
d->initialValue = initialValue;
|
||||
d->handle(mode);
|
||||
|
||||
@ -373,7 +400,13 @@ void QSystemSemaphorePrivate::setUnixErrorString(QLatin1StringView function)
|
||||
|
||||
bool QSystemSemaphore::isKeyTypeSupported(QNativeIpcKey::Type type)
|
||||
{
|
||||
return QSystemSemaphorePrivate::DefaultBackend::supports(type);
|
||||
if (!isIpcSupported(IpcType::SystemSemaphore, type))
|
||||
return false;
|
||||
using Variant = decltype(QSystemSemaphorePrivate::backend);
|
||||
return Variant::staticVisit(type, [](auto ptr) {
|
||||
using Impl = std::decay_t<decltype(*ptr)>;
|
||||
return Impl::runtimeSupportCheck();
|
||||
});
|
||||
}
|
||||
|
||||
QNativeIpcKey QSystemSemaphore::platformSafeKey(const QString &key, QNativeIpcKey::Type type)
|
||||
@ -386,8 +419,8 @@ QNativeIpcKey QSystemSemaphore::legacyNativeKey(const QString &key, QNativeIpcKe
|
||||
return { legacyPlatformSafeKey(key, IpcType::SystemSemaphore, type), type };
|
||||
}
|
||||
|
||||
#endif // QT_CONFIG(systemsemaphore)
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#include "moc_qsystemsemaphore.cpp"
|
||||
|
||||
#endif // QT_CONFIG(systemsemaphore)
|
||||
|
@ -42,8 +42,10 @@ class QSystemSemaphorePrivate;
|
||||
|
||||
struct QSystemSemaphorePosix
|
||||
{
|
||||
static constexpr bool Enabled = QT_CONFIG(posix_sem);
|
||||
static bool supports(QNativeIpcKey::Type type)
|
||||
{ return type == QNativeIpcKey::Type::PosixRealtime; }
|
||||
static bool runtimeSupportCheck();
|
||||
|
||||
bool handle(QSystemSemaphorePrivate *self, QSystemSemaphore::AccessMode mode);
|
||||
void cleanHandle(QSystemSemaphorePrivate *self);
|
||||
@ -55,8 +57,10 @@ struct QSystemSemaphorePosix
|
||||
|
||||
struct QSystemSemaphoreSystemV
|
||||
{
|
||||
static constexpr bool Enabled = QT_CONFIG(sysv_sem);
|
||||
static bool supports(QNativeIpcKey::Type type)
|
||||
{ return quint16(type) <= 0xff; }
|
||||
static bool runtimeSupportCheck();
|
||||
|
||||
#if QT_CONFIG(sysv_sem)
|
||||
key_t handle(QSystemSemaphorePrivate *self, QSystemSemaphore::AccessMode mode);
|
||||
@ -73,10 +77,16 @@ struct QSystemSemaphoreSystemV
|
||||
|
||||
struct QSystemSemaphoreWin32
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
static constexpr bool Enabled = true;
|
||||
#else
|
||||
static constexpr bool Enabled = false;
|
||||
#endif
|
||||
static bool supports(QNativeIpcKey::Type type)
|
||||
{ return type == QNativeIpcKey::Type::Windows; }
|
||||
static bool runtimeSupportCheck() { return Enabled; }
|
||||
|
||||
//#ifdef Q_OS_WIN32 but there's nothing Windows-specific in the header
|
||||
// we can declare the members without the #if
|
||||
Qt::HANDLE handle(QSystemSemaphorePrivate *self, QSystemSemaphore::AccessMode mode);
|
||||
void cleanHandle(QSystemSemaphorePrivate *self);
|
||||
bool modifySemaphore(QSystemSemaphorePrivate *self, int count);
|
||||
@ -87,6 +97,10 @@ struct QSystemSemaphoreWin32
|
||||
class QSystemSemaphorePrivate
|
||||
{
|
||||
public:
|
||||
QSystemSemaphorePrivate(QNativeIpcKey::Type type) : nativeKey(type)
|
||||
{ constructBackend(); }
|
||||
~QSystemSemaphorePrivate() { destructBackend(); }
|
||||
|
||||
void setWindowsErrorString(QLatin1StringView function); // Windows only
|
||||
void setUnixErrorString(QLatin1StringView function);
|
||||
inline void setError(QSystemSemaphore::SystemSemaphoreError e, const QString &message)
|
||||
@ -99,26 +113,34 @@ public:
|
||||
int initialValue;
|
||||
QSystemSemaphore::SystemSemaphoreError error = QSystemSemaphore::NoError;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
using DefaultBackend = QSystemSemaphoreWin32;
|
||||
#elif defined(QT_POSIX_IPC)
|
||||
using DefaultBackend = QSystemSemaphorePosix;
|
||||
#else
|
||||
using DefaultBackend = QSystemSemaphoreSystemV;
|
||||
#endif
|
||||
DefaultBackend backend;
|
||||
union Backend {
|
||||
Backend() {}
|
||||
~Backend() {}
|
||||
QSystemSemaphorePosix posix;
|
||||
QSystemSemaphoreSystemV sysv;
|
||||
QSystemSemaphoreWin32 win32;
|
||||
};
|
||||
QtIpcCommon::IpcStorageVariant<&Backend::posix, &Backend::sysv, &Backend::win32> backend;
|
||||
|
||||
void constructBackend();
|
||||
void destructBackend();
|
||||
|
||||
template <typename Lambda> auto visit(const Lambda &lambda)
|
||||
{
|
||||
return backend.visit(nativeKey.type(), lambda);
|
||||
}
|
||||
|
||||
void handle(QSystemSemaphore::AccessMode mode)
|
||||
{
|
||||
backend.handle(this, mode);
|
||||
visit([=](auto p) { p->handle(this, mode); });
|
||||
}
|
||||
void cleanHandle()
|
||||
{
|
||||
backend.cleanHandle(this);
|
||||
visit([=](auto p) { p->cleanHandle(this); });
|
||||
}
|
||||
bool modifySemaphore(int count)
|
||||
{
|
||||
return backend.modifySemaphore(this, count);
|
||||
return visit([=](auto p) { return p->modifySemaphore(this, count); });
|
||||
}
|
||||
|
||||
QString legacyKey; // deprecated
|
||||
|
@ -35,6 +35,15 @@ QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
bool QSystemSemaphorePosix::runtimeSupportCheck()
|
||||
{
|
||||
static const bool result = []() {
|
||||
sem_open("/", 0, 0, 0); // this WILL fail
|
||||
return errno != ENOSYS;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool QSystemSemaphorePosix::handle(QSystemSemaphorePrivate *self, QSystemSemaphore::AccessMode mode)
|
||||
{
|
||||
if (semaphore != SEM_FAILED)
|
||||
|
@ -33,6 +33,19 @@ QT_BEGIN_NAMESPACE
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
bool QSystemSemaphoreSystemV::runtimeSupportCheck()
|
||||
{
|
||||
#if defined(Q_OS_DARWIN)
|
||||
if (qt_apple_isSandboxed())
|
||||
return false;
|
||||
#endif
|
||||
static const bool result = []() {
|
||||
semget(IPC_PRIVATE, -1, 0); // this will fail
|
||||
return errno != ENOSYS;
|
||||
}();
|
||||
return result;
|
||||
}
|
||||
|
||||
/*!
|
||||
\internal
|
||||
|
||||
|
@ -63,6 +63,61 @@ bool isIpcSupportedAtRuntime(IpcType type, QNativeIpcKey::Type);
|
||||
static constexpr auto isIpcSupportedAtRuntime = isIpcSupported;
|
||||
#endif
|
||||
|
||||
template <auto Member1, auto... Members> class IpcStorageVariant
|
||||
{
|
||||
template <typename T, typename C> static C extractClass(T C::*);
|
||||
template <typename T, typename C> static T extractObject(T C::*);
|
||||
|
||||
template <auto M>
|
||||
static constexpr bool IsEnabled = decltype(extractObject(M))::Enabled;
|
||||
|
||||
static_assert(std::is_member_object_pointer_v<decltype(Member1)>);
|
||||
using StorageType = decltype(extractClass(Member1));
|
||||
StorageType d;
|
||||
|
||||
public:
|
||||
template <typename Lambda> static auto
|
||||
visit_internal(StorageType &storage, QNativeIpcKey::Type keyType, const Lambda &lambda)
|
||||
{
|
||||
if constexpr ((IsEnabled<Member1> || ... || IsEnabled<Members>)) {
|
||||
if constexpr (IsEnabled<Member1>) {
|
||||
using MemberType1 = decltype(extractObject(Member1));
|
||||
if (MemberType1::supports(keyType))
|
||||
return lambda(&(storage.*Member1));
|
||||
}
|
||||
if constexpr ((... || IsEnabled<Members>))
|
||||
return IpcStorageVariant<Members...>::visit_internal(storage, keyType, lambda);
|
||||
Q_UNREACHABLE();
|
||||
} else {
|
||||
// no backends enabled, but we can't return void
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Lambda> auto visit(QNativeIpcKey::Type keyType, const Lambda &lambda)
|
||||
{
|
||||
return visit_internal(d, keyType, lambda);
|
||||
}
|
||||
|
||||
template <typename Lambda> static auto
|
||||
staticVisit(QNativeIpcKey::Type keyType, const Lambda &lambda)
|
||||
{
|
||||
if constexpr ((IsEnabled<Member1> || ... || IsEnabled<Members>)) {
|
||||
if constexpr (IsEnabled<Member1>) {
|
||||
using MemberType1 = decltype(extractObject(Member1));
|
||||
if (MemberType1::supports(keyType))
|
||||
return lambda(static_cast<MemberType1 *>(nullptr));
|
||||
}
|
||||
if constexpr ((... || IsEnabled<Members>))
|
||||
return IpcStorageVariant<Members...>::staticVisit(keyType, lambda);
|
||||
Q_UNREACHABLE();
|
||||
} else {
|
||||
// no backends enabled, but we can't return void
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Q_AUTOTEST_EXPORT QString
|
||||
legacyPlatformSafeKey(const QString &key, IpcType ipcType,
|
||||
QNativeIpcKey::Type type = QNativeIpcKey::legacyDefaultTypeForOs());
|
||||
|
83
tests/auto/corelib/ipc/ipctestcommon.h
Normal file
83
tests/auto/corelib/ipc/ipctestcommon.h
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2022 Intel Corporation.
|
||||
|
||||
#include <QtTest/QTest>
|
||||
#include <QtCore/QNativeIpcKey>
|
||||
|
||||
namespace IpcTestCommon {
|
||||
static QList<QNativeIpcKey::Type> supportedKeyTypes;
|
||||
|
||||
template <typename IpcClass> void addGlobalTestRows()
|
||||
{
|
||||
qDebug() << "Default key type is" << QNativeIpcKey::DefaultTypeForOs
|
||||
<< "and legacy key type is" << QNativeIpcKey::legacyDefaultTypeForOs();
|
||||
|
||||
#if defined(Q_OS_FREEBSD) || defined(Q_OS_DARWIN) || defined(Q_OS_WIN) || (defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID))
|
||||
// only enforce that IPC works on the platforms above; other platforms may
|
||||
// have no working backends (notably, Android)
|
||||
QVERIFY(IpcClass::isKeyTypeSupported(QNativeIpcKey::DefaultTypeForOs));
|
||||
QVERIFY(IpcClass::isKeyTypeSupported(QNativeIpcKey::legacyDefaultTypeForOs()));
|
||||
#endif
|
||||
|
||||
auto addRowIfSupported = [](const char *name, QNativeIpcKey::Type type) {
|
||||
if (IpcClass::isKeyTypeSupported(type)) {
|
||||
supportedKeyTypes << type;
|
||||
QTest::newRow(name) << type;
|
||||
}
|
||||
};
|
||||
|
||||
QTest::addColumn<QNativeIpcKey::Type>("keyType");
|
||||
|
||||
addRowIfSupported("Windows", QNativeIpcKey::Type::Windows);
|
||||
addRowIfSupported("POSIX", QNativeIpcKey::Type::PosixRealtime);
|
||||
addRowIfSupported("SystemV-Q", QNativeIpcKey::Type::SystemV);
|
||||
addRowIfSupported("SystemV-T", QNativeIpcKey::Type('T'));
|
||||
|
||||
if (supportedKeyTypes.isEmpty())
|
||||
QSKIP("System reports no supported IPC types.");
|
||||
}
|
||||
|
||||
// rotate through the supported types and find another
|
||||
inline QNativeIpcKey::Type nextKeyType(QNativeIpcKey::Type type)
|
||||
{
|
||||
qsizetype idx = supportedKeyTypes.indexOf(type);
|
||||
Q_ASSERT(idx >= 0);
|
||||
|
||||
++idx;
|
||||
if (idx == supportedKeyTypes.size())
|
||||
idx = 0;
|
||||
return supportedKeyTypes.at(idx);
|
||||
}
|
||||
} // namespace IpcTestCommon
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace QTest {
|
||||
template<> inline char *toString(const QNativeIpcKey::Type &type)
|
||||
{
|
||||
switch (type) {
|
||||
case QNativeIpcKey::Type::SystemV: return qstrdup("SystemV");
|
||||
case QNativeIpcKey::Type::PosixRealtime: return qstrdup("PosixRealTime");
|
||||
case QNativeIpcKey::Type::Windows: return qstrdup("Windows");
|
||||
}
|
||||
if (type == QNativeIpcKey::Type{})
|
||||
return qstrdup("Invalid");
|
||||
|
||||
char buf[32];
|
||||
qsnprintf(buf, sizeof(buf), "%u", unsigned(type));
|
||||
return qstrdup(buf);
|
||||
}
|
||||
|
||||
template<> inline char *toString(const QNativeIpcKey &key)
|
||||
{
|
||||
if (!key.isValid())
|
||||
return qstrdup("<invalid>");
|
||||
|
||||
const char *type = toString(key.type());
|
||||
const char *text = toString(key.nativeKey());
|
||||
char buf[256];
|
||||
qsnprintf(buf, sizeof(buf), "QNativeIpcKey(%s, %s)", text, type);
|
||||
delete[] type;
|
||||
delete[] text;
|
||||
return qstrdup(buf);
|
||||
}
|
||||
} // namespace QTest
|
||||
QT_END_NAMESPACE
|
@ -4,39 +4,10 @@
|
||||
#include <QtCore/QNativeIpcKey>
|
||||
#include <QtTest/QTest>
|
||||
|
||||
#include "../ipctestcommon.h"
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
namespace QTest {
|
||||
template<> inline char *toString(const QNativeIpcKey::Type &type)
|
||||
{
|
||||
switch (type) {
|
||||
case QNativeIpcKey::Type::SystemV: return qstrdup("SystemV");
|
||||
case QNativeIpcKey::Type::PosixRealtime: return qstrdup("PosixRealTime");
|
||||
case QNativeIpcKey::Type::Windows: return qstrdup("Windows");
|
||||
}
|
||||
if (type == QNativeIpcKey::Type{})
|
||||
return qstrdup("Invalid");
|
||||
|
||||
char buf[32];
|
||||
qsnprintf(buf, sizeof(buf), "%u", unsigned(type));
|
||||
return qstrdup(buf);
|
||||
}
|
||||
|
||||
template<> inline char *toString(const QNativeIpcKey &key)
|
||||
{
|
||||
if (!key.isValid())
|
||||
return qstrdup("<invalid>");
|
||||
|
||||
const char *type = toString(key.type());
|
||||
const char *text = toString(key.nativeKey());
|
||||
char buf[256];
|
||||
qsnprintf(buf, sizeof(buf), "QNativeIpcKey(%s, %s)", text, type);
|
||||
delete[] type;
|
||||
delete[] text;
|
||||
return qstrdup(buf);
|
||||
}
|
||||
} // namespace QTest
|
||||
|
||||
class tst_QNativeIpcKey : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include <QtCore/QSystemSemaphore>
|
||||
#include <QtCore/QTemporaryDir>
|
||||
|
||||
#include "../ipctestcommon.h"
|
||||
|
||||
#define HELPERWAITTIME 10000
|
||||
|
||||
using namespace Qt::StringLiterals;
|
||||
@ -32,11 +34,12 @@ public:
|
||||
|
||||
QNativeIpcKey platformSafeKey(const QString &key)
|
||||
{
|
||||
QNativeIpcKey::Type keyType = QNativeIpcKey::DefaultTypeForOs;
|
||||
QFETCH_GLOBAL(QNativeIpcKey::Type, keyType);
|
||||
return QSystemSemaphore::platformSafeKey(mangleKey(key), keyType);
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
void initTestCase();
|
||||
void init();
|
||||
void cleanup();
|
||||
|
||||
@ -46,9 +49,11 @@ private slots:
|
||||
void legacyKey_data() { nativeKey_data(); }
|
||||
void legacyKey();
|
||||
|
||||
void changeKeyType();
|
||||
void basicacquire();
|
||||
void complexacquire();
|
||||
void release();
|
||||
void twoSemaphores();
|
||||
|
||||
void basicProcesses();
|
||||
|
||||
@ -70,6 +75,11 @@ tst_QSystemSemaphore::tst_QSystemSemaphore()
|
||||
{
|
||||
}
|
||||
|
||||
void tst_QSystemSemaphore::initTestCase()
|
||||
{
|
||||
IpcTestCommon::addGlobalTestRows<QSystemSemaphore>();
|
||||
}
|
||||
|
||||
void tst_QSystemSemaphore::init()
|
||||
{
|
||||
QNativeIpcKey key = platformSafeKey("existing");
|
||||
@ -111,6 +121,14 @@ void tst_QSystemSemaphore::nativeKey()
|
||||
QCOMPARE(sem.nativeIpcKey(), setIpcKey);
|
||||
QCOMPARE(sem.error(), QSystemSemaphore::NoError);
|
||||
QCOMPARE(sem.errorString(), QString());
|
||||
|
||||
// change the key type
|
||||
QNativeIpcKey::Type nextKeyType = IpcTestCommon::nextKeyType(setIpcKey.type());
|
||||
if (nextKeyType != setIpcKey.type()) {
|
||||
QNativeIpcKey setIpcKey2 = QSystemSemaphore::platformSafeKey(setKey, nextKeyType);
|
||||
sem.setNativeKey(setIpcKey2);
|
||||
QCOMPARE(sem.nativeIpcKey(), setIpcKey2);
|
||||
}
|
||||
}
|
||||
|
||||
QT_WARNING_PUSH
|
||||
@ -132,6 +150,21 @@ void tst_QSystemSemaphore::legacyKey()
|
||||
}
|
||||
QT_WARNING_POP
|
||||
|
||||
void tst_QSystemSemaphore::changeKeyType()
|
||||
{
|
||||
QString keyName = "changeKeyType";
|
||||
QNativeIpcKey key = platformSafeKey(keyName);
|
||||
QNativeIpcKey otherKey =
|
||||
QSystemSemaphore::platformSafeKey(mangleKey(keyName), IpcTestCommon::nextKeyType(key.type()));
|
||||
if (key == otherKey)
|
||||
QSKIP("System only supports one key type");
|
||||
|
||||
QSystemSemaphore sem1(key, 1, QSystemSemaphore::Create);
|
||||
QSystemSemaphore sem2(otherKey);
|
||||
sem1.setNativeKey(otherKey);
|
||||
sem2.setNativeKey(key);
|
||||
}
|
||||
|
||||
void tst_QSystemSemaphore::basicacquire()
|
||||
{
|
||||
QNativeIpcKey key = platformSafeKey("basicacquire");
|
||||
@ -185,6 +218,17 @@ void tst_QSystemSemaphore::release()
|
||||
QCOMPARE(sem.errorString(), QString());
|
||||
}
|
||||
|
||||
void tst_QSystemSemaphore::twoSemaphores()
|
||||
{
|
||||
QNativeIpcKey key = platformSafeKey("twoSemaphores");
|
||||
QSystemSemaphore sem1(key, 1, QSystemSemaphore::Create);
|
||||
QSystemSemaphore sem2(key);
|
||||
QVERIFY(sem1.acquire());
|
||||
QVERIFY(sem2.release());
|
||||
QVERIFY(sem1.acquire());
|
||||
QVERIFY(sem2.release());
|
||||
}
|
||||
|
||||
void tst_QSystemSemaphore::basicProcesses()
|
||||
{
|
||||
#if !QT_CONFIG(process)
|
||||
|
Loading…
x
Reference in New Issue
Block a user