// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.jetbrains.env.debug; import com.intellij.execution.ExecutionException; import com.intellij.execution.Executor; import com.intellij.execution.RunManager; import com.intellij.execution.RunnerAndConfigurationSettings; import com.intellij.execution.configurations.RunProfile; import com.intellij.execution.executors.DefaultDebugExecutor; import com.intellij.execution.process.*; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.execution.runners.ProgramRunner; import com.intellij.execution.target.TargetEnvironment; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.registry.Registry; import com.intellij.xdebugger.*; import com.jetbrains.python.debugger.PyDebugProcess; import com.jetbrains.python.debugger.PyDebugRunner; import com.jetbrains.python.debugger.PyDebugValueExecutionService; import com.jetbrains.python.debugger.PyDebuggerOptionsProvider; import com.jetbrains.python.run.AbstractPythonRunConfiguration; import com.jetbrains.python.run.CommandLinePatcher; import com.jetbrains.python.run.PythonCommandLineState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import java.io.IOException; import java.net.ServerSocket; import java.util.concurrent.Semaphore; /** * A base class for Python debugger test tasks. The main purpose is to make it possible to execute different run configurations * with debug executor. The type of the run configuration is controlled by redefining the {@link #createRunConfiguration} method. * * @see PyDebuggerTask * @see PyUnitTestDebuggingTask */ public abstract class PyCustomConfigDebuggerTask extends PyBaseDebuggerTask { protected AbstractPythonRunConfiguration> myRunConfiguration; protected RunnerAndConfigurationSettings mySettings; private boolean myMultiprocessDebug = false; private boolean myWaitForTermination = true; private final @NotNull StringBuilder myOutputBuilder = new StringBuilder(); private final @NotNull StringBuilder myStdErrBuilder = new StringBuilder(); protected PyCustomConfigDebuggerTask(@Nullable String relativeTestDataPath) { super(relativeTestDataPath); setProcessCanTerminate(false); } protected abstract AbstractPythonRunConfiguration> createRunConfiguration( @NotNull String sdkHome, @Nullable Sdk existingSdk); protected abstract CommandLinePatcher[] createCommandLinePatchers(PyDebugRunner runner, PythonCommandLineState pyState, RunProfile profile, int serverLocalPort); @Override public void runTestOn(@NotNull String sdkHome, @Nullable Sdk existingSdk) throws Exception { if (Registry.is("python.debug.use.single.port")) { runTestInClientMode(sdkHome, existingSdk); } else { runTestInServerMode(sdkHome, existingSdk); } } private void runTestInServerMode(@NotNull String sdkHome, @Nullable Sdk existingSdk) throws Exception { Project project = getProject(); myRunConfiguration = createRunConfiguration(sdkHome, existingSdk); WriteAction.runAndWait(() -> { RunManager runManager = RunManager.getInstance(project); runManager.addConfiguration(mySettings); runManager.setSelectedConfiguration(mySettings); Assert.assertSame(mySettings, runManager.getSelectedConfiguration()); }); PyDebugRunner runner = (PyDebugRunner)ProgramRunner.getRunner(getExecutorId(), mySettings.getConfiguration()); Assert.assertTrue(runner.canRun(getExecutorId(), myRunConfiguration)); Executor executor = DefaultDebugExecutor.getDebugExecutorInstance(); ExecutionEnvironment env = new ExecutionEnvironment(executor, runner, mySettings, project); PythonCommandLineState pyState = (PythonCommandLineState)myRunConfiguration.getState(executor, env); assert pyState != null; pyState.setMultiprocessDebug(isMultiprocessDebug()); try (ServerSocket serverSocket = new ServerSocket(0)) { int serverLocalPort = serverSocket.getLocalPort(); RunProfile profile = env.getRunProfile(); createExceptionBreak(myFixture, false, false, false); //turn off exception breakpoints by default before(); myTerminateSemaphore = new Semaphore(0); WriteAction.runAndWait(() -> { myExecutionResult = pyState.execute(executor, createCommandLinePatchers(runner, pyState, profile, serverLocalPort)); mySession = XDebuggerManager.getInstance(getProject()). startSession(env, new XDebugProcessStarter() { @Override @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { myDebugProcess = new PyDebugProcess(session, serverSocket, myExecutionResult.getExecutionConsole(), myExecutionResult.getProcessHandler(), isMultiprocessDebug()); myDebugProcess.getProcessHandler().addProcessListener(new ProcessAdapter() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { myOutputBuilder.append(event.getText()); if (outputType == ProcessOutputType.STDERR) { myStdErrBuilder.append(event.getText()); } } @Override public void processTerminated(@NotNull ProcessEvent event) { myTerminateSemaphore.release(); if (event.getExitCode() != 0 && !myProcessCanTerminate) { Assert.fail("Process terminated unexpectedly\n" + myOutputBuilder); } } }); myDebugProcess.getProcessHandler().startNotify(); return myDebugProcess; } }); }); myPausedSemaphore = new Semaphore(0); mySession.addSessionListener(new XDebugSessionListener() { @Override public void sessionPaused() { if (myPausedSemaphore != null) { myPausedSemaphore.release(); } } }); doTest(null); } catch (IOException e) { throw new ExecutionException("Failed to find free socket port", e); // NON-NLS } } private void runTestInClientMode(@NotNull String sdkHome, @Nullable Sdk existingSdk) throws Exception { Project project = getProject(); myRunConfiguration = createRunConfiguration(sdkHome, existingSdk); WriteAction.runAndWait(() -> { RunManager runManager = RunManager.getInstance(project); runManager.addConfiguration(mySettings); runManager.setSelectedConfiguration(mySettings); Assert.assertSame(mySettings, runManager.getSelectedConfiguration()); }); PyDebugRunner runner = (PyDebugRunner)ProgramRunner.getRunner(getExecutorId(), mySettings.getConfiguration()); Assert.assertTrue(runner.canRun(getExecutorId(), myRunConfiguration)); Executor executor = DefaultDebugExecutor.getDebugExecutorInstance(); ExecutionEnvironment env = new ExecutionEnvironment(executor, runner, mySettings, project); PythonCommandLineState pyState = (PythonCommandLineState)myRunConfiguration.getState(executor, env); assert pyState != null; pyState.setMultiprocessDebug(isMultiprocessDebug()); RunProfile profile = env.getRunProfile(); createExceptionBreak(myFixture, false, false, false); //turn off exception breakpoints by default before(); myTerminateSemaphore = new Semaphore(0); WriteAction.runAndWait(() -> { var port = PyDebuggerOptionsProvider.getInstance(project).getDebuggerPort(); TargetEnvironment.TargetPortBinding targetPortBinding = new TargetEnvironment.TargetPortBinding(port, port); var builder = runner.new PythonDebuggerServerModeTargetedCommandLineBuilder(project, pyState, profile, targetPortBinding); myExecutionResult = pyState.execute(executor, builder); mySession = XDebuggerManager.getInstance(getProject()). startSession(env, new XDebugProcessStarter() { @Override @NotNull public XDebugProcess start(@NotNull final XDebugSession session) { myDebugProcess = new PyDebugProcess(session, myExecutionResult.getExecutionConsole(), myExecutionResult.getProcessHandler(), "localhost", port); myDebugProcess.getProcessHandler().addProcessListener(new ProcessAdapter() { @Override public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { myOutputBuilder.append(event.getText()); if (outputType == ProcessOutputType.STDERR) { myStdErrBuilder.append(event.getText()); } } @Override public void processTerminated(@NotNull ProcessEvent event) { myTerminateSemaphore.release(); if (event.getExitCode() != 0 && !myProcessCanTerminate) { Assert.fail("Process terminated unexpectedly\n" + myOutputBuilder); } } }); myDebugProcess.getProcessHandler().startNotify(); return myDebugProcess; } }); }); myPausedSemaphore = new Semaphore(0); mySession.addSessionListener(new XDebugSessionListener() { @Override public void sessionPaused() { if (myPausedSemaphore != null) { myPausedSemaphore.release(); } } }); doTest(null); } @Override protected void disposeDebugProcess() { if (myDebugProcess != null) { ProcessHandler processHandler = myDebugProcess.getProcessHandler(); myDebugProcess.stop(); if (myWaitForTermination) { // for some tests (with infinite loops, for example, it has no sense) waitFor(processHandler); } try { PyDebugValueExecutionService.getInstance(getProject()).shutDownNow(NORMAL_TIMEOUT); } catch (InterruptedException e) { //pass } if (!processHandler.isProcessTerminated()) { killDebugProcess(); if (!waitFor(processHandler)) { new Throwable("Cannot stop debugger process").printStackTrace(); } } } } protected void killDebugProcess() { if (myDebugProcess.getProcessHandler() instanceof KillableColoredProcessHandler h) { h.killProcess(); } else { myDebugProcess.getProcessHandler().destroyProcess(); } } @Override protected @NotNull String output() { return myOutputBuilder.toString(); } protected @NotNull String stderr() { return myStdErrBuilder.toString(); } protected String getExecutorId() { return DefaultDebugExecutor.EXECUTOR_ID; } public boolean isMultiprocessDebug() { return myMultiprocessDebug; } public void setMultiprocessDebug(boolean multiprocessDebug) { myMultiprocessDebug = multiprocessDebug; } public void setWaitForTermination(boolean waitForTermination) { myWaitForTermination = waitForTermination; } protected void waitForAllThreadsPause() throws InterruptedException { waitForPause(); Assert.assertTrue(String.format("All threads didn't stop within timeout\n" + "Output: %s", output()), waitForAllThreads()); XDebuggerTestUtil.waitForSwing(); } protected boolean waitForAllThreads() throws InterruptedException { long until = System.currentTimeMillis() + NORMAL_TIMEOUT; while (System.currentTimeMillis() < until && getRunningThread() != null) { Thread.sleep(1000); } return getRunningThread() == null; } /** * Toggles breakpoint in the script returned by {@link PyDebuggerTask#getScriptName()}. * * @param line starting with 0 */ protected void toggleBreakpoint(int line) { toggleBreakpoint(getFilePath(getScriptName()), line); } /** * Toggles multiple breakpoints with {@link PyDebuggerTask#toggleBreakpoint(int)}. */ protected void toggleBreakpoints(int... lines) { toggleBreakpoints(getFilePath(getScriptName()), lines); } /** * Toggles multiple breakpoints with {@link PyDebuggerTask#toggleBreakpoint(String, int)}. */ protected void toggleBreakpoints(@NotNull String file, int... lines) { for (int line : lines) { toggleBreakpoint(file, line); } } }