[#962] Read data as soon as it is present in OIO and not wait till it match Buffer.writableBytes()
- Also add a new abstract class called StreamOioByteChannel which can be used by OIO channel implementation which are Stream based as a starting point.
This commit is contained in:
parent
b20e597217
commit
082b5f0dff
@ -18,35 +18,24 @@ package io.netty.channel.rxtx;
|
|||||||
import gnu.io.CommPort;
|
import gnu.io.CommPort;
|
||||||
import gnu.io.CommPortIdentifier;
|
import gnu.io.CommPortIdentifier;
|
||||||
import gnu.io.SerialPort;
|
import gnu.io.SerialPort;
|
||||||
import io.netty.buffer.BufType;
|
import io.netty.channel.socket.oio.StreamOioByteChannel;
|
||||||
import io.netty.buffer.ByteBuf;
|
|
||||||
import io.netty.channel.ChannelMetadata;
|
|
||||||
import io.netty.channel.socket.oio.AbstractOioByteChannel;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.nio.channels.NotYetConnectedException;
|
|
||||||
|
|
||||||
import static io.netty.channel.rxtx.RxtxChannelOption.*;
|
import static io.netty.channel.rxtx.RxtxChannelOption.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A channel to a serial device using the RXTX library.
|
* A channel to a serial device using the RXTX library.
|
||||||
*/
|
*/
|
||||||
public class RxtxChannel extends AbstractOioByteChannel {
|
public class RxtxChannel extends StreamOioByteChannel {
|
||||||
|
|
||||||
private static final RxtxDeviceAddress LOCAL_ADDRESS = new RxtxDeviceAddress("localhost");
|
private static final RxtxDeviceAddress LOCAL_ADDRESS = new RxtxDeviceAddress("localhost");
|
||||||
private static final ChannelMetadata METADATA = new ChannelMetadata(BufType.BYTE, true);
|
|
||||||
|
|
||||||
private final RxtxChannelConfig config;
|
private final RxtxChannelConfig config;
|
||||||
|
|
||||||
private boolean open = true;
|
private boolean open = true;
|
||||||
private RxtxDeviceAddress deviceAddress;
|
private RxtxDeviceAddress deviceAddress;
|
||||||
private SerialPort serialPort;
|
private SerialPort serialPort;
|
||||||
private InputStream in;
|
|
||||||
private OutputStream out;
|
|
||||||
|
|
||||||
public RxtxChannel() {
|
public RxtxChannel() {
|
||||||
super(null, null);
|
super(null, null);
|
||||||
@ -59,47 +48,11 @@ public class RxtxChannel extends AbstractOioByteChannel {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChannelMetadata metadata() {
|
|
||||||
return METADATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return open;
|
return open;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isActive() {
|
|
||||||
return in != null && out != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int available() {
|
|
||||||
try {
|
|
||||||
return in.available();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int doReadBytes(ByteBuf buf) throws Exception {
|
|
||||||
try {
|
|
||||||
return buf.writeBytes(in, buf.writableBytes());
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doWriteBytes(ByteBuf buf) throws Exception {
|
|
||||||
if (out == null) {
|
|
||||||
throw new NotYetConnectedException();
|
|
||||||
}
|
|
||||||
buf.readBytes(out, buf.readableBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
|
protected void doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
|
||||||
RxtxDeviceAddress remote = (RxtxDeviceAddress) remoteAddress;
|
RxtxDeviceAddress remote = (RxtxDeviceAddress) remoteAddress;
|
||||||
@ -118,8 +71,7 @@ public class RxtxChannel extends AbstractOioByteChannel {
|
|||||||
serialPort.setDTR(config().getOption(DTR));
|
serialPort.setDTR(config().getOption(DTR));
|
||||||
serialPort.setRTS(config().getOption(RTS));
|
serialPort.setRTS(config().getOption(RTS));
|
||||||
|
|
||||||
out = serialPort.getOutputStream();
|
activate(serialPort.getInputStream(), serialPort.getOutputStream());
|
||||||
in = serialPort.getInputStream();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -155,36 +107,14 @@ public class RxtxChannel extends AbstractOioByteChannel {
|
|||||||
@Override
|
@Override
|
||||||
protected void doClose() throws Exception {
|
protected void doClose() throws Exception {
|
||||||
open = false;
|
open = false;
|
||||||
|
|
||||||
IOException ex = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (in != null) {
|
super.doClose();
|
||||||
in.close();
|
} finally {
|
||||||
|
if (serialPort != null) {
|
||||||
|
serialPort.removeEventListener();
|
||||||
|
serialPort.close();
|
||||||
|
serialPort = null;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
ex = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (out != null) {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
ex = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serialPort != null) {
|
|
||||||
serialPort.removeEventListener();
|
|
||||||
serialPort.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
in = null;
|
|
||||||
out = null;
|
|
||||||
serialPort = null;
|
|
||||||
|
|
||||||
if (ex != null) {
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.channel.socket.oio;
|
package io.netty.channel.socket.oio;
|
||||||
|
|
||||||
|
import io.netty.buffer.BufType;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelMetadata;
|
||||||
import io.netty.channel.ChannelOption;
|
import io.netty.channel.ChannelOption;
|
||||||
import io.netty.channel.ChannelPipeline;
|
import io.netty.channel.ChannelPipeline;
|
||||||
import io.netty.channel.socket.ChannelInputShutdownEvent;
|
import io.netty.channel.socket.ChannelInputShutdownEvent;
|
||||||
@ -29,6 +31,7 @@ import java.io.IOException;
|
|||||||
public abstract class AbstractOioByteChannel extends AbstractOioChannel {
|
public abstract class AbstractOioByteChannel extends AbstractOioChannel {
|
||||||
|
|
||||||
private volatile boolean inputShutdown;
|
private volatile boolean inputShutdown;
|
||||||
|
private static final ChannelMetadata METADATA = new ChannelMetadata(BufType.BYTE, false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see AbstractOioByteChannel#AbstractOioByteChannel(Channel, Integer)
|
* @see AbstractOioByteChannel#AbstractOioByteChannel(Channel, Integer)
|
||||||
@ -41,6 +44,11 @@ public abstract class AbstractOioByteChannel extends AbstractOioChannel {
|
|||||||
return inputShutdown;
|
return inputShutdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelMetadata metadata() {
|
||||||
|
return METADATA;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doRead() {
|
protected void doRead() {
|
||||||
if (inputShutdown) {
|
if (inputShutdown) {
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.channel.socket.oio;
|
package io.netty.channel.socket.oio;
|
||||||
|
|
||||||
import io.netty.buffer.BufType;
|
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.channel.ChannelException;
|
import io.netty.channel.ChannelException;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelMetadata;
|
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.EventLoop;
|
import io.netty.channel.EventLoop;
|
||||||
import io.netty.channel.FileRegion;
|
|
||||||
import io.netty.channel.socket.DefaultSocketChannelConfig;
|
import io.netty.channel.socket.DefaultSocketChannelConfig;
|
||||||
import io.netty.channel.socket.ServerSocketChannel;
|
import io.netty.channel.socket.ServerSocketChannel;
|
||||||
import io.netty.channel.socket.SocketChannel;
|
import io.netty.channel.socket.SocketChannel;
|
||||||
@ -32,32 +29,22 @@ import io.netty.logging.InternalLogger;
|
|||||||
import io.netty.logging.InternalLoggerFactory;
|
import io.netty.logging.InternalLoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.channels.Channels;
|
|
||||||
import java.nio.channels.NotYetConnectedException;
|
|
||||||
import java.nio.channels.WritableByteChannel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SocketChannel} which is using Old-Blocking-IO
|
* A {@link SocketChannel} which is using Old-Blocking-IO
|
||||||
*/
|
*/
|
||||||
public class OioSocketChannel extends AbstractOioByteChannel
|
public class OioSocketChannel extends StreamOioByteChannel
|
||||||
implements SocketChannel {
|
implements SocketChannel {
|
||||||
|
|
||||||
private static final InternalLogger logger =
|
private static final InternalLogger logger =
|
||||||
InternalLoggerFactory.getInstance(OioSocketChannel.class);
|
InternalLoggerFactory.getInstance(OioSocketChannel.class);
|
||||||
|
|
||||||
private static final ChannelMetadata METADATA = new ChannelMetadata(BufType.BYTE, false);
|
|
||||||
|
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
private final SocketChannelConfig config;
|
private final SocketChannelConfig config;
|
||||||
private InputStream is;
|
|
||||||
private OutputStream os;
|
|
||||||
private WritableByteChannel outChannel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance with an new {@link Socket}
|
* Create a new instance with an new {@link Socket}
|
||||||
@ -91,8 +78,7 @@ public class OioSocketChannel extends AbstractOioByteChannel
|
|||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
if (socket.isConnected()) {
|
if (socket.isConnected()) {
|
||||||
is = socket.getInputStream();
|
activate(socket.getInputStream(), socket.getOutputStream());
|
||||||
os = socket.getOutputStream();
|
|
||||||
}
|
}
|
||||||
socket.setSoTimeout(SO_TIMEOUT);
|
socket.setSoTimeout(SO_TIMEOUT);
|
||||||
success = true;
|
success = true;
|
||||||
@ -114,11 +100,6 @@ public class OioSocketChannel extends AbstractOioByteChannel
|
|||||||
return (ServerSocketChannel) super.parent();
|
return (ServerSocketChannel) super.parent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public ChannelMetadata metadata() {
|
|
||||||
return METADATA;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SocketChannelConfig config() {
|
public SocketChannelConfig config() {
|
||||||
return config;
|
return config;
|
||||||
@ -149,6 +130,18 @@ public class OioSocketChannel extends AbstractOioByteChannel
|
|||||||
return shutdownOutput(newPromise());
|
return shutdownOutput(newPromise());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int doReadBytes(ByteBuf buf) throws Exception {
|
||||||
|
if (socket.isClosed()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return super.doReadBytes(buf);
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ChannelFuture shutdownOutput(final ChannelPromise future) {
|
public ChannelFuture shutdownOutput(final ChannelPromise future) {
|
||||||
EventLoop loop = eventLoop();
|
EventLoop loop = eventLoop();
|
||||||
@ -205,8 +198,7 @@ public class OioSocketChannel extends AbstractOioByteChannel
|
|||||||
boolean success = false;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
socket.connect(remoteAddress, config().getConnectTimeoutMillis());
|
socket.connect(remoteAddress, config().getConnectTimeoutMillis());
|
||||||
is = socket.getInputStream();
|
activate(socket.getInputStream(), socket.getOutputStream());
|
||||||
os = socket.getOutputStream();
|
|
||||||
success = true;
|
success = true;
|
||||||
} finally {
|
} finally {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@ -224,62 +216,4 @@ public class OioSocketChannel extends AbstractOioByteChannel
|
|||||||
protected void doClose() throws Exception {
|
protected void doClose() throws Exception {
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int available() {
|
|
||||||
try {
|
|
||||||
return is.available();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int doReadBytes(ByteBuf buf) throws Exception {
|
|
||||||
if (socket.isClosed()) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return buf.writeBytes(is, buf.writableBytes());
|
|
||||||
} catch (SocketTimeoutException e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doWriteBytes(ByteBuf buf) throws Exception {
|
|
||||||
OutputStream os = this.os;
|
|
||||||
if (os == null) {
|
|
||||||
throw new NotYetConnectedException();
|
|
||||||
}
|
|
||||||
buf.readBytes(os, buf.readableBytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doFlushFileRegion(FileRegion region, ChannelPromise promise) throws Exception {
|
|
||||||
OutputStream os = this.os;
|
|
||||||
if (os == null) {
|
|
||||||
throw new NotYetConnectedException();
|
|
||||||
}
|
|
||||||
if (outChannel == null) {
|
|
||||||
outChannel = Channels.newChannel(os);
|
|
||||||
}
|
|
||||||
long written = 0;
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
long localWritten = region.transferTo(outChannel, written);
|
|
||||||
if (localWritten == -1) {
|
|
||||||
checkEOF(region, written);
|
|
||||||
region.close();
|
|
||||||
promise.setSuccess();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
written += localWritten;
|
|
||||||
if (written >= region.count()) {
|
|
||||||
promise.setSuccess();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 The Netty Project
|
||||||
|
*
|
||||||
|
* The Netty Project licenses this file to you under the Apache License,
|
||||||
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at:
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package io.netty.channel.socket.oio;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.Channel;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import io.netty.channel.FileRegion;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.NotYetConnectedException;
|
||||||
|
import java.nio.channels.WritableByteChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for OIO Channels that are based on streams.
|
||||||
|
*/
|
||||||
|
public abstract class StreamOioByteChannel extends AbstractOioByteChannel {
|
||||||
|
|
||||||
|
private InputStream is;
|
||||||
|
private OutputStream os;
|
||||||
|
private WritableByteChannel outChannel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance
|
||||||
|
*
|
||||||
|
* @param parent the parent {@link Channel} which was used to create this instance. This can be null if the
|
||||||
|
* {@link} has no parent as it was created by your self.
|
||||||
|
* @param id the id which should be used for this instance or {@code null} if a new one should be generated
|
||||||
|
*/
|
||||||
|
protected StreamOioByteChannel(Channel parent, Integer id) {
|
||||||
|
super(parent, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate this instance. After this call {@link #isActive()} will return {@code true}.
|
||||||
|
*/
|
||||||
|
protected final void activate(InputStream is, OutputStream os) {
|
||||||
|
if (this.is != null) {
|
||||||
|
throw new IllegalStateException("input was set already");
|
||||||
|
}
|
||||||
|
if (this.os != null) {
|
||||||
|
throw new IllegalStateException("output was set already");
|
||||||
|
}
|
||||||
|
if (is == null) {
|
||||||
|
throw new NullPointerException("is");
|
||||||
|
}
|
||||||
|
if (os == null) {
|
||||||
|
throw new NullPointerException("os");
|
||||||
|
}
|
||||||
|
this.is = is;
|
||||||
|
this.os = os;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isActive() {
|
||||||
|
return is != null && os != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int available() {
|
||||||
|
try {
|
||||||
|
return is.available();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int doReadBytes(ByteBuf buf) throws Exception {
|
||||||
|
int length = available();
|
||||||
|
if (length < 1) {
|
||||||
|
length = 1;
|
||||||
|
}
|
||||||
|
if (length > buf.writableBytes()) {
|
||||||
|
length = buf.writableBytes();
|
||||||
|
}
|
||||||
|
return buf.writeBytes(is, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doWriteBytes(ByteBuf buf) throws Exception {
|
||||||
|
OutputStream os = this.os;
|
||||||
|
if (os == null) {
|
||||||
|
throw new NotYetConnectedException();
|
||||||
|
}
|
||||||
|
buf.readBytes(os, buf.readableBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFlushFileRegion(FileRegion region, ChannelPromise promise) throws Exception {
|
||||||
|
OutputStream os = this.os;
|
||||||
|
if (os == null) {
|
||||||
|
throw new NotYetConnectedException();
|
||||||
|
}
|
||||||
|
if (outChannel == null) {
|
||||||
|
outChannel = Channels.newChannel(os);
|
||||||
|
}
|
||||||
|
long written = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
long localWritten = region.transferTo(outChannel, written);
|
||||||
|
if (localWritten == -1) {
|
||||||
|
checkEOF(region, written);
|
||||||
|
region.close();
|
||||||
|
promise.setSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
written += localWritten;
|
||||||
|
if (written >= region.count()) {
|
||||||
|
promise.setSuccess();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doClose() throws Exception {
|
||||||
|
IOException ex = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (is != null) {
|
||||||
|
is.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
ex = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (os != null) {
|
||||||
|
os.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
ex = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
is = null;
|
||||||
|
os = null;
|
||||||
|
|
||||||
|
if (ex != null) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user