/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed 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 libcore.java.net;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import tests.net.StuckServer;

/**
 * Test that Socket.close called on another thread interrupts a thread that's blocked doing
 * network I/O.
 */
public class ConcurrentCloseTest extends junit.framework.TestCase {
    public void test_accept() throws Exception {
        ServerSocket ss = new ServerSocket(0);
        new Killer(ss).start();
        try {
            System.err.println("accept...");
            Socket s = ss.accept();
            fail("accept returned " + s + "!");
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        }
    }

    public void test_connect() throws Exception {
        StuckServer ss = new StuckServer(false);
        Socket s = new Socket();
        new Killer(s).start();
        try {
            System.err.println("connect...");
            s.connect(ss.getLocalSocketAddress());
            fail("connect returned: " + s + "!");
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        } finally {
            ss.close();
        }
    }

    public void test_connect_timeout() throws Exception {
        StuckServer ss = new StuckServer(false);
        Socket s = new Socket();
        new Killer(s).start();
        try {
            System.err.println("connect (with timeout)...");
            s.connect(ss.getLocalSocketAddress(), 3600 * 1000);
            fail("connect returned: " + s + "!");
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        } finally {
            ss.close();
        }
    }

    public void test_connect_nonBlocking() throws Exception {
        StuckServer ss = new StuckServer(false);
        SocketChannel s = SocketChannel.open();
        new Killer(s.socket()).start();
        try {
            System.err.println("connect (non-blocking)...");
            s.configureBlocking(false);
            s.connect(ss.getLocalSocketAddress());
            while (!s.finishConnect()) {
                // Spin like a mad thing!
            }
            fail("connect returned: " + s + "!");
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        } catch (AsynchronousCloseException alsoOkay) {
            // See below.
        } catch (ClosedChannelException alsoOkay) {
            // For now, I'm assuming that we're happy as long as we get any reasonable exception.
            // It may be that we're supposed to guarantee only one or the other.
        } finally {
            ss.close();
        }
    }

    public void test_read() throws Exception {
        SilentServer ss = new SilentServer();
        Socket s = new Socket();
        s.connect(ss.getLocalSocketAddress());
        new Killer(s).start();
        try {
            System.err.println("read...");
            int i = s.getInputStream().read();
            fail("read returned: " + i);
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        }
        ss.close();
    }

    public void test_read_multiple() throws Throwable {
        SilentServer ss = new SilentServer();
        final Socket s = new Socket();
        s.connect(ss.getLocalSocketAddress());

        // We want to test that we unblock *all* the threads blocked on a socket, not just one.
        // We know the implementation uses the same mechanism for all blocking calls, so we just
        // test read(2) because it's the easiest to test. (recv(2), for example, is only accessible
        // from Java via a synchronized method.)
        final ArrayList<Thread> threads = new ArrayList<Thread>();
        final List<Throwable> thrownExceptions = new CopyOnWriteArrayList<Throwable>();
        for (int i = 0; i < 10; ++i) {
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        try {
                            System.err.println("read...");
                            int i = s.getInputStream().read();
                            fail("read returned: " + i);
                        } catch (SocketException expected) {
                            assertEquals("Socket closed", expected.getMessage());
                        }
                    } catch (Throwable ex) {
                        thrownExceptions.add(ex);
                    }
                }
            });
            threads.add(t);
        }
        for (Thread t : threads) {
            t.start();
        }
        new Killer(s).start();
        for (Thread t : threads) {
            t.join();
        }
        for (Throwable exception : thrownExceptions) {
            throw exception;
        }

        ss.close();
    }

    public void test_recv() throws Exception {
        DatagramSocket s = new DatagramSocket();
        byte[] buf = new byte[200];
        DatagramPacket p = new DatagramPacket(buf, 200);
        new Killer(s).start();
        try {
            System.err.println("receive...");
            s.receive(p);
            fail("receive returned!");
        } catch (SocketException expected) {
            assertEquals("Socket closed", expected.getMessage());
        }
    }

    public void test_write() throws Exception {
        final SilentServer ss = new SilentServer(128); // Minimal receive buffer size.
        Socket s = new Socket();

        // Set the send buffer size really small, to ensure we block.
        int sendBufferSize = 1024;
        s.setSendBufferSize(sendBufferSize);
        sendBufferSize = s.getSendBufferSize(); // How big is the buffer really, Linux?

        // Linux still seems to accept more than it should.
        // How much seems to differ from device to device. This used to be (sendBufferSize * 2)
        // but that still failed on a bullhead (Nexus 5X).
        sendBufferSize *= 4;

        s.connect(ss.getLocalSocketAddress());
        new Killer(s).start();
        try {
            System.err.println("write...");
            // Write too much so the buffer is full and we block,
            // waiting for the server to read (which it never will).
            // If the asynchronous close fails, we'll see a test timeout here.
            byte[] buf = new byte[sendBufferSize];
            s.getOutputStream().write(buf);
            fail();
        } catch (SocketException expected) {
            // We throw "Connection reset by peer", which I don't _think_ is a problem.
            // assertEquals("Socket closed", expected.getMessage());
        }
        ss.close();
    }

    // This server accepts connections, but doesn't read or write anything.
    // It holds on to the Socket connecting to the client so it won't be GCed.
    // Call "close" to close both the server socket and its client connection.
    static class SilentServer {
        private final ServerSocket ss;
        private Socket client;

        public SilentServer() throws IOException {
            this(0);
        }

        public SilentServer(int receiveBufferSize) throws IOException {
            ss = new ServerSocket(0);
            if (receiveBufferSize != 0) {
                ss.setReceiveBufferSize(receiveBufferSize);
            }
            new Thread(new Runnable() {
                public void run() {
                    try {
                        client = ss.accept();
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }).start();
        }

        public SocketAddress getLocalSocketAddress() {
            return ss.getLocalSocketAddress();
        }

        public void close() throws IOException {
            client.close();
            ss.close();
        }
    }

    // This thread calls the "close" method on the supplied T after 2s.
    static class Killer<T> extends Thread {
        private final T s;

        public Killer(T s) {
            this.s = s;
        }

        public void run() {
            try {
                System.err.println("sleep...");
                Thread.sleep(2000);
                System.err.println("close...");
                s.getClass().getMethod("close").invoke(s);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}
