8383821: IllegalStateException when command executed by jpackage "Executor" timeouts

Reviewed-by: asemenyuk
This commit is contained in:
Alexander Matveev 2026-05-26 22:57:07 +00:00
parent 7da2477700
commit f75692d4c8
4 changed files with 59 additions and 10 deletions

View File

@ -34,19 +34,19 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException;
public final class Codesign {
public static final class CodesignException extends Exception {
CodesignException(UnexpectedExitCodeException cause) {
CodesignException(UnexpectedResultException cause) {
super(Objects.requireNonNull(cause));
}
@Override
public UnexpectedExitCodeException getCause() {
return (UnexpectedExitCodeException)super.getCause();
public UnexpectedResultException getCause() {
return (UnexpectedResultException)super.getCause();
}
private static final long serialVersionUID = 1L;
@ -97,7 +97,7 @@ public final class Codesign {
try {
exec.execute().expectExitCode(0);
} catch (UnexpectedExitCodeException ex) {
} catch (UnexpectedResultException ex) {
throw new CodesignException(ex);
}
}

View File

@ -913,18 +913,20 @@ public final class CommandOutputControl {
});
}
public Result expectExitCode(int main, int... other) throws UnexpectedExitCodeException {
public Result expectExitCode(int main, int... other) throws UnexpectedResultException {
return expectExitCode(v -> {
return IntStream.concat(IntStream.of(main), IntStream.of(other)).boxed().anyMatch(Predicate.isEqual(v));
});
}
public Result expectExitCode(Collection<Integer> expected) throws UnexpectedExitCodeException {
public Result expectExitCode(Collection<Integer> expected) throws UnexpectedResultException {
return expectExitCode(expected::contains);
}
public Result expectExitCode(IntPredicate expected) throws UnexpectedExitCodeException {
if (!expected.test(getExitCode())) {
public Result expectExitCode(IntPredicate expected) throws UnexpectedResultException {
if (!expected.test(exitCode.orElseThrow(() -> {
return new UnavailableExitCodeException(this);
}))) {
throw new UnexpectedExitCodeException(this);
}
return this;
@ -1089,6 +1091,23 @@ public final class CommandOutputControl {
private static final long serialVersionUID = 1L;
}
public static final class UnavailableExitCodeException extends UnexpectedResultException {
public UnavailableExitCodeException(Result value, String message) {
super(value, message);
if (value.exitCode.isPresent()) {
throw new IllegalArgumentException();
}
}
public UnavailableExitCodeException(Result value) {
this(value, String.format("Exit code unavailable from executing the command %s",
value.execAttrs().printableCommandLine()));
}
private static final long serialVersionUID = 1L;
}
public String description() {
var tokens = outputStreamsControl.descriptionTokens();
if (isBinaryOutput()) {

View File

@ -42,6 +42,7 @@ import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.CommandLineFormat;
import jdk.jpackage.internal.util.CommandOutputControl;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException;
import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException;
import jdk.jpackage.internal.util.RetryExecutor;
import jdk.jpackage.internal.util.function.ExceptionBox;
@ -371,7 +372,15 @@ public final class Executor extends CommandArguments<Executor> {
int mainExpectedExitCode, int... otherExpectedExitCodes) {
return new RetryExecutor<Result, UnexpectedExitCodeException>(UnexpectedExitCodeException.class).setExecutable(() -> {
var result = executeWithoutExitCodeCheck();
result.base().expectExitCode(mainExpectedExitCode, otherExpectedExitCodes);
try {
result.base().expectExitCode(mainExpectedExitCode, otherExpectedExitCodes);
} catch (UnexpectedResultException ex) {
if (ex instanceof UnexpectedExitCodeException uecex) {
throw uecex; // Pass to exception mapper
}
// Unreachable, because the result must always have the exit code, as the executor never runs commands with a timeout.
throw ExceptionBox.reachedUnreachable();
}
return result;
}).setExceptionMapper((UnexpectedExitCodeException ex) -> {
createResult(ex.getResult()).assertExitCodeIs(mainExpectedExitCode, otherExpectedExitCodes);

View File

@ -261,6 +261,20 @@ public class CommandOutputControlTest {
assertEquals("Unexpected exit code 3 from executing the command <unknown>", ex.getMessage());
}
@Test
public void test_Result_expectExitCode_unavailable() {
var result = CommandOutputControl.Result.build().noExitCode().create();
var ex = assertThrowsExactly(CommandOutputControl.UnavailableExitCodeException.class, () -> {
result.expectExitCode(0);
});
assertNull(ex.getCause());
assertSame(result, ex.getResult());
assertEquals(String.format("Exit code unavailable from executing the command %s",
result.execAttrs().printableCommandLine()), ex.getMessage());
}
@ParameterizedTest
@MethodSource
public void test_Result_toCharacterResult(ToCharacterResultTestSpec spec) throws IOException, InterruptedException {
@ -352,6 +366,13 @@ public class CommandOutputControlTest {
var getExitCodeEx = assertThrowsExactly(IllegalStateException.class, result::getExitCode);
assertEquals(("Exit code is unavailable for timed-out command"), getExitCodeEx.getMessage());
// Verify UnavailableExitCodeException
var expectExitCodeEx = assertThrowsExactly(CommandOutputControl.UnavailableExitCodeException.class, () -> {
result.expectExitCode(0);
});
assertEquals(String.format("Exit code unavailable from executing the command %s",
result.execAttrs().printableCommandLine()), expectExitCodeEx.getMessage());
// We want to check that the saved output contains only the text emitted before the "sleep" action.
// It works for a subprocess, but in the case of a ToolProvider, sometimes the timing is such
// that it gets interrupted before having written anything to the stdout, and the saved output is empty.