зеркало из https://github.com/mozilla/gecko-dev.git
Bug 782766 - [WebActivities] support blobs - Part 2: IPC changes. r=khuey, a=lsblakk
This commit is contained in:
Родитель
7770d2008b
Коммит
cbbaa0e14b
432
dom/ipc/Blob.cpp
432
dom/ipc/Blob.cpp
|
@ -17,6 +17,7 @@
|
|||
#include "mozilla/Assertions.h"
|
||||
#include "mozilla/Monitor.h"
|
||||
#include "mozilla/unused.h"
|
||||
#include "mozilla/Util.h"
|
||||
#include "mozilla/ipc/InputStreamUtils.h"
|
||||
#include "nsDOMFile.h"
|
||||
#include "nsThreadUtils.h"
|
||||
|
@ -24,15 +25,32 @@
|
|||
#include "ContentChild.h"
|
||||
#include "ContentParent.h"
|
||||
|
||||
#define PRIVATE_REMOTE_INPUT_STREAM_IID \
|
||||
{0x30c7699f, 0x51d2, 0x48c8, {0xad, 0x56, 0xc0, 0x16, 0xd7, 0x6f, 0x71, 0x27}}
|
||||
|
||||
using namespace mozilla::dom;
|
||||
using namespace mozilla::dom::ipc;
|
||||
using namespace mozilla::ipc;
|
||||
|
||||
namespace {
|
||||
|
||||
class NS_NO_VTABLE IPrivateRemoteInputStream : public nsISupports
|
||||
{
|
||||
public:
|
||||
NS_DECLARE_STATIC_IID_ACCESSOR(PRIVATE_REMOTE_INPUT_STREAM_IID)
|
||||
|
||||
// This will return the underlying stream.
|
||||
virtual nsIInputStream*
|
||||
BlockAndGetInternalStream() = 0;
|
||||
};
|
||||
|
||||
NS_DEFINE_STATIC_IID_ACCESSOR(IPrivateRemoteInputStream,
|
||||
PRIVATE_REMOTE_INPUT_STREAM_IID)
|
||||
|
||||
class RemoteInputStream : public nsIInputStream,
|
||||
public nsISeekableStream,
|
||||
public nsIIPCSerializableInputStream
|
||||
public nsIIPCSerializableInputStream,
|
||||
public IPrivateRemoteInputStream
|
||||
{
|
||||
mozilla::Monitor mMonitor;
|
||||
nsCOMPtr<nsIDOMBlob> mSourceBlob;
|
||||
|
@ -183,6 +201,14 @@ public:
|
|||
NS_IMETHOD
|
||||
Tell(int64_t* aResult)
|
||||
{
|
||||
// We can cheat here and assume that we're going to start at 0 if we don't
|
||||
// yet have our stream. Though, really, this should abort since most input
|
||||
// streams could block here.
|
||||
if (NS_IsMainThread() && !mStream) {
|
||||
*aResult = 0;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult rv = BlockAndWaitForStream();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
|
@ -214,6 +240,17 @@ public:
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
virtual nsIInputStream*
|
||||
BlockAndGetInternalStream()
|
||||
{
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
|
||||
nsresult rv = BlockAndWaitForStream();
|
||||
NS_ENSURE_SUCCESS(rv, nullptr);
|
||||
|
||||
return mStream;
|
||||
}
|
||||
|
||||
private:
|
||||
virtual ~RemoteInputStream()
|
||||
{ }
|
||||
|
@ -221,10 +258,28 @@ private:
|
|||
void
|
||||
ReallyBlockAndWaitForStream()
|
||||
{
|
||||
mozilla::MonitorAutoLock lock(mMonitor);
|
||||
while (!mStream) {
|
||||
mMonitor.Wait();
|
||||
mozilla::DebugOnly<bool> waited;
|
||||
|
||||
{
|
||||
mozilla::MonitorAutoLock lock(mMonitor);
|
||||
|
||||
waited = !mStream;
|
||||
|
||||
while (!mStream) {
|
||||
mMonitor.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
MOZ_ASSERT(mStream);
|
||||
|
||||
#ifdef DEBUG
|
||||
if (waited && mSeekableStream) {
|
||||
int64_t position;
|
||||
MOZ_ASSERT(NS_SUCCEEDED(mSeekableStream->Tell(&position)),
|
||||
"Failed to determine initial stream position!");
|
||||
MOZ_ASSERT(!position, "Stream not starting at 0!");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
@ -236,6 +291,7 @@ private:
|
|||
}
|
||||
|
||||
ReallyBlockAndWaitForStream();
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -264,6 +320,7 @@ NS_INTERFACE_MAP_BEGIN(RemoteInputStream)
|
|||
NS_INTERFACE_MAP_ENTRY(nsIIPCSerializableInputStream)
|
||||
NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsISeekableStream, IsSeekableStream())
|
||||
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
|
||||
NS_INTERFACE_MAP_ENTRY(IPrivateRemoteInputStream)
|
||||
NS_INTERFACE_MAP_END
|
||||
|
||||
template <ActorFlavorEnum ActorFlavor>
|
||||
|
@ -361,6 +418,216 @@ namespace mozilla {
|
|||
namespace dom {
|
||||
namespace ipc {
|
||||
|
||||
// Each instance of this class will be dispatched to the network stream thread
|
||||
// pool to run the first time where it will open the file input stream. It will
|
||||
// then dispatch itself back to the main thread to send the child process its
|
||||
// response (assuming that the child has not crashed). The runnable will then
|
||||
// dispatch itself to the thread pool again in order to close the file input
|
||||
// stream.
|
||||
class BlobTraits<Parent>::BaseType::OpenStreamRunnable : public nsRunnable
|
||||
{
|
||||
friend class nsRevocableEventPtr<OpenStreamRunnable>;
|
||||
|
||||
typedef BlobTraits<Parent> TraitsType;
|
||||
typedef TraitsType::BaseType BlobActorType;
|
||||
typedef TraitsType::StreamType BlobStreamProtocolType;
|
||||
|
||||
// Only safe to access these pointers if mRevoked is false!
|
||||
BlobActorType* mBlobActor;
|
||||
BlobStreamProtocolType* mStreamActor;
|
||||
|
||||
nsCOMPtr<nsIInputStream> mStream;
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> mSerializable;
|
||||
nsCOMPtr<nsIEventTarget> mTarget;
|
||||
|
||||
bool mRevoked;
|
||||
bool mClosing;
|
||||
|
||||
public:
|
||||
OpenStreamRunnable(BlobActorType* aBlobActor,
|
||||
BlobStreamProtocolType* aStreamActor,
|
||||
nsIInputStream* aStream,
|
||||
nsIIPCSerializableInputStream* aSerializable,
|
||||
nsIEventTarget* aTarget)
|
||||
: mBlobActor(aBlobActor), mStreamActor(aStreamActor), mStream(aStream),
|
||||
mSerializable(aSerializable), mTarget(aTarget), mRevoked(false),
|
||||
mClosing(false)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aBlobActor);
|
||||
MOZ_ASSERT(aStreamActor);
|
||||
MOZ_ASSERT(aStream);
|
||||
// aSerializable may be null.
|
||||
MOZ_ASSERT(aTarget);
|
||||
}
|
||||
|
||||
NS_IMETHOD
|
||||
Run()
|
||||
{
|
||||
if (NS_IsMainThread()) {
|
||||
return SendResponse();
|
||||
}
|
||||
|
||||
if (!mClosing) {
|
||||
return OpenStream();
|
||||
}
|
||||
|
||||
return CloseStream();
|
||||
}
|
||||
|
||||
nsresult
|
||||
Dispatch()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(mTarget);
|
||||
|
||||
nsresult rv = mTarget->Dispatch(this, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
Revoke()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
#ifdef DEBUG
|
||||
mBlobActor = nullptr;
|
||||
mStreamActor = nullptr;
|
||||
#endif
|
||||
mRevoked = true;
|
||||
}
|
||||
|
||||
nsresult
|
||||
OpenStream()
|
||||
{
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
MOZ_ASSERT(mStream);
|
||||
|
||||
if (!mSerializable) {
|
||||
nsCOMPtr<IPrivateRemoteInputStream> remoteStream =
|
||||
do_QueryInterface(mStream);
|
||||
MOZ_ASSERT(remoteStream, "Must QI to IPrivateRemoteInputStream here!");
|
||||
|
||||
nsCOMPtr<nsIInputStream> realStream =
|
||||
remoteStream->BlockAndGetInternalStream();
|
||||
NS_ENSURE_TRUE(realStream, NS_ERROR_FAILURE);
|
||||
|
||||
mSerializable = do_QueryInterface(realStream);
|
||||
if (!mSerializable) {
|
||||
MOZ_ASSERT(false, "Must be serializable!");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
mStream.swap(realStream);
|
||||
}
|
||||
|
||||
// To force the stream open we call Available(). We don't actually care
|
||||
// how much data is available.
|
||||
uint64_t available;
|
||||
if (NS_FAILED(mStream->Available(&available))) {
|
||||
NS_WARNING("Available failed on this stream!");
|
||||
}
|
||||
|
||||
nsresult rv = NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
CloseStream()
|
||||
{
|
||||
MOZ_ASSERT(!NS_IsMainThread());
|
||||
MOZ_ASSERT(mStream);
|
||||
|
||||
// Going to always release here.
|
||||
nsCOMPtr<nsIInputStream> stream;
|
||||
mStream.swap(stream);
|
||||
|
||||
nsresult rv = stream->Close();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
SendResponse()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
MOZ_ASSERT(mStream);
|
||||
MOZ_ASSERT(mSerializable);
|
||||
MOZ_ASSERT(mTarget);
|
||||
MOZ_ASSERT(!mClosing);
|
||||
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> serializable;
|
||||
mSerializable.swap(serializable);
|
||||
|
||||
if (mRevoked) {
|
||||
MOZ_ASSERT(!mBlobActor);
|
||||
MOZ_ASSERT(!mStreamActor);
|
||||
}
|
||||
else {
|
||||
MOZ_ASSERT(mBlobActor);
|
||||
MOZ_ASSERT(mStreamActor);
|
||||
|
||||
InputStreamParams params;
|
||||
serializable->Serialize(params);
|
||||
|
||||
MOZ_ASSERT(params.type() != InputStreamParams::T__None);
|
||||
|
||||
unused << mStreamActor->Send__delete__(mStreamActor, params);
|
||||
|
||||
mBlobActor->NoteRunnableCompleted(this);
|
||||
|
||||
#ifdef DEBUG
|
||||
mBlobActor = nullptr;
|
||||
mStreamActor = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
mClosing = true;
|
||||
|
||||
nsCOMPtr<nsIEventTarget> target;
|
||||
mTarget.swap(target);
|
||||
|
||||
nsresult rv = target->Dispatch(this, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
};
|
||||
|
||||
BlobTraits<Parent>::BaseType::BaseType()
|
||||
{
|
||||
}
|
||||
|
||||
BlobTraits<Parent>::BaseType::~BaseType()
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
BlobTraits<Parent>::BaseType::NoteRunnableCompleted(
|
||||
BlobTraits<Parent>::BaseType::OpenStreamRunnable* aRunnable)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
for (uint32_t index = 0; index < mOpenStreamRunnables.Length(); index++) {
|
||||
nsRevocableEventPtr<BaseType::OpenStreamRunnable>& runnable =
|
||||
mOpenStreamRunnables[index];
|
||||
|
||||
if (runnable.get() == aRunnable) {
|
||||
runnable.Forget();
|
||||
mOpenStreamRunnables.RemoveElementAt(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MOZ_NOT_REACHED("Runnable not in our array!");
|
||||
}
|
||||
|
||||
template <ActorFlavorEnum ActorFlavor>
|
||||
class RemoteBlob : public nsDOMFile,
|
||||
public nsIRemoteBlob
|
||||
|
@ -950,11 +1217,30 @@ Blob<Parent>::RecvPBlobStreamConstructor(StreamType* aActor)
|
|||
nsresult rv = mBlob->GetInternalStream(getter_AddRefs(stream));
|
||||
NS_ENSURE_SUCCESS(rv, false);
|
||||
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> serializable =
|
||||
do_QueryInterface(stream);
|
||||
if (!serializable) {
|
||||
MOZ_ASSERT(false, "Must be serializable!");
|
||||
return false;
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> serializableStream;
|
||||
|
||||
nsCOMPtr<nsIRemoteBlob> remoteBlob = do_QueryInterface(mBlob);
|
||||
if (remoteBlob) {
|
||||
// Sanity check that the remote blob returned a remote stream.
|
||||
nsCOMPtr<IPrivateRemoteInputStream> remoteStream =
|
||||
do_QueryInterface(stream);
|
||||
if (!remoteStream) {
|
||||
MOZ_ASSERT(false, "Remote blob didn't return a remote stream!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the underlying blob is not a remote blob or it is a remote blob
|
||||
// representing this actor then we can use the internal stream that it
|
||||
// provides. Otherwise we need to be on a background thread before we can
|
||||
// get to the real stream.
|
||||
if (!remoteBlob ||
|
||||
static_cast<ProtocolType*>(remoteBlob->GetPBlob()) == this) {
|
||||
serializableStream = do_QueryInterface(stream);
|
||||
if (!serializableStream) {
|
||||
MOZ_ASSERT(false, "Must be serializable!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIEventTarget> target =
|
||||
|
@ -962,10 +1248,10 @@ Blob<Parent>::RecvPBlobStreamConstructor(StreamType* aActor)
|
|||
NS_ENSURE_TRUE(target, false);
|
||||
|
||||
nsRefPtr<BaseType::OpenStreamRunnable> runnable =
|
||||
new BaseType::OpenStreamRunnable(this, aActor, stream, serializable,
|
||||
target);
|
||||
new BaseType::OpenStreamRunnable(this, aActor, stream, serializableStream,
|
||||
target);
|
||||
|
||||
rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL);
|
||||
rv = runnable->Dispatch();
|
||||
NS_ENSURE_SUCCESS(rv, false);
|
||||
|
||||
nsRevocableEventPtr<BaseType::OpenStreamRunnable>* arrayMember =
|
||||
|
@ -1028,128 +1314,6 @@ template <ActorFlavorEnum ActorFlavor>
|
|||
NS_IMPL_QUERY_INTERFACE_INHERITED1(RemoteBlob<ActorFlavor>, nsDOMFile,
|
||||
nsIRemoteBlob)
|
||||
|
||||
void
|
||||
BlobTraits<Parent>::BaseType::NoteRunnableCompleted(
|
||||
BlobTraits<Parent>::BaseType::OpenStreamRunnable* aRunnable)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
for (uint32_t index = 0; index < mOpenStreamRunnables.Length(); index++) {
|
||||
nsRevocableEventPtr<BaseType::OpenStreamRunnable>& runnable =
|
||||
mOpenStreamRunnables[index];
|
||||
|
||||
if (runnable.get() == aRunnable) {
|
||||
runnable.Forget();
|
||||
mOpenStreamRunnables.RemoveElementAt(index);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
MOZ_NOT_REACHED("Runnable not in our array!");
|
||||
}
|
||||
|
||||
BlobTraits<Parent>::BaseType::
|
||||
OpenStreamRunnable::OpenStreamRunnable(
|
||||
BlobTraits<Parent>::BaseType* aOwner,
|
||||
BlobTraits<Parent>::StreamType* aActor,
|
||||
nsIInputStream* aStream,
|
||||
nsIIPCSerializableInputStream* aSerializable,
|
||||
nsIEventTarget* aTarget)
|
||||
: mOwner(aOwner), mActor(aActor), mStream(aStream),
|
||||
mSerializable(aSerializable), mTarget(aTarget), mRevoked(false),
|
||||
mClosing(false)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(aOwner);
|
||||
MOZ_ASSERT(aActor);
|
||||
MOZ_ASSERT(aStream);
|
||||
MOZ_ASSERT(aSerializable);
|
||||
MOZ_ASSERT(aTarget);
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
BlobTraits<Parent>::BaseType::OpenStreamRunnable::Run()
|
||||
{
|
||||
MOZ_ASSERT(mStream);
|
||||
|
||||
nsresult rv;
|
||||
|
||||
if (NS_IsMainThread()) {
|
||||
MOZ_ASSERT(mTarget);
|
||||
MOZ_ASSERT(!mClosing);
|
||||
|
||||
if (mRevoked) {
|
||||
MOZ_ASSERT(!mOwner);
|
||||
MOZ_ASSERT(!mActor);
|
||||
}
|
||||
else {
|
||||
MOZ_ASSERT(mOwner);
|
||||
MOZ_ASSERT(mActor);
|
||||
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> serializable;
|
||||
mSerializable.swap(serializable);
|
||||
|
||||
InputStreamParams params;
|
||||
serializable->Serialize(params);
|
||||
|
||||
MOZ_ASSERT(params.type() != InputStreamParams::T__None);
|
||||
|
||||
unused << mActor->Send__delete__(mActor, params);
|
||||
|
||||
mOwner->NoteRunnableCompleted(this);
|
||||
|
||||
#ifdef DEBUG
|
||||
mOwner = nullptr;
|
||||
mActor = nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
mClosing = true;
|
||||
|
||||
nsCOMPtr<nsIEventTarget> target;
|
||||
mTarget.swap(target);
|
||||
|
||||
rv = target->Dispatch(this, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (!mClosing) {
|
||||
// To force the stream open we call Available(). We don't actually care how
|
||||
// much data is available.
|
||||
uint64_t available;
|
||||
if (NS_FAILED(mStream->Available(&available))) {
|
||||
NS_WARNING("Available failed on this stream!");
|
||||
}
|
||||
|
||||
rv = NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Going to always release here.
|
||||
nsCOMPtr<nsIInputStream> stream;
|
||||
mStream.swap(stream);
|
||||
|
||||
rv = stream->Close();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
void
|
||||
BlobTraits<Parent>::BaseType::OpenStreamRunnable::Revoke()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
mOwner = nullptr;
|
||||
mActor = nullptr;
|
||||
mRevoked = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Explicit instantiation of both classes.
|
||||
template class Blob<Parent>;
|
||||
template class Blob<Child>;
|
||||
|
|
|
@ -55,53 +55,10 @@ struct BlobTraits<Parent>
|
|||
class BaseType : public ProtocolType
|
||||
{
|
||||
protected:
|
||||
BaseType()
|
||||
{ }
|
||||
|
||||
virtual ~BaseType()
|
||||
{ }
|
||||
|
||||
// Each instance of this class will be dispatched to the network stream
|
||||
// thread pool to run the first time where it will open the file input
|
||||
// stream. It will then dispatch itself back to the main thread to send the
|
||||
// child process its response (assuming that the child has not crashed). The
|
||||
// runnable will then dispatch itself to the thread pool again in order to
|
||||
// close the file input stream.
|
||||
class OpenStreamRunnable : public nsRunnable
|
||||
{
|
||||
friend class nsRevocableEventPtr<OpenStreamRunnable>;
|
||||
public:
|
||||
NS_DECL_NSIRUNNABLE
|
||||
|
||||
OpenStreamRunnable(BaseType* aOwner, StreamType* aActor,
|
||||
nsIInputStream* aStream,
|
||||
nsIIPCSerializableInputStream* aSerializable,
|
||||
nsIEventTarget* aTarget);
|
||||
|
||||
private:
|
||||
#ifdef DEBUG
|
||||
void
|
||||
Revoke();
|
||||
#else
|
||||
void
|
||||
Revoke()
|
||||
{
|
||||
mRevoked = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Only safe to access these two pointers if mRevoked is false!
|
||||
BaseType* mOwner;
|
||||
StreamType* mActor;
|
||||
|
||||
nsCOMPtr<nsIInputStream> mStream;
|
||||
nsCOMPtr<nsIIPCSerializableInputStream> mSerializable;
|
||||
nsCOMPtr<nsIEventTarget> mTarget;
|
||||
|
||||
bool mRevoked;
|
||||
bool mClosing;
|
||||
};
|
||||
BaseType();
|
||||
virtual ~BaseType();
|
||||
|
||||
class OpenStreamRunnable;
|
||||
friend class OpenStreamRunnable;
|
||||
|
||||
void
|
||||
|
|
|
@ -1228,7 +1228,7 @@ ContentParent::GetOrCreateActorForBlob(nsIDOMBlob* aBlob)
|
|||
static_cast<PBlobParent*>(remoteBlob->GetPBlob()));
|
||||
NS_ASSERTION(actor, "Null actor?!");
|
||||
|
||||
if (actor->Manager() == this) {
|
||||
if (static_cast<ContentParent*>(actor->Manager()) == this) {
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче