mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-07 22:09:38 +07:00
PY-78649: Improve error handling.
There were three problems: 1. There was `PyExecutionException` with an optional message which was intended to be displayed via dialog using a message as a title. It breaks an exception contract (code that isn't aware of this particular class should still be able to fetch info, and a message is usually blank). 2. Moreover, `ErrorSink` wasn't aware of it either and displayed it as a plain text. 3. `PyExecutionException` didn't distinguish between "process can't be started" and "process died with error code != 0". Those are different cases. This change: 1. Fixes `PyExecutionException` for 1 and 3. 2. Introduces API in `ErrorSink.kt` to display `PyExecutionException` GitOrigin-RevId: a8d835afb086b23c73ced15f243d2b27b59dcf82
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4b22bc0b72
commit
ca2148932f
@@ -5,6 +5,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
python.execution.error={0}\nThe following command finished with error: {1}\nOutput: {2}\nError: {3}\Exit code: {4}
|
||||
python.execution.cant.start.error={0}\nThe following command could not be started: {1}
|
||||
27
python/openapi/src/com/jetbrains/python/PyCommunityBundle.kt
Normal file
27
python/openapi/src/com/jetbrains/python/PyCommunityBundle.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python
|
||||
|
||||
import com.intellij.DynamicBundle
|
||||
import org.jetbrains.annotations.Nls
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.function.Supplier
|
||||
|
||||
internal object PyCommunityBundle {
|
||||
private const val BUNDLE_FQN: @NonNls String = "messages.PyCommunityBundle"
|
||||
private val BUNDLE = DynamicBundle(PyCommunityBundle::class.java, BUNDLE_FQN)
|
||||
|
||||
fun message(
|
||||
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
|
||||
vararg params: Any
|
||||
): @Nls String {
|
||||
return BUNDLE.getMessage(key, *params)
|
||||
}
|
||||
|
||||
fun messagePointer(
|
||||
key: @PropertyKey(resourceBundle = BUNDLE_FQN) String,
|
||||
vararg params: Any
|
||||
): Supplier<String> {
|
||||
return BUNDLE.getLazyMessage(key, *params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.execution
|
||||
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.jetbrains.python.packaging.PyExecutionException
|
||||
|
||||
/**
|
||||
* Types of execution error for [PyExecutionFailure]
|
||||
*/
|
||||
sealed interface FailureReason {
|
||||
/**
|
||||
* A process failed to start, or the code that ought to start it decided not to run it.
|
||||
* That means the process hasn't been even created.
|
||||
*/
|
||||
data object CantStart : FailureReason
|
||||
|
||||
/**
|
||||
* A process started but failed with an error. See [output] for the result
|
||||
*/
|
||||
data class ExecutionFailed(val output: ProcessOutput) : FailureReason
|
||||
}
|
||||
|
||||
internal fun copyWith(ex: PyExecutionException, newCommand: String, newArgs: List<String>): PyExecutionException =
|
||||
when (val err = ex.failureReason) {
|
||||
FailureReason.CantStart -> {
|
||||
PyExecutionException(ex.additionalMessage, newCommand, newArgs, ex.fixes)
|
||||
}
|
||||
is FailureReason.ExecutionFailed -> {
|
||||
PyExecutionException(ex.additionalMessage, newCommand, newArgs, err.output, ex.fixes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.execution
|
||||
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
|
||||
/**
|
||||
* Some command can't be executed
|
||||
*/
|
||||
interface PyExecutionFailure {
|
||||
val command: String
|
||||
|
||||
val args: List<String>
|
||||
|
||||
/**
|
||||
* optional message to be displayed to the user
|
||||
*/
|
||||
val additionalMessage: @NlsContexts.DialogTitle String?
|
||||
|
||||
|
||||
val failureReason: FailureReason
|
||||
}
|
||||
@@ -1,63 +1,124 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.packaging;
|
||||
|
||||
import com.intellij.execution.ExecutionExceptionWithAttachments;
|
||||
import com.intellij.execution.ExecutionException;
|
||||
import com.intellij.execution.process.ProcessOutput;
|
||||
import com.intellij.openapi.util.NlsContexts.DialogMessage;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.jetbrains.python.PyCommunityBundle;
|
||||
import com.jetbrains.python.execution.FailureReason;
|
||||
import com.jetbrains.python.execution.FailureReasonKt;
|
||||
import com.jetbrains.python.execution.PyExecutionFailure;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class PyExecutionException extends ExecutionExceptionWithAttachments {
|
||||
/**
|
||||
* Process execution failed.
|
||||
* There are two cases, see {@link FailureReason}.
|
||||
* Each constructor represents one or another.
|
||||
*
|
||||
* @see FailureReason
|
||||
*/
|
||||
public final class PyExecutionException extends ExecutionException implements PyExecutionFailure {
|
||||
private final @NotNull String myCommand;
|
||||
private final @NotNull List<String> myArgs;
|
||||
private final int myExitCode;
|
||||
private final @NotNull List<? extends PyExecutionFix> myFixes;
|
||||
private final @DialogMessage @Nullable String myAdditionalMessage;
|
||||
private final @NotNull FailureReason myError;
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message, @NotNull String command, @NotNull List<String> args) {
|
||||
this(message, command, args, "", "", 0, Collections.emptyList());
|
||||
/**
|
||||
* A process failed to start, {@link FailureReason.CantStart}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args) {
|
||||
this(additionalMessage, command, args, Collections.emptyList());
|
||||
}
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message,
|
||||
/**
|
||||
* A process failed to start, {@link FailureReason.CantStart}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
super(PyCommunityBundle.INSTANCE.message("python.execution.cant.start.error",
|
||||
additionalMessage != null ? additionalMessage : "",
|
||||
command + " " + StringUtil.join(args, " ")));
|
||||
myAdditionalMessage = additionalMessage;
|
||||
myCommand = command;
|
||||
myArgs = args;
|
||||
myFixes = fixes;
|
||||
myError = FailureReason.CantStart.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
* @param output execution output
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull ProcessOutput output) {
|
||||
this(message, command, args, output.getStdout(), output.getStderr(), output.getExitCode(), Collections.emptyList());
|
||||
this(additionalMessage, command, args, output, Collections.emptyList());
|
||||
}
|
||||
|
||||
public PyExecutionException(@DialogMessage @NotNull String message, @NotNull String command, @NotNull List<String> args,
|
||||
@NotNull String stdout, @NotNull String stderr, int exitCode,
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
* @param output execution output
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull ProcessOutput output,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
super(message, stdout, stderr);
|
||||
super(PyCommunityBundle.INSTANCE.message("python.execution.error",
|
||||
additionalMessage != null ? additionalMessage : "",
|
||||
command + " " + StringUtil.join(args, " "),
|
||||
output.getStdout(),
|
||||
output.getStderr(),
|
||||
output.getExitCode()
|
||||
));
|
||||
myAdditionalMessage = additionalMessage;
|
||||
myCommand = command;
|
||||
myArgs = args;
|
||||
myExitCode = exitCode;
|
||||
myFixes = fixes;
|
||||
myError = new FailureReason.ExecutionFailed(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* A process started, but failed {@link FailureReason.ExecutionFailed}
|
||||
*
|
||||
* @param additionalMessage a process start reason for a user
|
||||
*/
|
||||
public PyExecutionException(@DialogMessage @Nullable String additionalMessage,
|
||||
@NotNull String command,
|
||||
@NotNull List<String> args,
|
||||
@NotNull String stdout,
|
||||
@NotNull String stderr,
|
||||
int exitCode,
|
||||
@NotNull List<? extends PyExecutionFix> fixes) {
|
||||
this(additionalMessage, command, args, new ProcessOutput(stdout, stderr, exitCode, false, false), fixes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder b = new StringBuilder();
|
||||
b.append("The following command was executed:\n\n");
|
||||
final String command = getCommand() + " " + StringUtil.join(getArgs(), " ");
|
||||
b.append(command);
|
||||
b.append("\n\n");
|
||||
b.append("The exit code: ").append(myExitCode).append("\n");
|
||||
b.append("The error output of the command:\n\n");
|
||||
b.append(getStdout());
|
||||
b.append("\n");
|
||||
b.append(getStderr());
|
||||
b.append("\n");
|
||||
b.append(getMessage());
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
public @NotNull String getCommand() {
|
||||
return myCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> getArgs() {
|
||||
return myArgs;
|
||||
}
|
||||
@@ -66,7 +127,30 @@ public class PyExecutionException extends ExecutionExceptionWithAttachments {
|
||||
return myFixes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getAdditionalMessage() {
|
||||
return myAdditionalMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull FailureReason getFailureReason() {
|
||||
return myError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #getFailureReason()} and match it as when process failed to start there is no exit code
|
||||
*/
|
||||
@Deprecated(forRemoval = true)
|
||||
public int getExitCode() {
|
||||
return myExitCode;
|
||||
if (getFailureReason() instanceof FailureReason.ExecutionFailed executionFailed) {
|
||||
return executionFailed.getOutput().getExitCode();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
public PyExecutionException copyWith(@NotNull String newCommand, @NotNull List<@NotNull String> newArgs) {
|
||||
return FailureReasonKt.copyWith(this, newCommand, newArgs);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user