/*
 * Copyright (c) 2006, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import com.sun.jdi.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;

/**
 * @test
 * @bug 6459476
 * @summary Test interrupting debuggee with single stepping enable
 * @author jjh
 *
 * @run build TestScaffold VMConnection TargetListener TargetAdapter
 * @run compile -g InterruptHangTest.java
 * @run driver InterruptHangTest precise
 * @run driver InterruptHangTest aggressive
 * @run driver InterruptHangTest remote
 */

/**
 * The debuggee loops in the main thread while the debugger has single stepping
 * enabled for the debuggee's main thread. The main thread is repeatedly
 * interrupted while it is looping. If a long time goes by with the debugger
 * not getting a single step event, the test fails.
 *
 * Interrupts are generated in 3 difference modes:
 *   precise - The debuggee creates a 2nd thread that repeatedly calls
 *       Thread.interrupt() on the main thread, but does so in a controlled
 *       fashion that allows to test to verify that every interrupt is
 *       received and handled.
 *   aggressive - The debuggee creates a 2nd thread that repeatedly calls
 *       Thread.interrupt() on the main thread, but does so at a high pace
 *       and without any coordination with the main thread. Because of
 *       this it is not possible to account for all the interrupts since some
 *       might be done before the previous interrupt is handled.
 *   remote - Much like the "aggressive" mode, except interrupts are remotely
 *       generated by the debugger using ThreadReference.interrupt(). There
 *       is no "precise" mode for remotely generated interrupts since it is
 *       difficult to coordinate between the debugger and debuggee in a
 *       reasonable way.
 */

class InterruptHangTarg {
    static String sync = "sync";
    static int interruptsSent;
    static boolean remoteMode;
    static boolean preciseMode;
    static boolean aggressiveMode;

    private final static int INTERRUPTS_EXPECTED = 200;

    public static void main(String[] args){
        int answer = 0; // number of interrupts answered

        System.out.println("Howdy!");

        remoteMode = "remote".equals(args[0]);
        preciseMode = "precise".equals(args[0]);
        aggressiveMode = "aggressive".equals(args[0]);

        if (!remoteMode && !preciseMode && !aggressiveMode) {
            throw new RuntimeException("Invalid first arg for debuggee: " + args[0]);
        }

        // Create the debuggee interruptor thread if needed.
        Thread interruptorThread;
        if (preciseMode) {
            interruptorThread =
                DebuggeeWrapper.newThread(new PreciseInterruptor(Thread.currentThread()));
            interruptorThread.start();
        } else if (aggressiveMode) {
            interruptorThread =
                DebuggeeWrapper.newThread(new AggressiveInterruptor(Thread.currentThread()));
            interruptorThread.start();
        } else {
            interruptorThread = null; // we are in "remote" interrupt mode
        }

        // Debugger will keep stepping thru this loop
        for (int ii = 0; ii < INTERRUPTS_EXPECTED; ii++) {
            boolean wasInterrupted = false;
            try {
                // Give other thread a chance to interrupt. Normally only a very short
                // sleep is needed, but we need to account for unexpected delays in
                // the interrupt due to machine and network hiccups.
                Thread.sleep(10*1000);
            } catch (InterruptedException ee) {
                answer++;
                wasInterrupted = true;
                boolean isInterrupted = Thread.currentThread().isInterrupted();
                System.out.println("Debuggee interruptee(" + ii + "): isInterrupted(" + isInterrupted + ")");
                if (preciseMode) {
                    // When Thread.sleep() is interrupted, the interrupted status of the thread
                    // should remain cleared (since InterruptedException was thrown), and the
                    // the next interrupt won't come in until after the sync.notify() below.
                    // Note this is not true for the aggressive and remote modes since
                    // interrupts can come in at any time, even while we are handling
                    // an intrrupt.
                    if (isInterrupted) {
                        throw new RuntimeException("Thread should not have interrupted status set.");
                    }
                    synchronized(InterruptHangTarg.sync) {
                        // Let the interruptor thread know it can start interrupting again
                        sync.notify();
                    }
                }
            }
            // Every Thread.sleep() call should get interrupted
            if (!wasInterrupted) {
                throw new RuntimeException("Thread was never interrupted during sleep: " + ii);
            }
        }

        if (interruptorThread != null) {
            synchronized(InterruptHangTarg.sync) {
                // Kill the interrupter thread
                interruptorThread.interrupt();
            }
        }

        // Print how many times an interrupt was sent. When in remote mode, the RemoteInterruptor
        // is in a different process, so interruptsSent is not updated, therefore we don't
        // print it here. The RemoteInterruptor thread keeps its own count and prints
        // it before it exits.
        if (!remoteMode) {
            System.out.println("interrupts sent: " + interruptsSent);
        }

        // When in precise mode, make sure that every interrupt sent resulted in
        // an InterruptedException. Note the interruptor always ends up sending
        // one extra interrupt at the end.
        if (preciseMode && interruptsSent != answer + 1) {
            throw new RuntimeException("Too many interrupts sent. Sent: " + interruptsSent +
                                       ". Expected to send: " + answer + 1);
        }
        System.out.println("Goodbye from InterruptHangTarg!");
    }

    static class AggressiveInterruptor implements Runnable {
        Thread interruptee;
        AggressiveInterruptor(Thread interruptee) {
            this.interruptee = interruptee;
        }

        public void run() {
            while (true) {
                InterruptHangTarg.interruptsSent++;
                interruptee.interrupt();
                try {
                    Thread.sleep(5);
                } catch (InterruptedException ee) {
                    System.out.println("Debuggee Interruptor: finished after " +
                                       InterruptHangTarg.interruptsSent + " iterrupts");
                    break;
                }
            }
        }
    }

    static class PreciseInterruptor implements Runnable {
        Thread interruptee;
        PreciseInterruptor(Thread interruptee) {
            this.interruptee = interruptee;
        }

        public void run() {
            synchronized(InterruptHangTarg.sync) {
                while (true) {
                    InterruptHangTarg.interruptsSent++;
                    interruptee.interrupt();
                    try {
                        // Wait until the interruptee has handled the interrupt
                        InterruptHangTarg.sync.wait();
                    } catch (InterruptedException ee) {
                        System.out.println("Debuggee Interruptor: finished after " +
                                           InterruptHangTarg.interruptsSent + " iterrupts");
                        break;
                    }
                }
            }
        }
    }
}

    /********** test program **********/

public class InterruptHangTest extends TestScaffold {
    class RemoteInterruptor extends Thread {
        static int interruptsSent;
        ThreadReference interruptee;

        RemoteInterruptor(ThreadReference interruptee) {
            this.interruptee = interruptee;
        }

        public void run() {
            try {
                while (true) {
                    interruptee.interrupt();
                    interruptsSent++;
                    Thread.sleep(5);
                }
            } catch (InterruptedException ee) {
                println("RemoteInterruptor thread: Unexpected Interrupt");
                throw new RuntimeException(ee);
            } catch (IllegalThreadStateException | VMDisconnectedException ee) {
                println("RemoteInterruptor thread: Got expected " + ee.getClass().getSimpleName()
                        + " after " + interruptsSent + " interrupts sent. Exiting.");
            } catch (Throwable ee) {
                println("RemoteInterruptor thread: Got unexpected exception after "
                        + interruptsSent + " interrupts sent. Exiting with exception.");
                ee.printStackTrace(System.out);
                throw new RuntimeException(ee);
            }
        }
    }

    ThreadReference mainThread;
    Thread timerThread;
    String sync = "sync";
    static int nSteps = 0;
    static boolean remoteMode;

    InterruptHangTest (String args[]) {
        super(args);
    }

    public static void main(String[] args)      throws Exception {
        remoteMode = "remote".equals(args[0]);
        new InterruptHangTest(args).startTests();
    }

    /********** event handlers **********/

    public void stepCompleted(StepEvent event) {
        synchronized(sync) {
            nSteps++;
        }
        println("Got StepEvent " + nSteps + " at line " +
                event.location().method() + ":" +
                event.location().lineNumber());
        if (nSteps == 1) {
            timerThread.start();
        }
    }

    /********** test core **********/

    protected void runTests() throws Exception {
        BreakpointEvent bpe = startToMain("InterruptHangTarg");
        mainThread = bpe.thread();
        EventRequestManager erm = vm().eventRequestManager();

        /*
         * Set event requests
         */
        StepRequest request = erm.createStepRequest(mainThread,
                                                    StepRequest.STEP_LINE,
                                                    StepRequest.STEP_OVER);
        request.enable();

        // Will be started by the step event handler
        timerThread = new Thread("test timer") {
                public void run() {
                    int mySteps = 0;
                    float timeoutFactor = Float.parseFloat(System.getProperty("test.timeout.factor", "1.0"));
                    long sleepSeconds = (long)(20 * timeoutFactor);
                    println("Timer watching for steps every " + sleepSeconds + " seconds");
                    while (true) {
                        try {
                            Thread.sleep(sleepSeconds * 1000);
                            synchronized(sync) {
                                println("steps = " + nSteps);
                                if (mySteps == nSteps) {
                                    // no step for a long time
                                    failure("failure: Debuggee appears to be hung (no steps for " + sleepSeconds + "s)");
                                    vm().exit(-1);
                                    break;
                                }
                            }
                            mySteps = nSteps;
                        } catch (InterruptedException ee) {
                            break;
                        }
                    }
                }
            };

        Thread remoteInterruptorThread = null;
        if (remoteMode) {
            // Create a thread to call ThreadReference.interrupt() on the
            // debuggee main thread.
            remoteInterruptorThread = new RemoteInterruptor(mainThread);
            remoteInterruptorThread.setName("RemoteInterruptor");
            remoteInterruptorThread.setDaemon(true);
            remoteInterruptorThread.start();
        }

        /*
         * resume the target listening for events
         */
        listenUntilVMDisconnect();
        if (remoteInterruptorThread != null) {
            remoteInterruptorThread.join();
        }
        timerThread.interrupt();

        /*
         * deal with results of test
         * if anything has called failure("foo") testFailed will be true
         */
        if (!testFailed) {
            println("InterruptHangTest("+ args[0] + "): passed");
        } else {
            throw new Exception("InterruptHangTest("+ args[0] + "): failed");
        }
    }
}
