diff --git a/.github/workflows/build-alpine-linux.yml b/.github/workflows/build-alpine-linux.yml index 569893d5ccc..c39962fa07f 100644 --- a/.github/workflows/build-alpine-linux.yml +++ b/.github/workflows/build-alpine-linux.yml @@ -97,7 +97,7 @@ jobs: --with-zlib=system --with-jmod-compress=zip-1 --with-external-symbols-in-bundles=none - --with-debug-info-level=1 + --with-native-debug-symbols-level=1 ${{ inputs.extra-conf-options }} ${{ inputs.configure-arguments }} || ( echo "Dumping config.log:" && cat config.log && diff --git a/.github/workflows/build-cross-compile.yml b/.github/workflows/build-cross-compile.yml index 4860228c7de..a0642d469aa 100644 --- a/.github/workflows/build-cross-compile.yml +++ b/.github/workflows/build-cross-compile.yml @@ -180,7 +180,7 @@ jobs: --with-sysroot=sysroot --with-jmod-compress=zip-1 --with-external-symbols-in-bundles=none - --with-debug-info-level=1 + --with-native-debug-symbols-level=1 CC=${{ matrix.gnu-arch }}-linux-gnu${{ matrix.gnu-abi}}-gcc-${{ inputs.gcc-major-version }} CXX=${{ matrix.gnu-arch }}-linux-gnu${{ matrix.gnu-abi}}-g++-${{ inputs.gcc-major-version }} ${{ inputs.extra-conf-options }} ${{ inputs.configure-arguments }} || ( diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 121416106f2..791b53a3f04 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -144,7 +144,7 @@ jobs: --with-zlib=system --with-jmod-compress=zip-1 --with-external-symbols-in-bundles=none - --with-debug-info-level=1 + --with-native-debug-symbols-level=1 ${{ inputs.extra-conf-options }} ${{ inputs.configure-arguments }} || ( echo "Dumping config.log:" && cat config.log && diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 32c5d43acbc..484e616fad7 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -111,7 +111,7 @@ jobs: --with-zlib=system --with-jmod-compress=zip-1 --with-external-symbols-in-bundles=none - --with-debug-info-level=1 + --with-native-debug-symbols-level=1 ${{ inputs.extra-conf-options }} ${{ inputs.configure-arguments }} || ( echo "Dumping config.log:" && cat config.log && diff --git a/make/autoconf/flags-cflags.m4 b/make/autoconf/flags-cflags.m4 index b7b2a6b5e17..5a9fdc57c74 100644 --- a/make/autoconf/flags-cflags.m4 +++ b/make/autoconf/flags-cflags.m4 @@ -69,20 +69,20 @@ AC_DEFUN([FLAGS_SETUP_DEBUG_SYMBOLS], # Debug prefix mapping if supported by compiler DEBUG_PREFIX_CFLAGS= - UTIL_ARG_WITH(NAME: debug-info-level, TYPE: string, + UTIL_ARG_WITH(NAME: native-debug-symbols-level, TYPE: string, DEFAULT: "", - RESULT: DEBUG_INFO_LEVEL, - DESC: [Sets the debug info level, when debug info generation is enabled (GCC and Clang only)], - DEFAULT_DESC: [default]) - AC_SUBST(DEBUG_INFO_LEVEL) + RESULT: DEBUG_SYMBOLS_LEVEL, + DESC: [set the native debug symbol level (GCC and Clang only)], + DEFAULT_DESC: [toolchain default]) + AC_SUBST(DEBUG_SYMBOLS_LEVEL) if test "x${TOOLCHAIN_TYPE}" = xgcc || \ test "x${TOOLCHAIN_TYPE}" = xclang; then - DEBUG_INFO_LEVEL_FLAGS="-g" - if test "x${DEBUG_INFO_LEVEL}" != "x"; then - DEBUG_INFO_LEVEL_FLAGS="-g${DEBUG_INFO_LEVEL}" - FLAGS_COMPILER_CHECK_ARGUMENTS(ARGUMENT: [${DEBUG_INFO_LEVEL_FLAGS}], - IF_FALSE: AC_MSG_ERROR("Debug info level ${DEBUG_INFO_LEVEL} is not supported")) + DEBUG_SYMBOLS_LEVEL_FLAGS="-g" + if test "x${DEBUG_SYMBOLS_LEVEL}" != "x"; then + DEBUG_SYMBOLS_LEVEL_FLAGS="-g${DEBUG_SYMBOLS_LEVEL}" + FLAGS_COMPILER_CHECK_ARGUMENTS(ARGUMENT: [${DEBUG_SYMBOLS_LEVEL_FLAGS}], + IF_FALSE: AC_MSG_ERROR("Debug info level ${DEBUG_SYMBOLS_LEVEL} is not supported")) fi fi @@ -111,8 +111,8 @@ AC_DEFUN([FLAGS_SETUP_DEBUG_SYMBOLS], fi # Debug info level should follow the debug format to be effective. - CFLAGS_DEBUG_SYMBOLS="-gdwarf-4 ${DEBUG_INFO_LEVEL_FLAGS}" - ASFLAGS_DEBUG_SYMBOLS="${DEBUG_INFO_LEVEL_FLAGS}" + CFLAGS_DEBUG_SYMBOLS="-gdwarf-4 ${DEBUG_SYMBOLS_LEVEL_FLAGS}" + ASFLAGS_DEBUG_SYMBOLS="${DEBUG_SYMBOLS_LEVEL_FLAGS}" elif test "x$TOOLCHAIN_TYPE" = xclang; then if test "x$ALLOW_ABSOLUTE_PATHS_IN_OUTPUT" = "xfalse"; then # Check if compiler supports -fdebug-prefix-map. If so, use that to make @@ -132,8 +132,8 @@ AC_DEFUN([FLAGS_SETUP_DEBUG_SYMBOLS], IF_FALSE: [GDWARF_FLAGS=""]) # Debug info level should follow the debug format to be effective. - CFLAGS_DEBUG_SYMBOLS="${GDWARF_FLAGS} ${DEBUG_INFO_LEVEL_FLAGS}" - ASFLAGS_DEBUG_SYMBOLS="${DEBUG_INFO_LEVEL_FLAGS}" + CFLAGS_DEBUG_SYMBOLS="${GDWARF_FLAGS} ${DEBUG_SYMBOLS_LEVEL_FLAGS}" + ASFLAGS_DEBUG_SYMBOLS="${DEBUG_SYMBOLS_LEVEL_FLAGS}" elif test "x$TOOLCHAIN_TYPE" = xmicrosoft; then CFLAGS_DEBUG_SYMBOLS="-Z7" fi diff --git a/make/common/Utils.gmk b/make/common/Utils.gmk index c0ebabca3f7..b4bb949d3ee 100644 --- a/make/common/Utils.gmk +++ b/make/common/Utils.gmk @@ -114,7 +114,7 @@ EscapeDollar = $(subst $$,\$$,$(subst \$$,$$,$(strip $1))) ################################################################################ # This macro works just like EscapeDollar above, but for #. -EscapeHash = $(subst \#,\\\#,$(subst \\\#,\#,$(strip $1))) +EscapeHash = $(subst $(HASH),\$(HASH),$(subst \$(HASH),$(HASH),$(strip $1))) ################################################################################ # This macro translates $ into $$ to protect the string from make itself. diff --git a/make/langtools/tools/propertiesparser/gen/ClassGenerator.java b/make/langtools/tools/propertiesparser/gen/ClassGenerator.java index 247537b4676..14e8c4fb00a 100644 --- a/make/langtools/tools/propertiesparser/gen/ClassGenerator.java +++ b/make/langtools/tools/propertiesparser/gen/ClassGenerator.java @@ -286,7 +286,7 @@ public class ClassGenerator { diagnosticFlags.isEmpty() ? StubKind.DIAGNOSTIC_FLAGS_EMPTY.format() : StubKind.DIAGNOSTIC_FLAGS_NON_EMPTY.format(diagnosticFlags), - StubKind.LINT_CATEGORY.format("\"" + lintCategory + "\""), + StubKind.LINT_CATEGORY.format(toLintFieldName(lintCategory)), "\"" + keyParts[0] + "\"", "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"", javadoc); @@ -314,7 +314,7 @@ public class ClassGenerator { diagnosticFlags.isEmpty() ? StubKind.DIAGNOSTIC_FLAGS_EMPTY.format() : StubKind.DIAGNOSTIC_FLAGS_NON_EMPTY.format(diagnosticFlags), - StubKind.LINT_CATEGORY.format("\"" + lintCategory + "\""), + StubKind.LINT_CATEGORY.format(toLintFieldName(lintCategory)), "\"" + keyParts[0] + "\"", "\"" + Stream.of(keyParts).skip(2).collect(Collectors.joining(".")) + "\"", argNames.stream().collect(Collectors.joining(", "))); @@ -329,6 +329,11 @@ public class ClassGenerator { } } + String toLintFieldName(String lintCategory) { + return lintCategory.toUpperCase() + .replaceAll("-", "_"); + } + /** * Form the name of a factory method/field given a resource key. */ diff --git a/make/langtools/tools/propertiesparser/resources/templates.properties b/make/langtools/tools/propertiesparser/resources/templates.properties index 81a9be2552c..f8ff07a878f 100644 --- a/make/langtools/tools/propertiesparser/resources/templates.properties +++ b/make/langtools/tools/propertiesparser/resources/templates.properties @@ -87,7 +87,7 @@ suppress.warnings=\ @SuppressWarnings("rawtypes")\n lint.category=\ - LintCategory.get({0}).get() + LintCategory.{0} diagnostic.flags.empty=\ EnumSet.noneOf(DiagnosticFlag.class) diff --git a/src/hotspot/cpu/aarch64/immediate_aarch64.cpp b/src/hotspot/cpu/aarch64/immediate_aarch64.cpp index 710a1f0677d..eb4cb23100c 100644 --- a/src/hotspot/cpu/aarch64/immediate_aarch64.cpp +++ b/src/hotspot/cpu/aarch64/immediate_aarch64.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2020, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -23,13 +23,13 @@ * */ -#include // do not reorder -#include // do not reorder - +#include "cppstdlib/cstdlib.hpp" #include "immediate_aarch64.hpp" #include "metaprogramming/primitiveConversions.hpp" #include "utilities/globalDefinitions.hpp" +#include + // there are at most 2^13 possible logical immediate encodings // however, some combinations of immr and imms are invalid static const unsigned LI_TABLE_SIZE = (1 << 13); diff --git a/src/hotspot/cpu/aarch64/immediate_aarch64.hpp b/src/hotspot/cpu/aarch64/immediate_aarch64.hpp index 0cbdb562088..fb38995417b 100644 --- a/src/hotspot/cpu/aarch64/immediate_aarch64.hpp +++ b/src/hotspot/cpu/aarch64/immediate_aarch64.hpp @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -22,10 +23,10 @@ * */ -#ifndef _IMMEDIATE_H -#define _IMMEDIATE_H +#ifndef CPU_AARCH64_IMMEDIATE_AARCH64_HPP +#define CPU_AARCH64_IMMEDIATE_AARCH64_HPP -#include +#include /* * functions to map backwards and forwards between logical or floating @@ -51,4 +52,4 @@ uint32_t encoding_for_logical_immediate(uint64_t immediate); uint64_t fp_immediate_for_encoding(uint32_t imm8, int is_dp); uint32_t encoding_for_fp_immediate(float immediate); -#endif // _IMMEDIATE_H +#endif // CPU_AARCH64_IMMEDIATE_AARCH64_HPP diff --git a/src/hotspot/os/aix/libodm_aix.cpp b/src/hotspot/os/aix/libodm_aix.cpp index 035703a1771..38e8067181a 100644 --- a/src/hotspot/os/aix/libodm_aix.cpp +++ b/src/hotspot/os/aix/libodm_aix.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2015, 2019 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -23,15 +23,15 @@ * */ +#include "cppstdlib/cstdlib.hpp" #include "libodm_aix.hpp" #include "misc_aix.hpp" -#include -#include -#include #include "runtime/arguments.hpp" #include "runtime/os.hpp" #include "utilities/permitForbiddenFunctions.hpp" +#include +#include dynamicOdm::dynamicOdm() { const char* libodmname = "/usr/lib/libodm.a(shr_64.o)"; diff --git a/src/hotspot/os/aix/libperfstat_aix.hpp b/src/hotspot/os/aix/libperfstat_aix.hpp index 5a6ddfd8f4d..aff11cfb41f 100644 --- a/src/hotspot/os/aix/libperfstat_aix.hpp +++ b/src/hotspot/os/aix/libperfstat_aix.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2024 SAP SE. All rights reserved. * Copyright (c) 2022, IBM Corp. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -32,8 +32,9 @@ #ifndef OS_AIX_LIBPERFSTAT_AIX_HPP #define OS_AIX_LIBPERFSTAT_AIX_HPP +#include "cppstdlib/cstdlib.hpp" + #include -#include /////////////////////////////////////////////////////////////////////////////////////////////// // These are excerpts from the AIX 7.1 libperfstat.h - diff --git a/src/hotspot/os/aix/os_perf_aix.cpp b/src/hotspot/os/aix/os_perf_aix.cpp index 8444002b871..aa8819d035f 100644 --- a/src/hotspot/os/aix/os_perf_aix.cpp +++ b/src/hotspot/os/aix/os_perf_aix.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2022, 2024, IBM Corp. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -23,6 +23,7 @@ * */ +#include "cppstdlib/cstdlib.hpp" #include "jvm.h" #include "libperfstat_aix.hpp" #include "memory/allocation.inline.hpp" @@ -46,7 +47,6 @@ #include #include #include -#include #include typedef struct { diff --git a/src/hotspot/os/bsd/memMapPrinter_macosx.cpp b/src/hotspot/os/bsd/memMapPrinter_macosx.cpp index a7ddab04d85..6fd08d63e85 100644 --- a/src/hotspot/os/bsd/memMapPrinter_macosx.cpp +++ b/src/hotspot/os/bsd/memMapPrinter_macosx.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2023, 2024, Red Hat, Inc. and/or its affiliates. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -25,6 +25,7 @@ #if defined(__APPLE__) +#include "cppstdlib/cstdlib.hpp" #include "nmt/memMapPrinter.hpp" #include "runtime/os.hpp" #include "utilities/align.hpp" @@ -34,7 +35,6 @@ #include #include -#include #include #include diff --git a/src/hotspot/os/linux/os_linux.cpp b/src/hotspot/os/linux/os_linux.cpp index a28f9850495..88e5e9b582a 100644 --- a/src/hotspot/os/linux/os_linux.cpp +++ b/src/hotspot/os/linux/os_linux.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2015, 2024 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -27,6 +27,7 @@ #include "code/vtableStubs.hpp" #include "compiler/compileBroker.hpp" #include "compiler/disassembler.hpp" +#include "cppstdlib/cstdlib.hpp" #include "hugepages.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" @@ -96,7 +97,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os/linux/os_perf_linux.cpp b/src/hotspot/os/linux/os_perf_linux.cpp index 5708194521f..9f91f3b4c0d 100644 --- a/src/hotspot/os/linux/os_perf_linux.cpp +++ b/src/hotspot/os/linux/os_perf_linux.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -22,6 +22,7 @@ * */ +#include "cppstdlib/cstdlib.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" #include "os_linux.inline.hpp" @@ -40,7 +41,6 @@ #include #include #include -#include #include #include #include diff --git a/src/hotspot/os/posix/forbiddenFunctions_posix.hpp b/src/hotspot/os/posix/forbiddenFunctions_posix.hpp index 529cf22078e..1c91e9d2599 100644 --- a/src/hotspot/os/posix/forbiddenFunctions_posix.hpp +++ b/src/hotspot/os/posix/forbiddenFunctions_posix.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -36,15 +36,12 @@ #include #endif +// POSIX puts _exit in . +FORBID_IMPORTED_NORETURN_C_FUNCTION(void _exit(int), /* not noexcept */, "use os::exit") + // If needed, add os::strndup and use that instead. FORBID_C_FUNCTION(char* strndup(const char*, size_t), noexcept, "don't use"); -// These are unimplementable for Windows, and they aren't useful for a -// POSIX implementation of NMT either. -// https://stackoverflow.com/questions/62962839/stdaligned-alloc-missing-from-visual-studio-2019 -FORBID_C_FUNCTION(int posix_memalign(void**, size_t, size_t), noexcept, "don't use"); -FORBID_C_FUNCTION(void* aligned_alloc(size_t, size_t), noexcept, "don't use"); - // realpath with a null second argument mallocs a string for the result. // With a non-null second argument, there is a risk of buffer overrun. PRAGMA_DIAG_PUSH diff --git a/src/hotspot/os/posix/os_posix.cpp b/src/hotspot/os/posix/os_posix.cpp index 07e1920c62d..4cae7d359e4 100644 --- a/src/hotspot/os/posix/os_posix.cpp +++ b/src/hotspot/os/posix/os_posix.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, 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 @@ -23,6 +23,7 @@ */ #include "classfile/classLoader.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "jvmtifiles/jvmti.h" diff --git a/src/hotspot/os/posix/permitForbiddenFunctions_posix.hpp b/src/hotspot/os/posix/permitForbiddenFunctions_posix.hpp index 3ff8c383a31..b45cede2e5f 100644 --- a/src/hotspot/os/posix/permitForbiddenFunctions_posix.hpp +++ b/src/hotspot/os/posix/permitForbiddenFunctions_posix.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -25,15 +25,20 @@ #ifndef OS_POSIX_PERMITFORBIDDENFUNCTIONS_POSIX_HPP #define OS_POSIX_PERMITFORBIDDENFUNCTIONS_POSIX_HPP +#include "cppstdlib/cstdlib.hpp" #include "utilities/compilerWarnings.hpp" #include "utilities/globalDefinitions.hpp" +#include + // Provide wrappers for some functions otherwise forbidden from use in HotSpot. // See forbiddenFunctions.hpp for details. namespace permit_forbidden_function { BEGIN_ALLOW_FORBIDDEN_FUNCTIONS +[[noreturn]] inline void _exit(int status) { ::_exit(status); } + // Used by the POSIX implementation of os::realpath. inline char* realpath(const char* path, char* resolved_path) { return ::realpath(path, resolved_path); diff --git a/src/hotspot/os/windows/os_windows.cpp b/src/hotspot/os/windows/os_windows.cpp index 0ac05e8a435..efbd1fe7c68 100644 --- a/src/hotspot/os/windows/os_windows.cpp +++ b/src/hotspot/os/windows/os_windows.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -30,6 +30,7 @@ #include "code/vtableStubs.hpp" #include "compiler/compileBroker.hpp" #include "compiler/disassembler.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "jvmtifiles/jvmti.h" diff --git a/src/hotspot/os/windows/permitForbiddenFunctions_windows.hpp b/src/hotspot/os/windows/permitForbiddenFunctions_windows.hpp index 99e77464fbd..fbd476ceb60 100644 --- a/src/hotspot/os/windows/permitForbiddenFunctions_windows.hpp +++ b/src/hotspot/os/windows/permitForbiddenFunctions_windows.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -25,6 +25,7 @@ #ifndef OS_WINDOWS_PERMITFORBIDDENFUNCTIONS_WINDOWS_HPP #define OS_WINDOWS_PERMITFORBIDDENFUNCTIONS_WINDOWS_HPP +#include "cppstdlib/cstdlib.hpp" #include "utilities/compilerWarnings.hpp" #include "utilities/globalDefinitions.hpp" @@ -34,6 +35,8 @@ namespace permit_forbidden_function { BEGIN_ALLOW_FORBIDDEN_FUNCTIONS +[[noreturn]] inline void _exit(int status) { ::_exit(status); } + // Used by the Windows implementation of os::realpath. inline char* _fullpath(char* absPath, const char* relPath, size_t maxLength) { return ::_fullpath(absPath, relPath, maxLength); diff --git a/src/hotspot/os/windows/vmError_windows.cpp b/src/hotspot/os/windows/vmError_windows.cpp index 22df4d82cbf..66df38d2734 100644 --- a/src/hotspot/os/windows/vmError_windows.cpp +++ b/src/hotspot/os/windows/vmError_windows.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, 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 @@ -24,6 +24,7 @@ #include "cds/aotMetaspace.hpp" #include "cds/cdsConfig.hpp" +#include "cppstdlib/cstdlib.hpp" #include "runtime/arguments.hpp" #include "runtime/javaThread.hpp" #include "runtime/os.hpp" diff --git a/src/hotspot/os_cpu/aix_ppc/prefetch_aix_ppc.inline.hpp b/src/hotspot/os_cpu/aix_ppc/prefetch_aix_ppc.inline.hpp index 1f9917acae7..c741335b5f0 100644 --- a/src/hotspot/os_cpu/aix_ppc/prefetch_aix_ppc.inline.hpp +++ b/src/hotspot/os_cpu/aix_ppc/prefetch_aix_ppc.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2013 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,8 +26,7 @@ #ifndef OS_CPU_AIX_PPC_PREFETCH_AIX_PPC_INLINE_HPP #define OS_CPU_AIX_PPC_PREFETCH_AIX_PPC_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read(const void *loc, intx interval) { #if !defined(USE_XLC_BUILTINS) diff --git a/src/hotspot/os_cpu/bsd_aarch64/os_bsd_aarch64.cpp b/src/hotspot/os_cpu/bsd_aarch64/os_bsd_aarch64.cpp index f6e2d39e315..62dba218b2f 100644 --- a/src/hotspot/os_cpu/bsd_aarch64/os_bsd_aarch64.cpp +++ b/src/hotspot/os_cpu/bsd_aarch64/os_bsd_aarch64.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * Copyright (c) 2021, Azul Systems, Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -29,6 +29,7 @@ #include "classfile/vmSymbols.hpp" #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "logging/log.hpp" @@ -63,7 +64,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/bsd_aarch64/prefetch_bsd_aarch64.inline.hpp b/src/hotspot/os_cpu/bsd_aarch64/prefetch_bsd_aarch64.inline.hpp index 1bcbdcaf90d..185f9b54144 100644 --- a/src/hotspot/os_cpu/bsd_aarch64/prefetch_bsd_aarch64.inline.hpp +++ b/src/hotspot/os_cpu/bsd_aarch64/prefetch_bsd_aarch64.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * Copyright (c) 2021, Azul Systems, Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -27,8 +27,7 @@ #ifndef OS_CPU_BSD_AARCH64_PREFETCH_BSD_AARCH64_INLINE_HPP #define OS_CPU_BSD_AARCH64_PREFETCH_BSD_AARCH64_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { if (interval >= 0) diff --git a/src/hotspot/os_cpu/bsd_x86/os_bsd_x86.cpp b/src/hotspot/os_cpu/bsd_x86/os_bsd_x86.cpp index 420e7b4cd8d..f1c80594eaf 100644 --- a/src/hotspot/os_cpu/bsd_x86/os_bsd_x86.cpp +++ b/src/hotspot/os_cpu/bsd_x86/os_bsd_x86.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, 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 @@ -26,6 +26,7 @@ #include "classfile/vmSymbols.hpp" #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "logging/log.hpp" @@ -58,7 +59,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/bsd_x86/prefetch_bsd_x86.inline.hpp b/src/hotspot/os_cpu/bsd_x86/prefetch_bsd_x86.inline.hpp index 52cc405e211..464740920a1 100644 --- a/src/hotspot/os_cpu/bsd_x86/prefetch_bsd_x86.inline.hpp +++ b/src/hotspot/os_cpu/bsd_x86/prefetch_bsd_x86.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, 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 @@ -25,8 +25,7 @@ #ifndef OS_CPU_BSD_X86_PREFETCH_BSD_X86_INLINE_HPP #define OS_CPU_BSD_X86_PREFETCH_BSD_X86_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { __asm__ ("prefetcht0 (%0,%1,1)" : : "r" (loc), "r" (interval)); diff --git a/src/hotspot/os_cpu/bsd_zero/prefetch_bsd_zero.inline.hpp b/src/hotspot/os_cpu/bsd_zero/prefetch_bsd_zero.inline.hpp index 220d6a08f68..5edf3744719 100644 --- a/src/hotspot/os_cpu/bsd_zero/prefetch_bsd_zero.inline.hpp +++ b/src/hotspot/os_cpu/bsd_zero/prefetch_bsd_zero.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright 2007, 2008 Red Hat, Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,7 +26,7 @@ #ifndef OS_CPU_BSD_ZERO_PREFETCH_BSD_ZERO_INLINE_HPP #define OS_CPU_BSD_ZERO_PREFETCH_BSD_ZERO_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read(const void* loc, intx interval) { } diff --git a/src/hotspot/os_cpu/linux_aarch64/os_linux_aarch64.cpp b/src/hotspot/os_cpu/linux_aarch64/os_linux_aarch64.cpp index e565f353382..da9e7e159f1 100644 --- a/src/hotspot/os_cpu/linux_aarch64/os_linux_aarch64.cpp +++ b/src/hotspot/os_cpu/linux_aarch64/os_linux_aarch64.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -28,6 +28,7 @@ #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" #include "code/nativeInst.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -59,7 +60,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_aarch64/prefetch_linux_aarch64.inline.hpp b/src/hotspot/os_cpu/linux_aarch64/prefetch_linux_aarch64.inline.hpp index 168a680a404..580e9f4ffa2 100644 --- a/src/hotspot/os_cpu/linux_aarch64/prefetch_linux_aarch64.inline.hpp +++ b/src/hotspot/os_cpu/linux_aarch64/prefetch_linux_aarch64.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, Red Hat Inc. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,8 +26,7 @@ #ifndef OS_CPU_LINUX_AARCH64_PREFETCH_LINUX_AARCH64_INLINE_HPP #define OS_CPU_LINUX_AARCH64_PREFETCH_LINUX_AARCH64_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { if (interval >= 0) diff --git a/src/hotspot/os_cpu/linux_arm/os_linux_arm.cpp b/src/hotspot/os_cpu/linux_arm/os_linux_arm.cpp index f4196d89fe3..41a4dbea384 100644 --- a/src/hotspot/os_cpu/linux_arm/os_linux_arm.cpp +++ b/src/hotspot/os_cpu/linux_arm/os_linux_arm.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2026, 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 @@ -25,6 +25,7 @@ #include "asm/assembler.inline.hpp" #include "classfile/vmSymbols.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -57,7 +58,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_arm/prefetch_linux_arm.inline.hpp b/src/hotspot/os_cpu/linux_arm/prefetch_linux_arm.inline.hpp index dfbab55d9b9..a536c17fbe3 100644 --- a/src/hotspot/os_cpu/linux_arm/prefetch_linux_arm.inline.hpp +++ b/src/hotspot/os_cpu/linux_arm/prefetch_linux_arm.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2026, 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 @@ -25,7 +25,7 @@ #ifndef OS_CPU_LINUX_ARM_PREFETCH_LINUX_ARM_INLINE_HPP #define OS_CPU_LINUX_ARM_PREFETCH_LINUX_ARM_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { #if defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_5TE__) diff --git a/src/hotspot/os_cpu/linux_ppc/os_linux_ppc.cpp b/src/hotspot/os_cpu/linux_ppc/os_linux_ppc.cpp index 5909b5799b9..6854fb805a9 100644 --- a/src/hotspot/os_cpu/linux_ppc/os_linux_ppc.cpp +++ b/src/hotspot/os_cpu/linux_ppc/os_linux_ppc.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2025 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -28,6 +28,7 @@ #include "classfile/vmSymbols.hpp" #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -62,7 +63,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_ppc/prefetch_linux_ppc.inline.hpp b/src/hotspot/os_cpu/linux_ppc/prefetch_linux_ppc.inline.hpp index 12c65e6bf00..c33624a07de 100644 --- a/src/hotspot/os_cpu/linux_ppc/prefetch_linux_ppc.inline.hpp +++ b/src/hotspot/os_cpu/linux_ppc/prefetch_linux_ppc.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2012, 2013 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,8 +26,7 @@ #ifndef OS_CPU_LINUX_PPC_PREFETCH_LINUX_PPC_INLINE_HPP #define OS_CPU_LINUX_PPC_PREFETCH_LINUX_PPC_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read(const void *loc, intx interval) { __asm__ __volatile__ ( diff --git a/src/hotspot/os_cpu/linux_riscv/os_linux_riscv.cpp b/src/hotspot/os_cpu/linux_riscv/os_linux_riscv.cpp index 7a5929b0f41..d4a306d35b1 100644 --- a/src/hotspot/os_cpu/linux_riscv/os_linux_riscv.cpp +++ b/src/hotspot/os_cpu/linux_riscv/os_linux_riscv.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2022, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -28,6 +28,7 @@ #include "code/codeCache.hpp" #include "code/nativeInst.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -61,7 +62,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_riscv/prefetch_linux_riscv.inline.hpp b/src/hotspot/os_cpu/linux_riscv/prefetch_linux_riscv.inline.hpp index a2dd79544c1..c17054e8a0c 100644 --- a/src/hotspot/os_cpu/linux_riscv/prefetch_linux_riscv.inline.hpp +++ b/src/hotspot/os_cpu/linux_riscv/prefetch_linux_riscv.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2020, 2021, Huawei Technologies Co., Ltd. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,7 +26,7 @@ #ifndef OS_CPU_LINUX_RISCV_VM_PREFETCH_LINUX_RISCV_INLINE_HPP #define OS_CPU_LINUX_RISCV_VM_PREFETCH_LINUX_RISCV_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { if (interval >= 0 && UseZicbop) { diff --git a/src/hotspot/os_cpu/linux_s390/os_linux_s390.cpp b/src/hotspot/os_cpu/linux_s390/os_linux_s390.cpp index 4e074512e34..749f6bec032 100644 --- a/src/hotspot/os_cpu/linux_s390/os_linux_s390.cpp +++ b/src/hotspot/os_cpu/linux_s390/os_linux_s390.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2016, 2024 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -30,6 +30,7 @@ #include "code/nativeInst.hpp" #include "code/vtableStubs.hpp" #include "compiler/disassembler.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -62,7 +63,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_s390/prefetch_linux_s390.inline.hpp b/src/hotspot/os_cpu/linux_s390/prefetch_linux_s390.inline.hpp index ee55d01886f..56038714a9a 100644 --- a/src/hotspot/os_cpu/linux_s390/prefetch_linux_s390.inline.hpp +++ b/src/hotspot/os_cpu/linux_s390/prefetch_linux_s390.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2016 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,7 +26,7 @@ #ifndef OS_CPU_LINUX_S390_PREFETCH_LINUX_S390_INLINE_HPP #define OS_CPU_LINUX_S390_PREFETCH_LINUX_S390_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read(const void* loc, intx interval) { // No prefetch instructions on z/Architecture -> implement trivially. diff --git a/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp b/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp index a7f4e5ef688..07f53582a76 100644 --- a/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp +++ b/src/hotspot/os_cpu/linux_x86/os_linux_x86.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, 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 @@ -26,6 +26,7 @@ #include "classfile/vmSymbols.hpp" #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "logging/log.hpp" @@ -59,7 +60,6 @@ # include # include # include -# include # include # include # include diff --git a/src/hotspot/os_cpu/linux_x86/prefetch_linux_x86.inline.hpp b/src/hotspot/os_cpu/linux_x86/prefetch_linux_x86.inline.hpp index bb4302e1ddb..aadd21bca40 100644 --- a/src/hotspot/os_cpu/linux_x86/prefetch_linux_x86.inline.hpp +++ b/src/hotspot/os_cpu/linux_x86/prefetch_linux_x86.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, 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 @@ -25,8 +25,7 @@ #ifndef OS_CPU_LINUX_X86_PREFETCH_LINUX_X86_INLINE_HPP #define OS_CPU_LINUX_X86_PREFETCH_LINUX_X86_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { __asm__ ("prefetcht0 (%0,%1,1)" : : "r" (loc), "r" (interval)); diff --git a/src/hotspot/os_cpu/linux_zero/prefetch_linux_zero.inline.hpp b/src/hotspot/os_cpu/linux_zero/prefetch_linux_zero.inline.hpp index a510fa87834..3a34c311acd 100644 --- a/src/hotspot/os_cpu/linux_zero/prefetch_linux_zero.inline.hpp +++ b/src/hotspot/os_cpu/linux_zero/prefetch_linux_zero.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright 2007, 2008 Red Hat, Inc. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,7 +26,7 @@ #ifndef OS_CPU_LINUX_ZERO_PREFETCH_LINUX_ZERO_INLINE_HPP #define OS_CPU_LINUX_ZERO_PREFETCH_LINUX_ZERO_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read(const void* loc, intx interval) { } diff --git a/src/hotspot/os_cpu/windows_aarch64/os_windows_aarch64.cpp b/src/hotspot/os_cpu/windows_aarch64/os_windows_aarch64.cpp index d99e0167cbd..2c2afb168fd 100644 --- a/src/hotspot/os_cpu/windows_aarch64/os_windows_aarch64.cpp +++ b/src/hotspot/os_cpu/windows_aarch64/os_windows_aarch64.cpp @@ -1,6 +1,6 @@ /* * Copyright (c) 2020, Microsoft Corporation. All rights reserved. - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -28,6 +28,7 @@ #include "code/codeCache.hpp" #include "code/vtableStubs.hpp" #include "code/nativeInst.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/interpreter.hpp" #include "jvm.h" #include "memory/allocation.inline.hpp" @@ -54,7 +55,6 @@ # include # include # include -# include # include # include diff --git a/src/hotspot/os_cpu/windows_aarch64/prefetch_windows_aarch64.inline.hpp b/src/hotspot/os_cpu/windows_aarch64/prefetch_windows_aarch64.inline.hpp index df301ade92d..a360ee342be 100644 --- a/src/hotspot/os_cpu/windows_aarch64/prefetch_windows_aarch64.inline.hpp +++ b/src/hotspot/os_cpu/windows_aarch64/prefetch_windows_aarch64.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Microsoft Corporation. All rights reserved. + * Copyright (c) 2020, 2026, Microsoft Corporation. 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 @@ -25,8 +25,7 @@ #ifndef OS_CPU_WINDOWS_AARCH64_PREFETCH_WINDOWS_AARCH64_INLINE_HPP #define OS_CPU_WINDOWS_AARCH64_PREFETCH_WINDOWS_AARCH64_INLINE_HPP -#include "runtime/prefetch.hpp" - +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) { } diff --git a/src/hotspot/os_cpu/windows_x86/prefetch_windows_x86.inline.hpp b/src/hotspot/os_cpu/windows_x86/prefetch_windows_x86.inline.hpp index 996625cb0ad..645fbe99a22 100644 --- a/src/hotspot/os_cpu/windows_x86/prefetch_windows_x86.inline.hpp +++ b/src/hotspot/os_cpu/windows_x86/prefetch_windows_x86.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2026, 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 @@ -25,7 +25,7 @@ #ifndef OS_CPU_WINDOWS_X86_PREFETCH_WINDOWS_X86_INLINE_HPP #define OS_CPU_WINDOWS_X86_PREFETCH_WINDOWS_X86_INLINE_HPP -#include "runtime/prefetch.hpp" +// Included in runtime/prefetch.inline.hpp inline void Prefetch::read (const void *loc, intx interval) {} inline void Prefetch::write(void *loc, intx interval) {} diff --git a/src/hotspot/share/cds/aotConstantPoolResolver.cpp b/src/hotspot/share/cds/aotConstantPoolResolver.cpp index c4bb26f6fb1..93145940955 100644 --- a/src/hotspot/share/cds/aotConstantPoolResolver.cpp +++ b/src/hotspot/share/cds/aotConstantPoolResolver.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -116,6 +116,10 @@ bool AOTConstantPoolResolver::is_class_resolution_deterministic(InstanceKlass* c return false; } } else if (resolved_class->is_objArray_klass()) { + if (CDSConfig::is_dumping_dynamic_archive()) { + // This is difficult to handle. See JDK-8374639 + return false; + } Klass* elem = ObjArrayKlass::cast(resolved_class)->bottom_klass(); if (elem->is_instance_klass()) { return is_class_resolution_deterministic(cp_holder, InstanceKlass::cast(elem)); diff --git a/src/hotspot/share/cds/aotMappedHeapWriter.cpp b/src/hotspot/share/cds/aotMappedHeapWriter.cpp index 9c67b2e0e8d..e73b980614a 100644 --- a/src/hotspot/share/cds/aotMappedHeapWriter.cpp +++ b/src/hotspot/share/cds/aotMappedHeapWriter.cpp @@ -696,7 +696,7 @@ template void AOTMappedHeapWriter::relocate_field_in_buffer(T* fiel // We use zero-based, 0-shift encoding, so the narrowOop is just the lower // 32 bits of request_referent intptr_t addr = cast_from_oop(request_referent); - *((narrowOop*)field_addr_in_buffer) = checked_cast(addr); + *((narrowOop*)field_addr_in_buffer) = CompressedOops::narrow_oop_cast(addr); } else { store_requested_oop_in_buffer(field_addr_in_buffer, request_referent); } diff --git a/src/hotspot/share/cds/aotMetaspace.cpp b/src/hotspot/share/cds/aotMetaspace.cpp index 3824a2be3e2..79d789e0c70 100644 --- a/src/hotspot/share/cds/aotMetaspace.cpp +++ b/src/hotspot/share/cds/aotMetaspace.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -2159,7 +2159,6 @@ void AOTMetaspace::initialize_shared_spaces() { intptr_t* buffer = (intptr_t*)dynamic_mapinfo->serialized_data(); ReadClosure rc(&buffer, (intptr_t)SharedBaseAddress); DynamicArchive::serialize(&rc); - DynamicArchive::setup_array_klasses(); } LogStreamHandle(Info, aot) lsh; diff --git a/src/hotspot/share/cds/archiveBuilder.cpp b/src/hotspot/share/cds/archiveBuilder.cpp index 3c7f25a950b..6bbefea5cd9 100644 --- a/src/hotspot/share/cds/archiveBuilder.cpp +++ b/src/hotspot/share/cds/archiveBuilder.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, 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 @@ -984,8 +984,6 @@ void ArchiveBuilder::make_klasses_shareable() { #undef STATS_FORMAT #undef STATS_PARAMS - - DynamicArchive::make_array_klasses_shareable(); } void ArchiveBuilder::make_training_data_shareable() { diff --git a/src/hotspot/share/cds/dynamicArchive.cpp b/src/hotspot/share/cds/dynamicArchive.cpp index 8fae8dabf8c..d39cf3775e4 100644 --- a/src/hotspot/share/cds/dynamicArchive.cpp +++ b/src/hotspot/share/cds/dynamicArchive.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -48,6 +48,7 @@ #include "logging/log.hpp" #include "memory/metaspaceClosure.hpp" #include "memory/resourceArea.hpp" +#include "oops/array.hpp" #include "oops/klass.inline.hpp" #include "runtime/arguments.hpp" #include "runtime/os.hpp" @@ -95,7 +96,6 @@ public: void sort_methods(InstanceKlass* ik) const; void remark_pointers_for_instance_klass(InstanceKlass* k, bool should_mark) const; void write_archive(char* serialized_data, AOTClassLocationConfig* cl_config); - void gather_array_klasses(); public: // Do this before and after the archive dump to see if any corruption @@ -132,7 +132,6 @@ public: init_header(); gather_source_objs(); - gather_array_klasses(); reserve_buffer(); log_info(cds, dynamic)("Copying %d klasses and %d symbols", @@ -159,7 +158,6 @@ public: ArchiveBuilder::OtherROAllocMark mark; SystemDictionaryShared::write_to_archive(false); cl_config = AOTClassLocationConfig::dumptime()->write_to_archive(); - DynamicArchive::dump_array_klasses(); serialized_data = ro_region()->top(); WriteClosure wc(ro_region()); @@ -175,8 +173,6 @@ public: write_archive(serialized_data, cl_config); release_header(); - DynamicArchive::post_dump(); - post_dump(); verify_universe("After CDS dynamic dump"); @@ -185,30 +181,6 @@ public: virtual void iterate_roots(MetaspaceClosure* it) { AOTArtifactFinder::all_cached_classes_do(it); SystemDictionaryShared::dumptime_classes_do(it); - iterate_primitive_array_klasses(it); - } - - void iterate_primitive_array_klasses(MetaspaceClosure* it) { - for (int i = T_BOOLEAN; i <= T_LONG; i++) { - assert(is_java_primitive((BasicType)i), "sanity"); - Klass* k = Universe::typeArrayKlass((BasicType)i); // this give you "[I", etc - assert(AOTMetaspace::in_aot_cache_static_region((void*)k), - "one-dimensional primitive array should be in static archive"); - ArrayKlass* ak = ArrayKlass::cast(k); - while (ak != nullptr && ak->in_aot_cache()) { - Klass* next_k = ak->array_klass_or_null(); - if (next_k != nullptr) { - ak = ArrayKlass::cast(next_k); - } else { - ak = nullptr; - } - } - if (ak != nullptr) { - assert(ak->dimension() > 1, "sanity"); - // this is the lowest dimension that's not in the static archive - it->push(&ak); - } - } } }; @@ -367,26 +339,6 @@ void DynamicArchiveBuilder::write_archive(char* serialized_data, AOTClassLocatio log_info(cds, dynamic)("%d klasses; %d symbols", klasses()->length(), symbols()->length()); } -void DynamicArchiveBuilder::gather_array_klasses() { - for (int i = 0; i < klasses()->length(); i++) { - if (klasses()->at(i)->is_objArray_klass()) { - ObjArrayKlass* oak = ObjArrayKlass::cast(klasses()->at(i)); - Klass* elem = oak->element_klass(); - if (AOTMetaspace::in_aot_cache_static_region(elem)) { - // Only capture the array klass whose element_klass is in the static archive. - // During run time, setup (see DynamicArchive::setup_array_klasses()) is needed - // so that the element_klass can find its array klasses from the dynamic archive. - DynamicArchive::append_array_klass(oak); - } else { - // The element_klass and its array klasses are in the same archive. - assert(!AOTMetaspace::in_aot_cache_static_region(oak), - "we should not gather klasses that are already in the static archive"); - } - } - } - log_debug(aot)("Total array klasses gathered for dynamic archive: %d", DynamicArchive::num_array_klasses()); -} - class VM_PopulateDynamicDumpSharedSpace: public VM_Heap_Sync_Operation { DynamicArchiveBuilder _builder; public: @@ -403,76 +355,9 @@ public: } }; -// _array_klasses and _dynamic_archive_array_klasses only hold the array klasses -// which have element klass in the static archive. -GrowableArray* DynamicArchive::_array_klasses = nullptr; -Array* DynamicArchive::_dynamic_archive_array_klasses = nullptr; - void DynamicArchive::serialize(SerializeClosure* soc) { SymbolTable::serialize_shared_table_header(soc, false); SystemDictionaryShared::serialize_dictionary_headers(soc, false); - soc->do_ptr(&_dynamic_archive_array_klasses); -} - -void DynamicArchive::append_array_klass(ObjArrayKlass* ak) { - if (_array_klasses == nullptr) { - _array_klasses = new (mtClassShared) GrowableArray(50, mtClassShared); - } - _array_klasses->append(ak); -} - -void DynamicArchive::dump_array_klasses() { - assert(CDSConfig::is_dumping_dynamic_archive(), "sanity"); - if (_array_klasses != nullptr) { - ArchiveBuilder* builder = ArchiveBuilder::current(); - int num_array_klasses = _array_klasses->length(); - _dynamic_archive_array_klasses = - ArchiveBuilder::new_ro_array(num_array_klasses); - for (int i = 0; i < num_array_klasses; i++) { - builder->write_pointer_in_buffer(_dynamic_archive_array_klasses->adr_at(i), _array_klasses->at(i)); - } - } -} - -void DynamicArchive::setup_array_klasses() { - if (_dynamic_archive_array_klasses != nullptr) { - for (int i = 0; i < _dynamic_archive_array_klasses->length(); i++) { - ObjArrayKlass* oak = _dynamic_archive_array_klasses->at(i); - Klass* elm = oak->element_klass(); - assert(AOTMetaspace::in_aot_cache_static_region((void*)elm), "must be"); - - if (elm->is_instance_klass()) { - assert(InstanceKlass::cast(elm)->array_klasses() == nullptr, "must be"); - InstanceKlass::cast(elm)->set_array_klasses(oak); - } else { - assert(elm->is_array_klass(), "sanity"); - assert(ArrayKlass::cast(elm)->higher_dimension() == nullptr, "must be"); - ArrayKlass::cast(elm)->set_higher_dimension(oak); - } - } - log_debug(aot)("Total array klasses read from dynamic archive: %d", _dynamic_archive_array_klasses->length()); - } -} - -void DynamicArchive::make_array_klasses_shareable() { - if (_array_klasses != nullptr) { - int num_array_klasses = _array_klasses->length(); - for (int i = 0; i < num_array_klasses; i++) { - ObjArrayKlass* k = ArchiveBuilder::current()->get_buffered_addr(_array_klasses->at(i)); - k->remove_unshareable_info(); - } - } -} - -void DynamicArchive::post_dump() { - if (_array_klasses != nullptr) { - delete _array_klasses; - _array_klasses = nullptr; - } -} - -int DynamicArchive::num_array_klasses() { - return _array_klasses != nullptr ? _array_klasses->length() : 0; } void DynamicArchive::dump_impl(bool jcmd_request, const char* archive_name, TRAPS) { diff --git a/src/hotspot/share/cds/dynamicArchive.hpp b/src/hotspot/share/cds/dynamicArchive.hpp index c42c4b7dfde..63a48e252ee 100644 --- a/src/hotspot/share/cds/dynamicArchive.hpp +++ b/src/hotspot/share/cds/dynamicArchive.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -26,13 +26,8 @@ #define SHARE_CDS_DYNAMICARCHIVE_HPP #include "cds/filemap.hpp" -#include "classfile/compactHashtable.hpp" #include "memory/allStatic.hpp" -#include "memory/memRegion.hpp" -#include "oops/array.hpp" -#include "oops/oop.hpp" #include "utilities/exceptions.hpp" -#include "utilities/growableArray.hpp" #include "utilities/macros.hpp" #if INCLUDE_CDS @@ -59,22 +54,13 @@ public: }; class DynamicArchive : AllStatic { -private: - static GrowableArray* _array_klasses; - static Array* _dynamic_archive_array_klasses; public: static void dump_for_jcmd(const char* archive_name, TRAPS); static void dump_at_exit(JavaThread* current); static void dump_impl(bool jcmd_request, const char* archive_name, TRAPS); static bool is_mapped() { return FileMapInfo::dynamic_info() != nullptr; } static bool validate(FileMapInfo* dynamic_info); - static void dump_array_klasses(); - static void setup_array_klasses(); - static void append_array_klass(ObjArrayKlass* oak); static void serialize(SerializeClosure* soc); - static void make_array_klasses_shareable(); - static void post_dump(); - static int num_array_klasses(); }; #endif // INCLUDE_CDS #endif // SHARE_CDS_DYNAMICARCHIVE_HPP diff --git a/src/hotspot/share/classfile/classLoader.cpp b/src/hotspot/share/classfile/classLoader.cpp index 12fbda899b9..d9a63cd154b 100644 --- a/src/hotspot/share/classfile/classLoader.cpp +++ b/src/hotspot/share/classfile/classLoader.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -42,6 +42,7 @@ #include "classfile/vmClasses.hpp" #include "classfile/vmSymbols.hpp" #include "compiler/compileBroker.hpp" +#include "cppstdlib/cstdlib.hpp" #include "interpreter/bytecodeStream.hpp" #include "interpreter/oopMapCache.hpp" #include "jimage.hpp" @@ -81,7 +82,6 @@ #include "utilities/utf8.hpp" #include -#include // Entry point in java.dll for path canonicalization diff --git a/src/hotspot/share/classfile/javaClasses.cpp b/src/hotspot/share/classfile/javaClasses.cpp index 77a94c8afa5..dd70d7b49ab 100644 --- a/src/hotspot/share/classfile/javaClasses.cpp +++ b/src/hotspot/share/classfile/javaClasses.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -4297,10 +4297,6 @@ int jdk_internal_foreign_abi_NativeEntryPoint::_downcall_stub_address_offset; macro(_method_type_offset, k, "methodType", java_lang_invoke_MethodType_signature, false); \ macro(_downcall_stub_address_offset, k, "downcallStubAddress", long_signature, false); -bool jdk_internal_foreign_abi_NativeEntryPoint::is_instance(oop obj) { - return obj != nullptr && is_subclass(obj->klass()); -} - void jdk_internal_foreign_abi_NativeEntryPoint::compute_offsets() { InstanceKlass* k = vmClasses::NativeEntryPoint_klass(); NEP_FIELDS_DO(FIELD_COMPUTE_OFFSET); @@ -4337,10 +4333,6 @@ int jdk_internal_foreign_abi_ABIDescriptor::_scratch2_offset; macro(_scratch1_offset, k, "scratch1", jdk_internal_foreign_abi_VMStorage_signature, false); \ macro(_scratch2_offset, k, "scratch2", jdk_internal_foreign_abi_VMStorage_signature, false); -bool jdk_internal_foreign_abi_ABIDescriptor::is_instance(oop obj) { - return obj != nullptr && is_subclass(obj->klass()); -} - void jdk_internal_foreign_abi_ABIDescriptor::compute_offsets() { InstanceKlass* k = vmClasses::ABIDescriptor_klass(); ABIDescriptor_FIELDS_DO(FIELD_COMPUTE_OFFSET); diff --git a/src/hotspot/share/classfile/javaClasses.hpp b/src/hotspot/share/classfile/javaClasses.hpp index 33dc912404c..a8562a345c8 100644 --- a/src/hotspot/share/classfile/javaClasses.hpp +++ b/src/hotspot/share/classfile/javaClasses.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -1179,13 +1179,6 @@ class jdk_internal_foreign_abi_NativeEntryPoint: AllStatic { static oop method_type(oop entry); static jlong downcall_stub_address(oop entry); - // Testers - static bool is_subclass(Klass* klass) { - return vmClasses::NativeEntryPoint_klass() != nullptr && - klass->is_subclass_of(vmClasses::NativeEntryPoint_klass()); - } - static bool is_instance(oop obj); - // Accessors for code generation: static int method_type_offset_in_bytes() { return _method_type_offset; } static int downcall_stub_address_offset_in_bytes() { return _downcall_stub_address_offset; } @@ -1216,13 +1209,6 @@ class jdk_internal_foreign_abi_ABIDescriptor: AllStatic { static jint shadowSpace(oop entry); static oop scratch1(oop entry); static oop scratch2(oop entry); - - // Testers - static bool is_subclass(Klass* klass) { - return vmClasses::ABIDescriptor_klass() != nullptr && - klass->is_subclass_of(vmClasses::ABIDescriptor_klass()); - } - static bool is_instance(oop obj); }; class jdk_internal_foreign_abi_VMStorage: AllStatic { diff --git a/src/hotspot/share/cppstdlib/cstdlib.hpp b/src/hotspot/share/cppstdlib/cstdlib.hpp new file mode 100644 index 00000000000..2b58e737882 --- /dev/null +++ b/src/hotspot/share/cppstdlib/cstdlib.hpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025, 2026, 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. + * + */ + +#ifndef SHARE_CPPSTDLIB_CSTDLIB_HPP +#define SHARE_CPPSTDLIB_CSTDLIB_HPP + +#include "utilities/compilerWarnings.hpp" + +// HotSpot usage for : +// +// Some functions are explicitly forbidden below. That may not be a complete +// list of all the functions we should forbid. +// +// We assume provides definitions in the global namespace, in +// addition to providing them in the std namespace. We prefer to use the names +// in the global namespace. + +BEGIN_ALLOW_FORBIDDEN_FUNCTIONS +#include "utilities/vmassert_uninstall.hpp" + +#include + +#include "utilities/vmassert_reinstall.hpp" // don't reorder +END_ALLOW_FORBIDDEN_FUNCTIONS + +// AIX may define malloc and calloc as macros when certain other features are +// present, causing us all sorts of grief. +// https://www.ibm.com/docs/en/openxl-c-and-cpp-aix/17.1.4?topic=compilers-memory-allocation +// Replace the macro definitions with something we can work with. +// AIX 7.3 no longer uses macro renaming when building with clang, instead +// using the same asm replacement approach as used below. This workaround can +// be removed once earlier versions are no longer supported as build platforms. +#if defined(AIX) && (defined(__VEC__) || defined(__AIXVEC)) +#if defined(malloc) || defined(calloc) +#if !defined(malloc) || !defined(calloc) +#error "Inconsistent alloc macro mappings, expected both to be mapped." +#endif +// Remove the macros. +#undef malloc +#undef calloc +// Implement the mapping using gcc/clang asm name mapping. +extern "C" { +extern void* malloc(size_t) noexcept asm("vec_malloc"); +extern void* calloc(size_t, size_t) noexcept asm("vec_calloc"); +} // extern "C" +// Because the macros are in place when brings names into the std +// namespace, macro replacement causes the expanded names to be added instead +// of the intended names. We can't remove std::vec_malloc and std::vec_calloc, +// but we do add the standard names in case someone uses them. +namespace std { +using ::malloc; +using ::calloc; +} // namespace std +#endif // Macro definition for malloc or calloc +#endif // AIX altivec allocator support + +// Prefer os:: variants of these. +FORBID_IMPORTED_NORETURN_C_FUNCTION(void exit(int), noexcept, "use os::exit") +FORBID_IMPORTED_NORETURN_C_FUNCTION(void _Exit(int), noexcept, "use os::exit") + +// Windows puts _exit in . POSIX puts it in . +// We can't forbid it here when using clang if it's not in - see +// the clang definition for FORBIDDEN_FUNCTION_NORETURN_ATTRIBUTE. +#ifdef _WINDOWS +FORBID_IMPORTED_NORETURN_C_FUNCTION(void _exit(int), /* not noexcept */, "use os::exit") +#endif // _WINDOWS + +// These functions return raw C-heap pointers or, in case of free(), take raw +// C-heap pointers. We generally want allocation to be done through NMT, using +// os::malloc and friends. +FORBID_IMPORTED_C_FUNCTION(void* malloc(size_t), noexcept, "use os::malloc"); +FORBID_IMPORTED_C_FUNCTION(void free(void*), noexcept, "use os::free"); +FORBID_IMPORTED_C_FUNCTION(void* calloc(size_t, size_t), noexcept, "use os::malloc and zero out manually"); +FORBID_IMPORTED_C_FUNCTION(void* realloc(void*, size_t), noexcept, "use os::realloc"); + +// These are not provided (and are unimplementable?) by Windows. +// https://stackoverflow.com/questions/62962839/stdaligned-alloc-missing-from-visual-studio-2019 +// They also aren't useful for a POSIX implementation of NMT. +#ifndef _WINDOWS +FORBID_C_FUNCTION(void* aligned_alloc(size_t, size_t), noexcept, "don't use"); +FORBID_C_FUNCTION(int posix_memalign(void**, size_t, size_t), noexcept, "don't use"); +#endif // !_WINDOWS + +#endif // SHARE_CPPSTDLIB_CSTDLIB_HPP diff --git a/src/hotspot/share/gc/epsilon/epsilonHeap.cpp b/src/hotspot/share/gc/epsilon/epsilonHeap.cpp index e5ae673ef0c..24182c22a23 100644 --- a/src/hotspot/share/gc/epsilon/epsilonHeap.cpp +++ b/src/hotspot/share/gc/epsilon/epsilonHeap.cpp @@ -77,6 +77,7 @@ jint EpsilonHeap::initialize() { void EpsilonHeap::initialize_serviceability() { _pool = new EpsilonMemoryPool(this); _memory_manager.add_pool(_pool); + _monitoring_support->mark_ready(); } GrowableArray EpsilonHeap::memory_managers() { @@ -101,7 +102,7 @@ EpsilonHeap* EpsilonHeap::heap() { return named_heap(CollectedHeap::Epsilon); } -HeapWord* EpsilonHeap::allocate_work(size_t size, bool verbose) { +HeapWord* EpsilonHeap::allocate_work(size_t size) { assert(is_object_aligned(size), "Allocation size should be aligned: %zu", size); HeapWord* res = nullptr; @@ -151,19 +152,23 @@ HeapWord* EpsilonHeap::allocate_work(size_t size, bool verbose) { size_t used = _space->used(); - // Allocation successful, update counters - if (verbose) { - size_t last = _last_counter_update; - if ((used - last >= _step_counter_update) && AtomicAccess::cmpxchg(&_last_counter_update, last, used) == last) { + // Allocation successful, update counters and print status. + // At this point, some diagnostic subsystems might not yet be initialized. + // We pretend the printout happened either way. This keeps allocation path + // from obsessively checking the subsystems' status on every allocation. + size_t last_counter = AtomicAccess::load(&_last_counter_update); + if ((used - last_counter >= _step_counter_update) && + AtomicAccess::cmpxchg(&_last_counter_update, last_counter, used) == last_counter) { + if (_monitoring_support->is_ready()) { _monitoring_support->update_counters(); } } - // ...and print the occupancy line, if needed - if (verbose) { - size_t last = _last_heap_print; - if ((used - last >= _step_heap_print) && AtomicAccess::cmpxchg(&_last_heap_print, last, used) == last) { - print_heap_info(used); + size_t last_heap = AtomicAccess::load(&_last_heap_print); + if ((used - last_heap >= _step_heap_print) && + AtomicAccess::cmpxchg(&_last_heap_print, last_heap, used) == last_heap) { + print_heap_info(used); + if (Metaspace::initialized()) { print_metaspace_info(); } } @@ -265,8 +270,7 @@ HeapWord* EpsilonHeap::mem_allocate(size_t size) { } HeapWord* EpsilonHeap::allocate_loaded_archive_space(size_t size) { - // Cannot use verbose=true because Metaspace is not initialized - return allocate_work(size, /* verbose = */false); + return allocate_work(size); } void EpsilonHeap::collect(GCCause::Cause cause) { diff --git a/src/hotspot/share/gc/epsilon/epsilonHeap.hpp b/src/hotspot/share/gc/epsilon/epsilonHeap.hpp index 4f812bde8b3..9693c63b15c 100644 --- a/src/hotspot/share/gc/epsilon/epsilonHeap.hpp +++ b/src/hotspot/share/gc/epsilon/epsilonHeap.hpp @@ -83,7 +83,7 @@ public: bool requires_barriers(stackChunkOop obj) const override { return false; } // Allocation - HeapWord* allocate_work(size_t size, bool verbose = true); + HeapWord* allocate_work(size_t size); HeapWord* mem_allocate(size_t size) override; HeapWord* allocate_new_tlab(size_t min_size, size_t requested_size, diff --git a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp index 51d0a8356d2..38be736df74 100644 --- a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp +++ b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.cpp @@ -96,9 +96,11 @@ public: EpsilonMonitoringSupport::EpsilonMonitoringSupport(EpsilonHeap* heap) { _heap_counters = new EpsilonGenerationCounters(heap); _space_counters = new EpsilonSpaceCounters("Heap", 0, heap->max_capacity(), 0, _heap_counters); + _ready = false; } void EpsilonMonitoringSupport::update_counters() { + assert(is_ready(), "Must be ready"); MemoryService::track_memory_usage(); if (UsePerfData) { @@ -110,3 +112,11 @@ void EpsilonMonitoringSupport::update_counters() { MetaspaceCounters::update_performance_counters(); } } + +bool EpsilonMonitoringSupport::is_ready() { + return AtomicAccess::load_acquire(&_ready); +} + +void EpsilonMonitoringSupport::mark_ready() { + return AtomicAccess::release_store(&_ready, true); +} diff --git a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.hpp b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.hpp index 67a60d92778..76cdac6df1b 100644 --- a/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.hpp +++ b/src/hotspot/share/gc/epsilon/epsilonMonitoringSupport.hpp @@ -35,9 +35,12 @@ class EpsilonMonitoringSupport : public CHeapObj { private: EpsilonGenerationCounters* _heap_counters; EpsilonSpaceCounters* _space_counters; + volatile bool _ready; public: EpsilonMonitoringSupport(EpsilonHeap* heap); + bool is_ready(); + void mark_ready(); void update_counters(); }; diff --git a/src/hotspot/share/gc/g1/g1Arguments.cpp b/src/hotspot/share/gc/g1/g1Arguments.cpp index 2be0e008c22..58e76cdd43a 100644 --- a/src/hotspot/share/gc/g1/g1Arguments.cpp +++ b/src/hotspot/share/gc/g1/g1Arguments.cpp @@ -36,6 +36,7 @@ #include "gc/shared/fullGCForwarding.hpp" #include "gc/shared/gcArguments.hpp" #include "gc/shared/workerPolicy.hpp" +#include "runtime/flags/jvmFlagLimit.hpp" #include "runtime/globals.hpp" #include "runtime/globals_extension.hpp" #include "runtime/java.hpp" @@ -190,7 +191,8 @@ void G1Arguments::initialize() { } FLAG_SET_DEFAULT(G1ConcRefinementThreads, 0); } else if (FLAG_IS_DEFAULT(G1ConcRefinementThreads)) { - FLAG_SET_ERGO(G1ConcRefinementThreads, ParallelGCThreads); + const JVMTypedFlagLimit* conc_refinement_threads_limits = JVMFlagLimit::get_range_at(FLAG_MEMBER_ENUM(G1ConcRefinementThreads))->cast(); + FLAG_SET_ERGO(G1ConcRefinementThreads, MIN2(ParallelGCThreads, conc_refinement_threads_limits->max())); } if (FLAG_IS_DEFAULT(ConcGCThreads) || ConcGCThreads == 0) { diff --git a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp index d875d9c8b8a..ec5d2393d8c 100644 --- a/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp +++ b/src/hotspot/share/gc/g1/g1YoungGCPostEvacuateTasks.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, 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 @@ -46,7 +46,7 @@ #include "oops/access.inline.hpp" #include "oops/compressedOops.inline.hpp" #include "oops/oop.inline.hpp" -#include "runtime/prefetch.hpp" +#include "runtime/prefetch.inline.hpp" #include "runtime/threads.hpp" #include "runtime/threadSMR.hpp" #include "utilities/bitMap.inline.hpp" diff --git a/src/hotspot/share/gc/serial/cardTableRS.cpp b/src/hotspot/share/gc/serial/cardTableRS.cpp index 80985424a62..a53ab066387 100644 --- a/src/hotspot/share/gc/serial/cardTableRS.cpp +++ b/src/hotspot/share/gc/serial/cardTableRS.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, 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 @@ -28,6 +28,7 @@ #include "gc/serial/serialHeap.inline.hpp" #include "gc/shared/space.hpp" #include "memory/iterator.inline.hpp" +#include "runtime/prefetch.inline.hpp" #include "utilities/align.hpp" void CardTableRS::scan_old_to_young_refs(TenuredGeneration* tg, HeapWord* saved_top) { diff --git a/src/hotspot/share/gc/serial/generation.hpp b/src/hotspot/share/gc/serial/generation.hpp index 8c9da3b42b7..ddfd4028a7d 100644 --- a/src/hotspot/share/gc/serial/generation.hpp +++ b/src/hotspot/share/gc/serial/generation.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -34,7 +34,6 @@ #include "memory/virtualspace.hpp" #include "runtime/mutex.hpp" #include "runtime/perfData.hpp" -#include "runtime/prefetch.inline.hpp" // A Generation models a heap area for similarly-aged objects. // It will contain one ore more spaces holding the actual objects. diff --git a/src/hotspot/share/gc/serial/serialHeap.cpp b/src/hotspot/share/gc/serial/serialHeap.cpp index 08d730bf877..8eafdfdcc82 100644 --- a/src/hotspot/share/gc/serial/serialHeap.cpp +++ b/src/hotspot/share/gc/serial/serialHeap.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, 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 @@ -70,6 +70,7 @@ #include "runtime/init.hpp" #include "runtime/java.hpp" #include "runtime/mutexLocker.hpp" +#include "runtime/prefetch.inline.hpp" #include "runtime/threads.hpp" #include "runtime/vmThread.hpp" #include "services/memoryManager.hpp" diff --git a/src/hotspot/share/gc/shared/barrierSetNMethod.cpp b/src/hotspot/share/gc/shared/barrierSetNMethod.cpp index cb5d6b5a886..ab94bae079a 100644 --- a/src/hotspot/share/gc/shared/barrierSetNMethod.cpp +++ b/src/hotspot/share/gc/shared/barrierSetNMethod.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, 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 @@ -33,6 +33,7 @@ #include "memory/universe.hpp" #include "oops/access.inline.hpp" #include "oops/method.inline.hpp" +#include "runtime/atomic.hpp" #include "runtime/frame.inline.hpp" #include "runtime/javaThread.hpp" #include "runtime/threads.hpp" @@ -196,8 +197,8 @@ int BarrierSetNMethod::nmethod_stub_entry_barrier(address* return_address_ptr) { // Diagnostic option to force deoptimization 1 in 10 times. It is otherwise // a very rare event. if (DeoptimizeNMethodBarriersALot && !nm->is_osr_method()) { - static volatile uint32_t counter=0; - if (AtomicAccess::add(&counter, 1u) % 10 == 0) { + static Atomic counter{0}; + if (counter.add_then_fetch(1u) % 10 == 0) { may_enter = false; } } diff --git a/src/hotspot/share/gc/shared/concurrentGCThread.cpp b/src/hotspot/share/gc/shared/concurrentGCThread.cpp index ac281f82f8a..ed6c1b4d283 100644 --- a/src/hotspot/share/gc/shared/concurrentGCThread.cpp +++ b/src/hotspot/share/gc/shared/concurrentGCThread.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, 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 @@ -23,7 +23,7 @@ */ #include "gc/shared/concurrentGCThread.hpp" -#include "runtime/atomicAccess.hpp" +#include "runtime/atomic.hpp" #include "runtime/init.hpp" #include "runtime/jniHandles.hpp" #include "runtime/mutexLocker.hpp" @@ -48,7 +48,7 @@ void ConcurrentGCThread::run() { // Signal thread has terminated MonitorLocker ml(Terminator_lock); - AtomicAccess::release_store(&_has_terminated, true); + _has_terminated.release_store(true); ml.notify_all(); } @@ -57,21 +57,21 @@ void ConcurrentGCThread::stop() { assert(!has_terminated(), "Invalid state"); // Signal thread to terminate - AtomicAccess::release_store_fence(&_should_terminate, true); + _should_terminate.release_store_fence(true); stop_service(); // Wait for thread to terminate MonitorLocker ml(Terminator_lock); - while (!_has_terminated) { + while (!_has_terminated.load_relaxed()) { ml.wait(); } } bool ConcurrentGCThread::should_terminate() const { - return AtomicAccess::load_acquire(&_should_terminate); + return _should_terminate.load_acquire(); } bool ConcurrentGCThread::has_terminated() const { - return AtomicAccess::load_acquire(&_has_terminated); + return _has_terminated.load_acquire(); } diff --git a/src/hotspot/share/gc/shared/concurrentGCThread.hpp b/src/hotspot/share/gc/shared/concurrentGCThread.hpp index 630abeaeb9f..0c764546045 100644 --- a/src/hotspot/share/gc/shared/concurrentGCThread.hpp +++ b/src/hotspot/share/gc/shared/concurrentGCThread.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, 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 @@ -25,14 +25,15 @@ #ifndef SHARE_GC_SHARED_CONCURRENTGCTHREAD_HPP #define SHARE_GC_SHARED_CONCURRENTGCTHREAD_HPP +#include "runtime/atomic.hpp" #include "runtime/javaThread.hpp" #include "runtime/nonJavaThread.hpp" #include "utilities/debug.hpp" class ConcurrentGCThread: public NamedThread { private: - volatile bool _should_terminate; - volatile bool _has_terminated; + Atomic _should_terminate; + Atomic _has_terminated; protected: void create_and_start(ThreadPriority prio = NearMaxPriority); diff --git a/src/hotspot/share/gc/shared/gcLocker.cpp b/src/hotspot/share/gc/shared/gcLocker.cpp index 01d17b1117d..898588e6b06 100644 --- a/src/hotspot/share/gc/shared/gcLocker.cpp +++ b/src/hotspot/share/gc/shared/gcLocker.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -28,7 +28,7 @@ #include "logging/log.hpp" #include "memory/resourceArea.hpp" #include "memory/universe.hpp" -#include "runtime/atomicAccess.hpp" +#include "runtime/atomic.hpp" #include "runtime/interfaceSupport.inline.hpp" #include "runtime/javaThread.inline.hpp" #include "runtime/safepoint.hpp" @@ -60,16 +60,13 @@ public: }; Monitor* GCLocker::_lock; -volatile bool GCLocker::_is_gc_request_pending; +Atomic GCLocker::_is_gc_request_pending{false}; -DEBUG_ONLY(uint64_t GCLocker::_verify_in_cr_count;) +DEBUG_ONLY(Atomic GCLocker::_verify_in_cr_count{0};) void GCLocker::initialize() { assert(JNICritical_lock != nullptr, "inv"); _lock = JNICritical_lock; - _is_gc_request_pending = false; - - DEBUG_ONLY(_verify_in_cr_count = 0;) } bool GCLocker::is_active() { @@ -84,11 +81,11 @@ bool GCLocker::is_active() { void GCLocker::block() { // _lock is held from the beginning of block() to the end of of unblock(). _lock->lock(); - assert(AtomicAccess::load(&_is_gc_request_pending) == false, "precondition"); + assert(_is_gc_request_pending.load_relaxed() == false, "precondition"); GCLockerTimingDebugLogger logger("Thread blocked to start GC."); - AtomicAccess::store(&_is_gc_request_pending, true); + _is_gc_request_pending.store_relaxed(true); // The _is_gc_request_pending and _jni_active_critical (inside // in_critical_atomic()) variables form a Dekker duality. On the GC side, the @@ -112,14 +109,14 @@ void GCLocker::block() { #ifdef ASSERT // Matching the storestore in GCLocker::exit. OrderAccess::loadload(); - assert(AtomicAccess::load(&_verify_in_cr_count) == 0, "inv"); + assert(_verify_in_cr_count.load_relaxed() == 0, "inv"); #endif } void GCLocker::unblock() { - assert(AtomicAccess::load(&_is_gc_request_pending) == true, "precondition"); + assert(_is_gc_request_pending.load_relaxed() == true, "precondition"); - AtomicAccess::store(&_is_gc_request_pending, false); + _is_gc_request_pending.store_relaxed(false); _lock->unlock(); } @@ -139,7 +136,7 @@ void GCLocker::enter_slow(JavaThread* current_thread) { // Same as fast path. OrderAccess::fence(); - if (!AtomicAccess::load(&_is_gc_request_pending)) { + if (!_is_gc_request_pending.load_relaxed()) { return; } diff --git a/src/hotspot/share/gc/shared/gcLocker.hpp b/src/hotspot/share/gc/shared/gcLocker.hpp index c0e28b66539..be3c0c3593e 100644 --- a/src/hotspot/share/gc/shared/gcLocker.hpp +++ b/src/hotspot/share/gc/shared/gcLocker.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -27,6 +27,7 @@ #include "gc/shared/gcCause.hpp" #include "memory/allStatic.hpp" +#include "runtime/atomic.hpp" #include "runtime/mutex.hpp" // GCLocker provides synchronization between the garbage collector (GC) and @@ -43,11 +44,11 @@ class GCLocker: public AllStatic { static Monitor* _lock; - static volatile bool _is_gc_request_pending; + static Atomic _is_gc_request_pending; #ifdef ASSERT // Debug-only: to track the number of java threads in critical-region. - static uint64_t _verify_in_cr_count; + static Atomic _verify_in_cr_count; #endif static void enter_slow(JavaThread* current_thread); diff --git a/src/hotspot/share/gc/shared/gcLocker.inline.hpp b/src/hotspot/share/gc/shared/gcLocker.inline.hpp index 050b9570280..135bd9eab62 100644 --- a/src/hotspot/share/gc/shared/gcLocker.inline.hpp +++ b/src/hotspot/share/gc/shared/gcLocker.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2026, 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 @@ -38,13 +38,13 @@ void GCLocker::enter(JavaThread* current_thread) { // Matching the fence in GCLocker::block. OrderAccess::fence(); - if (AtomicAccess::load(&_is_gc_request_pending)) { + if (_is_gc_request_pending.load_relaxed()) { current_thread->exit_critical(); // slow-path enter_slow(current_thread); } - DEBUG_ONLY(AtomicAccess::add(&_verify_in_cr_count, (uint64_t)1);) + DEBUG_ONLY(_verify_in_cr_count.add_then_fetch(1u);) } else { current_thread->enter_critical(); } @@ -55,7 +55,7 @@ void GCLocker::exit(JavaThread* current_thread) { #ifdef ASSERT if (current_thread->in_last_critical()) { - AtomicAccess::add(&_verify_in_cr_count, (uint64_t)-1); + _verify_in_cr_count.sub_then_fetch(1u); // Matching the loadload in GCLocker::block. OrderAccess::storestore(); } diff --git a/src/hotspot/share/gc/shared/pretouchTask.cpp b/src/hotspot/share/gc/shared/pretouchTask.cpp index cc84c8c449d..c999c98ea99 100644 --- a/src/hotspot/share/gc/shared/pretouchTask.cpp +++ b/src/hotspot/share/gc/shared/pretouchTask.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, 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 @@ -25,7 +25,7 @@ #include "gc/shared/gc_globals.hpp" #include "gc/shared/pretouchTask.hpp" #include "logging/log.hpp" -#include "runtime/atomicAccess.hpp" +#include "runtime/atomic.hpp" #include "runtime/globals.hpp" #include "runtime/os.hpp" #include "utilities/align.hpp" @@ -52,11 +52,11 @@ size_t PretouchTask::chunk_size() { void PretouchTask::work(uint worker_id) { while (true) { - char* cur_start = AtomicAccess::load(&_cur_addr); + char* cur_start = _cur_addr.load_relaxed(); char* cur_end = cur_start + MIN2(_chunk_size, pointer_delta(_end_addr, cur_start, 1)); if (cur_start >= cur_end) { break; - } else if (cur_start == AtomicAccess::cmpxchg(&_cur_addr, cur_start, cur_end)) { + } else if (cur_start == _cur_addr.compare_exchange(cur_start, cur_end)) { os::pretouch_memory(cur_start, cur_end, _page_size); } // Else attempt to claim chunk failed, so try again. } diff --git a/src/hotspot/share/gc/shared/pretouchTask.hpp b/src/hotspot/share/gc/shared/pretouchTask.hpp index 7c66c8b717c..6355d61edf7 100644 --- a/src/hotspot/share/gc/shared/pretouchTask.hpp +++ b/src/hotspot/share/gc/shared/pretouchTask.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026, 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 @@ -26,9 +26,11 @@ #define SHARE_GC_SHARED_PRETOUCH_HPP #include "gc/shared/workerThread.hpp" +#include "runtime/atomic.hpp" +#include "utilities/globalDefinitions.hpp" class PretouchTask : public WorkerTask { - char* volatile _cur_addr; + Atomic _cur_addr; char* const _end_addr; size_t _page_size; size_t _chunk_size; diff --git a/src/hotspot/share/gc/shared/suspendibleThreadSet.cpp b/src/hotspot/share/gc/shared/suspendibleThreadSet.cpp index 83783b31ad9..834a3d04c00 100644 --- a/src/hotspot/share/gc/shared/suspendibleThreadSet.cpp +++ b/src/hotspot/share/gc/shared/suspendibleThreadSet.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -30,7 +30,7 @@ uint SuspendibleThreadSet::_nthreads = 0; uint SuspendibleThreadSet::_nthreads_stopped = 0; -volatile bool SuspendibleThreadSet::_suspend_all = false; +Atomic SuspendibleThreadSet::_suspend_all{false}; double SuspendibleThreadSet::_suspend_all_start = 0.0; static Semaphore* _synchronize_wakeup = nullptr; @@ -96,7 +96,7 @@ void SuspendibleThreadSet::synchronize() { { MonitorLocker ml(STS_lock, Mutex::_no_safepoint_check_flag); assert(!should_yield(), "Only one at a time"); - AtomicAccess::store(&_suspend_all, true); + _suspend_all.store_relaxed(true); if (is_synchronized()) { return; } @@ -127,6 +127,6 @@ void SuspendibleThreadSet::desynchronize() { MonitorLocker ml(STS_lock, Mutex::_no_safepoint_check_flag); assert(should_yield(), "STS not synchronizing"); assert(is_synchronized(), "STS not synchronized"); - AtomicAccess::store(&_suspend_all, false); + _suspend_all.store_relaxed(false); ml.notify_all(); } diff --git a/src/hotspot/share/gc/shared/suspendibleThreadSet.hpp b/src/hotspot/share/gc/shared/suspendibleThreadSet.hpp index 38568c015bc..72fca5302e7 100644 --- a/src/hotspot/share/gc/shared/suspendibleThreadSet.hpp +++ b/src/hotspot/share/gc/shared/suspendibleThreadSet.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -26,7 +26,7 @@ #define SHARE_GC_SHARED_SUSPENDIBLETHREADSET_HPP #include "memory/allocation.hpp" -#include "runtime/atomicAccess.hpp" +#include "runtime/atomic.hpp" // A SuspendibleThreadSet is a set of threads that can be suspended. // A thread can join and later leave the set, and periodically yield. @@ -43,7 +43,7 @@ class SuspendibleThreadSet : public AllStatic { private: static uint _nthreads; static uint _nthreads_stopped; - static volatile bool _suspend_all; + static Atomic _suspend_all; static double _suspend_all_start; static bool is_synchronized(); @@ -59,7 +59,7 @@ private: public: // Returns true if an suspension is in progress. - static bool should_yield() { return AtomicAccess::load(&_suspend_all); } + static bool should_yield() { return _suspend_all.load_relaxed(); } // Suspends the current thread if a suspension is in progress. static void yield() { diff --git a/src/hotspot/share/gc/shared/workerThread.cpp b/src/hotspot/share/gc/shared/workerThread.cpp index 3a999da59dc..7a9404a195a 100644 --- a/src/hotspot/share/gc/shared/workerThread.cpp +++ b/src/hotspot/share/gc/shared/workerThread.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2026, 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 @@ -42,7 +42,7 @@ WorkerTaskDispatcher::WorkerTaskDispatcher() : void WorkerTaskDispatcher::coordinator_distribute_task(WorkerTask* task, uint num_workers) { // No workers are allowed to read the state variables until they have been signaled. _task = task; - _not_finished = num_workers; + _not_finished.store_relaxed(num_workers); // Dispatch 'num_workers' number of tasks. _start_semaphore.signal(num_workers); @@ -51,9 +51,12 @@ void WorkerTaskDispatcher::coordinator_distribute_task(WorkerTask* task, uint nu _end_semaphore.wait(); // No workers are allowed to read the state variables after the coordinator has been signaled. - assert(_not_finished == 0, "%d not finished workers?", _not_finished); +#ifdef ASSERT + uint not_finished = _not_finished.load_relaxed(); + assert(not_finished == 0, "%u not finished workers?", not_finished); +#endif // ASSERT _task = nullptr; - _started = 0; + _started.store_relaxed(0); } void WorkerTaskDispatcher::worker_run_task() { @@ -61,7 +64,7 @@ void WorkerTaskDispatcher::worker_run_task() { _start_semaphore.wait(); // Get and set worker id. - const uint worker_id = AtomicAccess::fetch_then_add(&_started, 1u); + const uint worker_id = _started.fetch_then_add(1u); WorkerThread::set_worker_id(worker_id); // Run task. @@ -70,7 +73,7 @@ void WorkerTaskDispatcher::worker_run_task() { // Mark that the worker is done with the task. // The worker is not allowed to read the state variables after this line. - const uint not_finished = AtomicAccess::sub(&_not_finished, 1u); + const uint not_finished = _not_finished.sub_then_fetch(1u); // The last worker signals to the coordinator that all work is completed. if (not_finished == 0) { diff --git a/src/hotspot/share/gc/shared/workerThread.hpp b/src/hotspot/share/gc/shared/workerThread.hpp index d4e92797039..a1f7282abe4 100644 --- a/src/hotspot/share/gc/shared/workerThread.hpp +++ b/src/hotspot/share/gc/shared/workerThread.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2026, 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 @@ -27,6 +27,7 @@ #include "gc/shared/gcId.hpp" #include "memory/allocation.hpp" +#include "runtime/atomic.hpp" #include "runtime/nonJavaThread.hpp" #include "runtime/semaphore.hpp" #include "utilities/debug.hpp" @@ -58,8 +59,8 @@ class WorkerTaskDispatcher { // The task currently being dispatched to the WorkerThreads. WorkerTask* _task; - volatile uint _started; - volatile uint _not_finished; + Atomic _started; + Atomic _not_finished; // Semaphore used to start the WorkerThreads. Semaphore _start_semaphore; diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp index febd79c979c..f0531f3b0f9 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.cpp @@ -406,8 +406,8 @@ void ShenandoahAdaptiveHeuristics::record_success_concurrent() { } } -void ShenandoahAdaptiveHeuristics::record_success_degenerated() { - ShenandoahHeuristics::record_success_degenerated(); +void ShenandoahAdaptiveHeuristics::record_degenerated() { + ShenandoahHeuristics::record_degenerated(); add_degenerated_gc_time(_precursor_cycle_start, elapsed_degenerated_cycle_time()); // Adjust both trigger's parameters in the case of a degenerated GC because // either of them should have triggered earlier to avoid this case. diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp index 73bd25b17ed..6e8b50afad7 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahAdaptiveHeuristics.hpp @@ -152,7 +152,7 @@ public: virtual void record_cycle_start() override; virtual void record_success_concurrent() override; - virtual void record_success_degenerated() override; + virtual void record_degenerated() override; virtual void record_success_full() override; virtual bool should_start_gc() override; diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp index 65337748b56..62de867b489 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.cpp @@ -264,7 +264,7 @@ void ShenandoahHeuristics::record_success_concurrent() { adjust_penalty(Concurrent_Adjust); } -void ShenandoahHeuristics::record_success_degenerated() { +void ShenandoahHeuristics::record_degenerated() { adjust_penalty(Degenerated_Penalty); } diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp index e6d2b769607..65029503885 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahHeuristics.hpp @@ -224,7 +224,7 @@ public: virtual void record_success_concurrent(); - virtual void record_success_degenerated(); + virtual void record_degenerated(); virtual void record_success_full(); diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp index 8bf068df0a8..029a4dd98fb 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.cpp @@ -766,10 +766,10 @@ void ShenandoahOldHeuristics::record_success_concurrent() { this->ShenandoahHeuristics::record_success_concurrent(); } -void ShenandoahOldHeuristics::record_success_degenerated() { +void ShenandoahOldHeuristics::record_degenerated() { // Forget any triggers that occurred while OLD GC was ongoing. If we really need to start another, it will retrigger. clear_triggers(); - this->ShenandoahHeuristics::record_success_degenerated(); + this->ShenandoahHeuristics::record_degenerated(); } void ShenandoahOldHeuristics::record_success_full() { diff --git a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.hpp b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.hpp index 288d3d68e56..f38194c1ee7 100644 --- a/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.hpp +++ b/src/hotspot/share/gc/shenandoah/heuristics/shenandoahOldHeuristics.hpp @@ -201,7 +201,7 @@ public: void record_success_concurrent() override; - void record_success_degenerated() override; + void record_degenerated() override; void record_success_full() override; diff --git a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp index 302e524fe44..0a5dff3b5ad 100644 --- a/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp +++ b/src/hotspot/share/gc/shenandoah/shenandoahDegeneratedGC.cpp @@ -314,10 +314,13 @@ void ShenandoahDegenGC::op_degenerated() { if (progress) { heap->notify_gc_progress(); heap->shenandoah_policy()->record_success_degenerated(_generation->is_young(), _abbreviated); - _generation->heuristics()->record_success_degenerated(); + _generation->heuristics()->record_degenerated(); heap->start_idle_span(); } else if (!heap->mode()->is_generational() || policy->generational_should_upgrade_degenerated_gc()) { + // Upgrade to full GC, register full-GC impact on heuristics. op_degenerated_futile(); + } else { + _generation->heuristics()->record_degenerated(); } } diff --git a/src/hotspot/share/opto/callnode.cpp b/src/hotspot/share/opto/callnode.cpp index 4cf72fbde4b..fac8596173f 100644 --- a/src/hotspot/share/opto/callnode.cpp +++ b/src/hotspot/share/opto/callnode.cpp @@ -1031,6 +1031,15 @@ bool CallNode::is_call_to_arraycopystub() const { return false; } +bool CallNode::is_call_to_multianewarray_stub() const { + if (_name != nullptr && + strstr(_name, "multianewarray") != nullptr && + strstr(_name, "C2 runtime") != nullptr) { + return true; + } + return false; +} + //============================================================================= uint CallJavaNode::size_of() const { return sizeof(*this); } bool CallJavaNode::cmp( const Node &n ) const { diff --git a/src/hotspot/share/opto/callnode.hpp b/src/hotspot/share/opto/callnode.hpp index de4b8941359..0bb064efc43 100644 --- a/src/hotspot/share/opto/callnode.hpp +++ b/src/hotspot/share/opto/callnode.hpp @@ -758,6 +758,7 @@ public: virtual uint match_edge(uint idx) const; bool is_call_to_arraycopystub() const; + bool is_call_to_multianewarray_stub() const; virtual void copy_call_debug_info(PhaseIterGVN* phase, SafePointNode* sfpt) {} diff --git a/src/hotspot/share/opto/escape.cpp b/src/hotspot/share/opto/escape.cpp index b067d93176a..7b5c3855a9e 100644 --- a/src/hotspot/share/opto/escape.cpp +++ b/src/hotspot/share/opto/escape.cpp @@ -2066,8 +2066,7 @@ void ConnectionGraph::add_call_node(CallNode* call) { // Use bytecode estimator to record whether the call's return value escapes. ciMethod* meth = call->as_CallJava()->method(); if (meth == nullptr) { - const char* name = call->as_CallStaticJava()->_name; - assert(strncmp(name, "C2 Runtime multianewarray", 25) == 0, "TODO: add failed case check"); + assert(call->as_CallStaticJava()->is_call_to_multianewarray_stub(), "TODO: add failed case check"); // Returns a newly allocated non-escaped object. add_java_object(call, PointsToNode::NoEscape); set_not_scalar_replaceable(ptnode_adr(call_idx) NOT_PRODUCT(COMMA "is result of multinewarray")); @@ -2775,8 +2774,7 @@ int ConnectionGraph::find_init_values_phantom(JavaObjectNode* pta) { assert(pta->arraycopy_dst() || alloc->as_CallStaticJava(), "sanity"); #ifdef ASSERT if (!pta->arraycopy_dst() && alloc->as_CallStaticJava()->method() == nullptr) { - const char* name = alloc->as_CallStaticJava()->_name; - assert(strncmp(name, "C2 Runtime multianewarray", 25) == 0, "sanity"); + assert(alloc->as_CallStaticJava()->is_call_to_multianewarray_stub(), "sanity"); } #endif // Non-escaped allocation returned from Java or runtime call have unknown values in fields. diff --git a/src/hotspot/share/opto/superword.cpp b/src/hotspot/share/opto/superword.cpp index 35b46d22732..31cc8fa0460 100644 --- a/src/hotspot/share/opto/superword.cpp +++ b/src/hotspot/share/opto/superword.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2026, 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 @@ -2482,6 +2482,11 @@ static bool can_subword_truncate(Node* in, const Type* type) { return false; } + // Since casts specifically change the type of a node, stay on the safe side and do not truncate them. + if (in->is_ConstraintCast()) { + return false; + } + // Cannot be truncated: switch (opc) { case Op_AbsI: diff --git a/src/hotspot/share/runtime/prefetch.inline.hpp b/src/hotspot/share/runtime/prefetch.inline.hpp index 4cc1d93f613..18630f71a62 100644 --- a/src/hotspot/share/runtime/prefetch.inline.hpp +++ b/src/hotspot/share/runtime/prefetch.inline.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -25,9 +25,24 @@ #ifndef SHARE_RUNTIME_PREFETCH_INLINE_HPP #define SHARE_RUNTIME_PREFETCH_INLINE_HPP -#include "runtime/prefetch.hpp" - +#include "memory/allStatic.hpp" #include "utilities/macros.hpp" + +// If calls to prefetch methods are in a loop, the loop should be cloned +// such that if Prefetch{Scan,Copy}Interval and/or PrefetchFieldInterval +// say not to do prefetching, these methods aren't called. At the very +// least, they take up a memory issue slot. They should be implemented +// as inline assembly code: doing an actual call isn't worth the cost. + +class Prefetch : AllStatic { + public: + // Prefetch anticipating read; must not fault, semantically a no-op + static void read(const void* loc, intx interval); + + // Prefetch anticipating write; must not fault, semantically a no-op + static void write(void* loc, intx interval); +}; + #include OS_CPU_HEADER_INLINE(prefetch) #endif // SHARE_RUNTIME_PREFETCH_INLINE_HPP diff --git a/src/hotspot/share/runtime/stubDeclarations.hpp b/src/hotspot/share/runtime/stubDeclarations.hpp index f9080364dc4..c478eda3e7c 100644 --- a/src/hotspot/share/runtime/stubDeclarations.hpp +++ b/src/hotspot/share/runtime/stubDeclarations.hpp @@ -191,8 +191,7 @@ // // C2 stub blob/field names // -// C2 stubs are provided with names in the format "C2 Runtime -// _blob". +// C2 stubs are provided with names in the format "_blob (C2 runtime)". // // A stub creation method OptoRuntime::generate(ciEnv* env) is // generated which invokes the C2 compiler to generate each stub in diff --git a/src/hotspot/share/runtime/stubInfo.cpp b/src/hotspot/share/runtime/stubInfo.cpp index 47a31fe7967..ee90631145a 100644 --- a/src/hotspot/share/runtime/stubInfo.cpp +++ b/src/hotspot/share/runtime/stubInfo.cpp @@ -479,7 +479,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, #define PROCESS_SHARED_BLOB(name, type) \ process_shared_blob(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Shared Runtime " # name "_blob", \ + #name "_blob (shared runtime)", \ BlobId:: JOIN3(shared, name, id), \ StubId:: JOIN3(shared, name, id), \ EntryId:: JOIN3(shared, name, id), \ @@ -488,7 +488,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, #define PROCESS_C1_BLOB(name) \ process_c1_blob(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "C1 Runtime " # name "_blob", \ + #name "_blob (C1 runtime)", \ BlobId:: JOIN3(c1, name, id), \ StubId:: JOIN3(c1, name, id), \ EntryId:: JOIN3(c1, name, id)); \ @@ -496,7 +496,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, #define PROCESS_C2_BLOB(name, type) \ process_c2_blob(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "C2 Runtime " # name "_blob", \ + #name "_blob (C2 runtime)", \ BlobId:: JOIN3(c2, name, id), \ StubId:: JOIN3(c2, name, id), \ EntryId:: JOIN3(c2, name, id)); \ @@ -504,7 +504,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, #define PROCESS_C2_STUB(name, fancy_jump, pass_tls, return_pc) \ process_c2_blob(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "C2 Runtime " # name "_blob", \ + #name "_blob (C2 runtime)", \ BlobId:: JOIN3(c2, name, id), \ StubId:: JOIN3(c2, name, id), \ EntryId:: JOIN3(c2, name, id)); \ @@ -512,20 +512,20 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, #define PROCESS_STUBGEN_BLOB(blob) \ process_stubgen_blob(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # blob "_blob", \ + #blob "_blob (stub gen)", \ BlobId:: JOIN3(stubgen, blob, id)); \ #define PROCESS_STUBGEN_STUB(blob, stub) \ process_stubgen_stub(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # stub "_stub", \ + #stub "_stub (stub gen)", \ BlobId:: JOIN3(stubgen, blob, id), \ StubId:: JOIN3(stubgen, stub, id)); \ #define PROCESS_STUBGEN_ENTRY(blob, stub, field_name, getter_name) \ process_stubgen_entry(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # field_name "_entry", \ + #field_name "_entry (stub gen)", \ BlobId:: JOIN3(stubgen, blob, id), \ StubId:: JOIN3(stubgen, stub, id), \ EntryId:: JOIN3(stubgen, field_name, id), \ @@ -535,7 +535,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, init_funcion) \ process_stubgen_entry(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # field_name "_entry", \ + #field_name "_entry (stub gen)", \ BlobId:: JOIN3(stubgen, blob, id), \ StubId:: JOIN3(stubgen, stub, id), \ EntryId:: JOIN3(stubgen, field_name, id), \ @@ -545,7 +545,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, count) \ process_stubgen_entry(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # field_name "_entry", \ + #field_name "_entry (stub gen)", \ BlobId:: JOIN3(stubgen, blob, id), \ StubId:: JOIN3(stubgen, stub, id), \ EntryId:: JOIN3(stubgen, field_name, id), \ @@ -567,7 +567,7 @@ void StubInfo::process_stubgen_entry(StubGroup& group_cursor, init_function) \ process_stubgen_entry(_group_cursor, _blob_cursor, \ _stub_cursor, _entry_cursor, \ - "Stub Generator " # arch_name "_" # field_name "_entry", \ + #arch_name "_" # field_name "_entry (stub gen)",\ BlobId:: JOIN3(stubgen, blob, id), \ StubId:: JOIN3(stubgen, stub, id), \ EntryId:: JOIN4(stubgen, arch_name, \ diff --git a/src/hotspot/share/utilities/byteswap.hpp b/src/hotspot/share/utilities/byteswap.hpp index 371095bce49..9fc367c784e 100644 --- a/src/hotspot/share/utilities/byteswap.hpp +++ b/src/hotspot/share/utilities/byteswap.hpp @@ -1,4 +1,5 @@ /* + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2023, 2024, Google and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -141,7 +142,7 @@ struct ByteswapImpl : public ByteswapFallbackImpl {}; *****************************************************************************/ #elif defined(TARGET_COMPILER_visCPP) -#include +#include "cppstdlib/cstdlib.hpp" #pragma intrinsic(_byteswap_ushort) #pragma intrinsic(_byteswap_ulong) diff --git a/src/hotspot/share/utilities/forbiddenFunctions.hpp b/src/hotspot/share/utilities/forbiddenFunctions.hpp index 47becd7b4c7..9d1b88e6233 100644 --- a/src/hotspot/share/utilities/forbiddenFunctions.hpp +++ b/src/hotspot/share/utilities/forbiddenFunctions.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -32,12 +32,6 @@ #include #include -// Workaround for noreturn functions: exit, _exit, _Exit - see the clang -// definition of FORBIDDEN_FUNCTION_NORETURN_ATTRIBUTE. -#ifdef __clang__ -#include -#endif - #ifdef _WINDOWS #include "forbiddenFunctions_windows.hpp" #else @@ -49,12 +43,6 @@ // or have security concerns, either with preferred alternatives, or to be // avoided entirely. -FORBID_IMPORTED_NORETURN_C_FUNCTION(void exit(int), noexcept, "use os::exit") -FORBID_IMPORTED_NORETURN_C_FUNCTION(void _Exit(int), noexcept, "use os::exit") - -// Windows puts _exit in , POSIX in . -FORBID_IMPORTED_NORETURN_C_FUNCTION(void _exit(int), /* not noexcept */, "use os::exit") - FORBID_IMPORTED_C_FUNCTION(char* strerror(int), noexcept, "use os::strerror"); FORBID_IMPORTED_C_FUNCTION(char* strtok(char*, const char*), noexcept, "use strtok_r"); @@ -70,13 +58,8 @@ FORBID_C_FUNCTION(int vsprintf(char*, const char*, va_list), noexcept, "use os:: FORBID_C_FUNCTION(int vsnprintf(char*, size_t, const char*, va_list), noexcept, "use os::vsnprintf"); PRAGMA_DIAG_POP -// All of the following functions return raw C-heap pointers (sometimes as an -// option, e.g. realpath or getwd) or, in case of free(), take raw C-heap -// pointers. We generally want allocation to be done through NMT. -FORBID_IMPORTED_C_FUNCTION(void* malloc(size_t size), noexcept, "use os::malloc"); -FORBID_IMPORTED_C_FUNCTION(void free(void *ptr), noexcept, "use os::free"); -FORBID_IMPORTED_C_FUNCTION(void* calloc(size_t nmemb, size_t size), noexcept, "use os::malloc and zero out manually"); -FORBID_IMPORTED_C_FUNCTION(void* realloc(void *ptr, size_t size), noexcept, "use os::realloc"); +// All of the following functions return raw C-heap pointers. We generally +// want allocation to be done through NMT. FORBID_IMPORTED_C_FUNCTION(char* strdup(const char *s), noexcept, "use os::strdup"); FORBID_IMPORTED_C_FUNCTION(wchar_t* wcsdup(const wchar_t *s), noexcept, "don't use"); diff --git a/src/hotspot/share/utilities/globalDefinitions_gcc.hpp b/src/hotspot/share/utilities/globalDefinitions_gcc.hpp index f82f9b1386a..302a62de6c1 100644 --- a/src/hotspot/share/utilities/globalDefinitions_gcc.hpp +++ b/src/hotspot/share/utilities/globalDefinitions_gcc.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1998, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1998, 2026, 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 @@ -31,6 +31,8 @@ // globally used constants & types, class (forward) // declarations and a few frequently used utility functions. +#include "cppstdlib/cstdlib.hpp" + #include #include #include @@ -44,15 +46,6 @@ #include #include #include -#include -// In stdlib.h on AIX malloc is defined as a macro causing -// compiler errors when resolving them in different depths as it -// happens in the log tags. This avoids the macro. -#if (defined(__VEC__) || defined(__AIXVEC)) && defined(AIX) \ - && defined(__open_xl_version__) && __open_xl_version__ >= 17 - #undef malloc - extern void *malloc(size_t) asm("vec_malloc"); -#endif #include #include #include diff --git a/src/hotspot/share/utilities/globalDefinitions_visCPP.hpp b/src/hotspot/share/utilities/globalDefinitions_visCPP.hpp index b9d25096cd5..dfd6f2f1880 100644 --- a/src/hotspot/share/utilities/globalDefinitions_visCPP.hpp +++ b/src/hotspot/share/utilities/globalDefinitions_visCPP.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, 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 @@ -34,6 +34,8 @@ // Need this on windows to get the math constants (e.g., M_PI). #define _USE_MATH_DEFINES +#include "cppstdlib/cstdlib.hpp" + # include # include # include // for _isnan @@ -45,7 +47,6 @@ # include // for offsetof # include # include -# include # include # include # include diff --git a/src/hotspot/share/utilities/parseInteger.hpp b/src/hotspot/share/utilities/parseInteger.hpp index 3e3eafada87..8840275a8cb 100644 --- a/src/hotspot/share/utilities/parseInteger.hpp +++ b/src/hotspot/share/utilities/parseInteger.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2022 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -26,6 +26,7 @@ #ifndef SHARE_UTILITIES_PARSE_INTEGER_HPP #define SHARE_UTILITIES_PARSE_INTEGER_HPP +#include "cppstdlib/cstdlib.hpp" #include "cppstdlib/limits.hpp" #include "metaprogramming/enableIf.hpp" #include "utilities/debug.hpp" @@ -33,7 +34,6 @@ #include "utilities/macros.hpp" #include -#include // ************************************************************************* // ** Attention compatibility! ** diff --git a/src/hotspot/share/utilities/permitForbiddenFunctions.hpp b/src/hotspot/share/utilities/permitForbiddenFunctions.hpp index 1ba42f8e386..71719ac8a76 100644 --- a/src/hotspot/share/utilities/permitForbiddenFunctions.hpp +++ b/src/hotspot/share/utilities/permitForbiddenFunctions.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -25,6 +25,7 @@ #ifndef SHARE_UTILITIES_PERMITFORBIDDENFUNCTIONS_HPP #define SHARE_UTILITIES_PERMITFORBIDDENFUNCTIONS_HPP +#include "cppstdlib/cstdlib.hpp" #include "utilities/compilerWarnings.hpp" #include "utilities/globalDefinitions.hpp" @@ -34,6 +35,9 @@ #include "permitForbiddenFunctions_posix.hpp" #endif +#include +#include + // Provide wrappers for some functions otherwise forbidden from use in HotSpot. // // There may be special circumstances where an otherwise forbidden function @@ -53,7 +57,6 @@ namespace permit_forbidden_function { BEGIN_ALLOW_FORBIDDEN_FUNCTIONS [[noreturn]] inline void exit(int status) { ::exit(status); } -[[noreturn]] inline void _exit(int status) { ::_exit(status); } ATTRIBUTE_PRINTF(3, 0) inline int vsnprintf(char* str, size_t size, const char* format, va_list ap) { diff --git a/src/java.base/share/classes/java/lang/Math.java b/src/java.base/share/classes/java/lang/Math.java index 0f39ecf0a8a..55659bed57b 100644 --- a/src/java.base/share/classes/java/lang/Math.java +++ b/src/java.base/share/classes/java/lang/Math.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1994, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1994, 2026, 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 @@ -2378,6 +2378,20 @@ public final class Math { */ @IntrinsicCandidate public static double fma(double a, double b, double c) { + // Implementation note: this method is intentionally coded in + // a straightforward manner relying on BigDecimal for the + // heavy-lifting of the numerical computation. It would be + // possible for the computation to be done solely using binary + // floating-point and integer operations, at the cost of more + // complicated logic. Since most processors have hardware + // support for fma and this method is an intrinsic candidate, + // the software implementation below would only be used on + // processors without native fma support (and also possibly on + // processors with native fma support while running in the + // interpreter). Therefore, the direct performance of the code + // is less of a concern than the code's simplicity, + // maintainability, and testability. + /* * Infinity and NaN arithmetic is not quite the same with two * roundings as opposed to just one so the simple expression @@ -2492,6 +2506,8 @@ public final class Math { */ @IntrinsicCandidate public static float fma(float a, float b, float c) { + // See implementation note in fma(double, double, double). + if (Float.isFinite(a) && Float.isFinite(b) && Float.isFinite(c)) { if (a == 0.0 || b == 0.0) { return a * b + c; // Handled signed zero cases diff --git a/src/java.base/share/classes/java/security/Security.java b/src/java.base/share/classes/java/security/Security.java index 6969fe8a8e1..30a22b05742 100644 --- a/src/java.base/share/classes/java/security/Security.java +++ b/src/java.base/share/classes/java/security/Security.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2026, 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 @@ -34,6 +34,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; @@ -112,7 +113,7 @@ public final class Security { private static Path currentPath; - private static final Set activePaths = new HashSet<>(); + private static final List activePaths = new ArrayList<>(); static void loadAll() { // first load the master properties file to @@ -262,30 +263,40 @@ public final class Security { } } + private static void checkCyclicInclude(Path path) { + for (Path activePath : activePaths) { + try { + if (Files.isSameFile(path, activePath)) { + throw new InternalError( + "Cyclic include of '" + path + "'"); + } + } catch (IOException e) { + if (sdebug != null) { + sdebug.println("skipped exception when checking for " + + "cyclic inclusion of " + path + ":"); + e.printStackTrace(); + } + } + } + } + private static void loadFromPath(Path path, LoadingMode mode) throws IOException { - boolean isRegularFile = Files.isRegularFile(path); - if (isRegularFile) { - path = path.toRealPath(); - } else if (Files.isDirectory(path)) { + if (Files.isDirectory(path)) { throw new IOException("Is a directory"); - } else { - path = path.toAbsolutePath(); - } - if (activePaths.contains(path)) { - throw new InternalError("Cyclic include of '" + path + "'"); } try (InputStream is = Files.newInputStream(path)) { + checkCyclicInclude(path); reset(mode); Path previousPath = currentPath; - currentPath = isRegularFile ? path : null; + currentPath = Files.isRegularFile(path) ? path : null; activePaths.add(path); try { debugLoad(true, path); props.load(is); debugLoad(false, path); } finally { - activePaths.remove(path); + activePaths.removeLast(); currentPath = previousPath; } } diff --git a/src/java.base/share/classes/java/util/zip/GZIPInputStream.java b/src/java.base/share/classes/java/util/zip/GZIPInputStream.java index ab7ea53793f..ebcb9e3204c 100644 --- a/src/java.base/share/classes/java/util/zip/GZIPInputStream.java +++ b/src/java.base/share/classes/java/util/zip/GZIPInputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2026, 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 @@ -79,7 +79,11 @@ public class GZIPInputStream extends InflaterInputStream { super(in, createInflater(in, size), size); usesDefaultInflater = true; try { - readHeader(in); + // we don't expect the stream to be at EOF + // and if it is, then we want readHeader to + // raise an exception, so we pass "true" for + // the "failOnEOF" param. + readHeader(in, true); } catch (IOException ioe) { this.inf.end(); throw ioe; @@ -190,12 +194,40 @@ public class GZIPInputStream extends InflaterInputStream { /* * Reads GZIP member header and returns the total byte number * of this member header. + * If failOnEOF is false and if the given InputStream has already + * reached EOF when this method was invoked, then this method returns + * -1 (indicating that there's no GZIP member header). + * In all other cases of malformed header or EOF being detected + * when reading the header, this method will throw an IOException. */ - private int readHeader(InputStream this_in) throws IOException { + private int readHeader(InputStream this_in, boolean failOnEOF) throws IOException { CheckedInputStream in = new CheckedInputStream(this_in, crc); crc.reset(); + + int magic; + if (!failOnEOF) { + // read an unsigned short value representing the GZIP magic header. + // this is the same as calling readUShort(in), except that here, + // when reading the first byte, we don't raise an EOFException + // if the stream has already reached EOF. + + // read unsigned byte + int b = in.read(); + if (b == -1) { // EOF + crc.reset(); + return -1; // represents no header bytes available + } + checkUnexpectedByte(b); + // read the next unsigned byte to form the unsigned + // short. we throw the usual EOFException/ZipException + // from this point on if there is no more data or + // the data doesn't represent a header. + magic = (readUByte(in) << 8) | b; + } else { + magic = readUShort(in); + } // Check header magic - if (readUShort(in) != GZIP_MAGIC) { + if (magic != GZIP_MAGIC) { throw new ZipException("Not in GZIP format"); } // Check compression method @@ -261,7 +293,11 @@ public class GZIPInputStream extends InflaterInputStream { // try concatenated case int m = 8; // this.trailer try { - m += readHeader(in); // next.header + int numNextHeaderBytes = readHeader(in, false); // next.header (if available) + if (numNextHeaderBytes == -1) { + return true; // end of stream reached + } + m += numNextHeaderBytes; } catch (IOException ze) { return true; // ignore any malformed, do nothing } @@ -295,14 +331,18 @@ public class GZIPInputStream extends InflaterInputStream { if (b == -1) { throw new EOFException(); } - if (b < -1 || b > 255) { - // Report on this.in, not argument in; see read{Header, Trailer}. - throw new IOException(this.in.getClass().getName() - + ".read() returned value out of range -1..255: " + b); - } + checkUnexpectedByte(b); return b; } + private void checkUnexpectedByte(final int b) throws IOException { + if (b < -1 || b > 255) { + // report the InputStream type which returned this unexpected byte + throw new IOException(this.in.getClass().getName() + + ".read() returned value out of range -1..255: " + b); + } + } + private byte[] tmpbuf = new byte[128]; /* diff --git a/src/java.base/share/classes/jdk/internal/util/ByteArray.java b/src/java.base/share/classes/jdk/internal/util/ByteArray.java index 94395df2c34..92b9dbbef19 100644 --- a/src/java.base/share/classes/jdk/internal/util/ByteArray.java +++ b/src/java.base/share/classes/jdk/internal/util/ByteArray.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, 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 @@ -241,7 +241,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length] + * the range [0, array.length - 1] * @see #getBoolean(byte[], int) */ public static void setBoolean(byte[] array, int offset, boolean value) { @@ -328,7 +328,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 4] * @see #getFloat(byte[], int) */ public static void setFloat(byte[] array, int offset, float value) { @@ -350,7 +350,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 4] * @see #getFloatRaw(byte[], int) */ public static void setFloatRaw(byte[] array, int offset, float value) { @@ -368,7 +368,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 4] + * the range [0, array.length - 8] * @see #getLong(byte[], int) */ public static void setLong(byte[] array, int offset, long value) { @@ -387,7 +387,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 8] * @see #getDouble(byte[], int) */ public static void setDouble(byte[] array, int offset, double value) { @@ -409,7 +409,7 @@ public final class ByteArray { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 8] * @see #getDoubleRaw(byte[], int) */ public static void setDoubleRaw(byte[] array, int offset, double value) { diff --git a/src/java.base/share/classes/jdk/internal/util/ByteArrayLittleEndian.java b/src/java.base/share/classes/jdk/internal/util/ByteArrayLittleEndian.java index fcd7ca2b9bf..cc50686429b 100644 --- a/src/java.base/share/classes/jdk/internal/util/ByteArrayLittleEndian.java +++ b/src/java.base/share/classes/jdk/internal/util/ByteArrayLittleEndian.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, 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 @@ -241,7 +241,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length] + * the range [0, array.length - 1] * @see #getBoolean(byte[], int) */ public static void setBoolean(byte[] array, int offset, boolean value) { @@ -328,7 +328,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 4] * @see #getFloat(byte[], int) */ public static void setFloat(byte[] array, int offset, float value) { @@ -350,7 +350,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 4] * @see #getFloatRaw(byte[], int) */ public static void setFloatRaw(byte[] array, int offset, float value) { @@ -368,7 +368,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 4] + * the range [0, array.length - 8] * @see #getLong(byte[], int) */ public static void setLong(byte[] array, int offset, long value) { @@ -387,7 +387,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 8] * @see #getDouble(byte[], int) */ public static void setDouble(byte[] array, int offset, double value) { @@ -409,7 +409,7 @@ public final class ByteArrayLittleEndian { * @param offset where setting (writing) in the array should begin * @param value value to set in the array * @throws IndexOutOfBoundsException if the provided {@code offset} is outside - * the range [0, array.length - 2] + * the range [0, array.length - 8] * @see #getDoubleRaw(byte[], int) */ public static void setDoubleRaw(byte[] array, int offset, double value) { diff --git a/src/java.base/share/classes/sun/security/ec/ECDSAOperations.java b/src/java.base/share/classes/sun/security/ec/ECDSAOperations.java index f58d7d8f2d7..7badcf42d9c 100644 --- a/src/java.base/share/classes/sun/security/ec/ECDSAOperations.java +++ b/src/java.base/share/classes/sun/security/ec/ECDSAOperations.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -68,7 +68,7 @@ public class ECDSAOperations { public ECDSAOperations(ECOperations ecOps, ECPoint basePoint) { this.ecOps = ecOps; - this.basePoint = toAffinePoint(basePoint, ecOps.getField()); + this.basePoint = AffinePoint.fromECPoint(basePoint, ecOps.getField()); } public ECOperations getEcOperations() { @@ -79,14 +79,6 @@ public class ECDSAOperations { return ecOps.multiply(basePoint, scalar).asAffine(); } - public static AffinePoint toAffinePoint(ECPoint point, - IntegerFieldModuloP field) { - - ImmutableIntegerModuloP affineX = field.getElement(point.getAffineX()); - ImmutableIntegerModuloP affineY = field.getElement(point.getAffineY()); - return new AffinePoint(affineX, affineY); - } - public static Optional forParameters(ECParameterSpec ecParams) { Optional curveOps = diff --git a/src/java.base/share/classes/sun/security/validator/CADistrustPolicy.java b/src/java.base/share/classes/sun/security/validator/CADistrustPolicy.java index 9c64402c123..44c9ed2d075 100644 --- a/src/java.base/share/classes/sun/security/validator/CADistrustPolicy.java +++ b/src/java.base/share/classes/sun/security/validator/CADistrustPolicy.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, 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 @@ -83,6 +83,22 @@ enum CADistrustPolicy { } CamerfirmaTLSPolicy.checkDistrust(chain); } + }, + + /** + * Distrust TLS Server certificates anchored by the Chunghwa ePKI root CA + * and issued after March 17, 2026. If enabled, this policy is currently + * enforced by the PKIX and SunX509 TrustManager implementations + * of the SunJSSE provider implementation. + */ + CHUNGHWA_TLS { + void checkDistrust(String variant, X509Certificate[] chain) + throws ValidatorException { + if (!variant.equals(Validator.VAR_TLS_SERVER)) { + return; + } + ChunghwaTLSPolicy.checkDistrust(chain); + } }; /** diff --git a/src/java.base/share/classes/sun/security/validator/ChunghwaTLSPolicy.java b/src/java.base/share/classes/sun/security/validator/ChunghwaTLSPolicy.java new file mode 100644 index 00000000000..114f5043fbf --- /dev/null +++ b/src/java.base/share/classes/sun/security/validator/ChunghwaTLSPolicy.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package sun.security.validator; + +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.Month; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.Map; +import java.util.Set; + +import sun.security.util.Debug; +import sun.security.x509.X509CertImpl; + +/** + * This class checks if Chunghwa issued TLS Server certificates should be + * restricted. + */ +final class ChunghwaTLSPolicy { + + private static final Debug debug = Debug.getInstance("certpath"); + + // SHA-256 certificate fingerprint of distrusted root for TLS + // cacerts alias: chunghwaepkirootca + // DN: OU=ePKI Root Certification Authority, + // O="Chunghwa Telecom Co., Ltd.", C=TW + private static final String FINGERPRINT = + "C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5"; + + // Any TLS Server certificate that is anchored by the Chunghwa + // root above and is issued after this date will be distrusted. + private static final LocalDate MARCH_17_2026 = + LocalDate.of(2026, Month.MARCH, 17); + + /** + * This method assumes the eeCert is a TLS Server Cert and chains back to + * the anchor. + * + * @param chain the end-entity's certificate chain. The end entity cert + * is at index 0, the trust anchor at index n-1. + * @throws ValidatorException if the certificate is distrusted + */ + static void checkDistrust(X509Certificate[] chain) + throws ValidatorException { + X509Certificate anchor = chain[chain.length-1]; + String fp = fingerprint(anchor); + if (fp == null) { + throw new ValidatorException("Cannot generate fingerprint for " + + "trust anchor of TLS server certificate"); + } + if (FINGERPRINT.equalsIgnoreCase(fp)) { + Date notBefore = chain[0].getNotBefore(); + LocalDate ldNotBefore = LocalDate.ofInstant(notBefore.toInstant(), + ZoneOffset.UTC); + // reject if certificate is issued after March 17, 2026 + checkNotBefore(ldNotBefore, MARCH_17_2026, anchor); + } + } + + private static String fingerprint(X509Certificate cert) { + return X509CertImpl.getFingerprint("SHA-256", cert, debug); + } + + // Check whether the certificate's notBeforeDate is after the + // distrust date for the anchor (root CA). Throw ValidatorException + // if it is after the distrust date. + private static void checkNotBefore(LocalDate notBeforeDate, + LocalDate distrustDate, X509Certificate anchor) + throws ValidatorException { + if (notBeforeDate.isAfter(distrustDate)) { + throw new ValidatorException + ("TLS Server certificate issued after " + distrustDate + + " and anchored by a distrusted legacy Chunghwa root CA: " + + anchor.getSubjectX500Principal(), + ValidatorException.T_UNTRUSTED_CERT, anchor); + } + } + + private ChunghwaTLSPolicy() {} +} diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index 3d26c1dcd96..b5cbce413b2 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -1437,6 +1437,9 @@ jdk.sasl.disabledMechanisms= # CAMERFIRMA_TLS : Distrust TLS Server certificates anchored by # a Camerfirma root CA and issued after April 15, 2025. # +# CHUNGHWA_TLS : Distrust TLS Server certificates anchored by +# a Chunghwa root CA and issued after March 17, 2026. +# # Leading and trailing whitespace surrounding each value are ignored. # Unknown values are ignored. If the property is commented out or set to the # empty String, no policies are enforced. @@ -1448,7 +1451,8 @@ jdk.sasl.disabledMechanisms= # jdk.certpath.disabledAlgorithms; those restrictions are still enforced even # if this property is not enabled. # -jdk.security.caDistrustPolicies=SYMANTEC_TLS,ENTRUST_TLS,CAMERFIRMA_TLS +jdk.security.caDistrustPolicies=SYMANTEC_TLS,ENTRUST_TLS,CAMERFIRMA_TLS,\ + CHUNGHWA_TLS # # FilePermission path canonicalization diff --git a/src/java.base/unix/native/libjava/io_util_md.c b/src/java.base/unix/native/libjava/io_util_md.c index 2e81cbd05c2..bcac334191c 100644 --- a/src/java.base/unix/native/libjava/io_util_md.c +++ b/src/java.base/unix/native/libjava/io_util_md.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, 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 @@ -27,6 +27,7 @@ #include "jvm.h" #include "io_util.h" #include "io_util_md.h" +#include #include #include @@ -75,20 +76,26 @@ FD handleOpen(const char *path, int oflag, int mode) { FD fd; RESTARTABLE(open(path, oflag, mode), fd); - if (fd != -1) { - struct stat buf; - int result; - RESTARTABLE(fstat(fd, &buf), result); - if (result != -1) { - if (S_ISDIR(buf.st_mode)) { - close(fd); - errno = EISDIR; - fd = -1; - } - } else { + // No further checking is needed if the file is not a + // directory or open returned an error + if (fd == -1 || ((oflag & O_ACCMODE) != O_RDONLY) != 0) { + return fd; + } + + // FileInputStream is specified to throw if the + // file is a directory + struct stat buf; + int result; + RESTARTABLE(fstat(fd, &buf), result); + if (result != -1) { + if (S_ISDIR(buf.st_mode)) { close(fd); + errno = EISDIR; fd = -1; } + } else { + close(fd); + fd = -1; } return fd; } diff --git a/src/java.compiler/share/classes/javax/lang/model/element/AnnotationMirror.java b/src/java.compiler/share/classes/javax/lang/model/element/AnnotationMirror.java index 2d92504edb6..2ffed3f7942 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/AnnotationMirror.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/AnnotationMirror.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -36,6 +36,8 @@ import javax.lang.model.type.DeclaredType; * method. There is no guarantee that any particular annotation will * always be represented by the same object. * + * @jls 9.6 Annotation Interfaces + * @jls 9.7 Annotations * @since 1.6 */ public interface AnnotationMirror { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/AnnotationValue.java b/src/java.compiler/share/classes/javax/lang/model/element/AnnotationValue.java index 77521dac191..71594d31150 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/AnnotationValue.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/AnnotationValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -27,14 +27,16 @@ package javax.lang.model.element; /** * Represents a value of an annotation interface element. - * A value is of one of the following types: - *
  • a wrapper class (such as {@link Integer}) for a primitive type - *
  • {@code String} - *
  • {@code TypeMirror} - *
  • {@code VariableElement} (representing an enum constant) - *
  • {@code AnnotationMirror} + * A value is of one of the following types (JLS {@jls 9.6.1}): + *
    • a {@linkplain java.lang##wrapperClass wrapper class} to hold a + * primitive type, such as an {@code Integer} object to hold an + * {@code int} + *
    • {@code String} representing a {@code String} + *
    • {@link javax.lang.model.type.TypeMirror TypeMirror} representing a {@code Class} literal + *
    • {@link VariableElement} representing an enum constant + *
    • {@link AnnotationMirror} representing an annotation *
    • {@code List} - * (representing the elements, in declared order, if the value is an array) + * representing the elements, in declared order, if the value is an array *
    * * @since 1.6 diff --git a/src/java.compiler/share/classes/javax/lang/model/element/ExecutableElement.java b/src/java.compiler/share/classes/javax/lang/model/element/ExecutableElement.java index 18923463248..cc0924fda61 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/ExecutableElement.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/ExecutableElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -37,6 +37,13 @@ import javax.lang.model.type.*; * clause, among other restrictions; see JLS {@jls 9.6.1} for details. * * @see ExecutableType + * @jls 8.4 Method Declarations + * @jls 8.6 Instance Initializers + * @jls 8.7 Static Initializers + * @jls 8.8 Constructor Declarations + * @jls 9.4 Method Declarations + * @jls 9.6.1 Annotation Interface Elements + * * @since 1.6 */ public interface ExecutableElement extends Element, Parameterizable { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/PackageElement.java b/src/java.compiler/share/classes/javax/lang/model/element/PackageElement.java index 651bbe70eee..79b1fb84683 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/PackageElement.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/PackageElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -58,6 +58,8 @@ import javax.lang.model.type.TypeMirror; *
* * @see javax.lang.model.util.Elements#getPackageOf + * @jls 7.4 Package Declarations + * * @since 1.6 */ public interface PackageElement extends Element, QualifiedNameable { @@ -87,6 +89,7 @@ public interface PackageElement extends Element, QualifiedNameable { * @return the fully qualified name of this package, or an * empty name if this is an unnamed package * @jls 6.7 Fully Qualified Names and Canonical Names + * @jls 7.4.1 Named Packages */ Name getQualifiedName(); diff --git a/src/java.compiler/share/classes/javax/lang/model/element/Parameterizable.java b/src/java.compiler/share/classes/javax/lang/model/element/Parameterizable.java index cf0bbdfba37..a4280fbcf29 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/Parameterizable.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/Parameterizable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, 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 @@ -30,6 +30,10 @@ import java.util.List; /** * A mixin interface for an element that has type parameters. * + * @jls 4.5 Parameterized Types + * @jls 8.4.4 Generic Methods + * @jls 8.8.4 Generic Constructors + * * @since 1.7 */ public interface Parameterizable extends Element { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/QualifiedNameable.java b/src/java.compiler/share/classes/javax/lang/model/element/QualifiedNameable.java index 1b0316f893f..555d90c1b13 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/QualifiedNameable.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/QualifiedNameable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2026, 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 @@ -28,6 +28,10 @@ package javax.lang.model.element; /** * A mixin interface for an element that has a qualified name. * + * @jls 6.5.3.2 Qualified Package Names + * @jls 6.5.5.2 Qualified Type Names + * @jls 6.7 Fully Qualified Names and Canonical Names + * * @since 1.7 */ public interface QualifiedNameable extends Element { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/TypeElement.java b/src/java.compiler/share/classes/javax/lang/model/element/TypeElement.java index abf95bcefad..3dcc8e8c7fd 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/TypeElement.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/TypeElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -80,7 +80,16 @@ import javax.lang.model.util.*; * javax.lang.model.util.Elements#getAllTypeElements(CharSequence) * queried for} in the configured environment * + * * @see DeclaredType + * @jls 8.1 Class Declarations + * @jls 8.5 Member Class and Interface Declarations + * @jls 8.9 Enum Classes + * @jls 8.10 Record Classes + * @jls 9.1 Interface Declarations + * @jls 9.5 Member Class and Interface Declarations + * @jls 9.6 Annotation Interfaces + * * @since 1.6 */ public interface TypeElement extends Element, Parameterizable, QualifiedNameable { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/TypeParameterElement.java b/src/java.compiler/share/classes/javax/lang/model/element/TypeParameterElement.java index cbaa8969126..abd06a3e038 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/TypeParameterElement.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/TypeParameterElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -35,6 +35,11 @@ import javax.lang.model.type.TypeVariable; * A type parameter declares a {@link TypeVariable}. * * @see TypeVariable + * @jls 8.1.2 Generic Classes and Type Parameters + * @jls 8.4.4 Generic Methods + * @jls 8.8.4 Generic Constructors + * @jls 9.1.2 Generic Interfaces and Type Parameters + * * @since 1.6 */ public interface TypeParameterElement extends Element { diff --git a/src/java.compiler/share/classes/javax/lang/model/element/VariableElement.java b/src/java.compiler/share/classes/javax/lang/model/element/VariableElement.java index 4d2b2582901..b8f42c46b1e 100644 --- a/src/java.compiler/share/classes/javax/lang/model/element/VariableElement.java +++ b/src/java.compiler/share/classes/javax/lang/model/element/VariableElement.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -34,6 +34,14 @@ import javax.lang.model.type.TypeKind; * parameter, local variable, resource variable, or exception * parameter. * + * @jls 8.3 Field Declaration + * @jls 8.9.1 Enum Constants + * @jls 8.4.1 Formal Parameters + * @jls 8.8.1 Formal Parameters + * @jls 14.4 Local Variable Declarations + * @jls 14.20 The {@code try} statement + * @jls 14.20.3 {@code try}-with-resources + * @since 1.6 */ public interface VariableElement extends Element { diff --git a/src/java.desktop/macosx/classes/com/apple/laf/AquaUtils.java b/src/java.desktop/macosx/classes/com/apple/laf/AquaUtils.java index da1785a9a63..5e68cc7a82c 100644 --- a/src/java.desktop/macosx/classes/com/apple/laf/AquaUtils.java +++ b/src/java.desktop/macosx/classes/com/apple/laf/AquaUtils.java @@ -35,8 +35,6 @@ import javax.swing.*; import javax.swing.border.Border; import javax.swing.plaf.UIResource; -import sun.awt.AppContext; - import sun.lwawt.macosx.CPlatformWindow; import sun.swing.SwingUtilities2; @@ -150,13 +148,34 @@ final class AquaUtils { protected abstract T create(); } - abstract static class RecyclableSingleton { - final T get() { - return AppContext.getSoftReferenceValue(this, () -> getInstance()); - } + abstract static class LazySingleton { + T instance; - void reset() { - AppContext.getAppContext().remove(this); + final T get() { + if (instance == null) { + instance = getInstance(); + } + return instance; + } + + abstract T getInstance(); + } + + abstract static class RecyclableSingleton { + + SoftReference ref; + + final T get() { + T instance; + if (ref != null) { + instance = ref.get(); + if (instance != null) { + return instance; + } + } + instance = getInstance(); + ref = new SoftReference<>(instance); + return instance; } abstract T getInstance(); @@ -197,11 +216,11 @@ final class AquaUtils { protected abstract V getInstance(K key); } - private static final RecyclableSingleton enableAnimations = new RecyclableSingleton() { + private static final LazySingleton enableAnimations = new LazySingleton() { @Override protected Boolean getInstance() { - final String sizeProperty = System.getProperty(ANIMATIONS_PROPERTY); - return !"false".equals(sizeProperty); // should be true by default + final String animationsProperty = System.getProperty(ANIMATIONS_PROPERTY); + return !"false".equals(animationsProperty); // should be true by default } }; private static boolean animationsEnabled() { diff --git a/src/java.desktop/share/classes/sun/awt/AppContext.java b/src/java.desktop/share/classes/sun/awt/AppContext.java index 39df737badb..01b1dd09887 100644 --- a/src/java.desktop/share/classes/sun/awt/AppContext.java +++ b/src/java.desktop/share/classes/sun/awt/AppContext.java @@ -47,7 +47,6 @@ import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; /** * The AppContext is a table referenced by ThreadGroup which stores @@ -735,24 +734,6 @@ public final class AppContext { } return changeSupport.getPropertyChangeListeners(propertyName); } - - public static T getSoftReferenceValue(Object key, - Supplier supplier) { - - final AppContext appContext = AppContext.getAppContext(); - @SuppressWarnings("unchecked") - SoftReference ref = (SoftReference) appContext.get(key); - if (ref != null) { - final T object = ref.get(); - if (object != null) { - return object; - } - } - final T object = supplier.get(); - ref = new SoftReference<>(object); - appContext.put(key, ref); - return object; - } } final class MostRecentKeyValue { diff --git a/src/java.desktop/share/classes/sun/awt/image/ImageCache.java b/src/java.desktop/share/classes/sun/awt/image/ImageCache.java index eed1aa5bdb7..b7ac69923da 100644 --- a/src/java.desktop/share/classes/sun/awt/image/ImageCache.java +++ b/src/java.desktop/share/classes/sun/awt/image/ImageCache.java @@ -29,7 +29,6 @@ import java.awt.*; import java.lang.ref.*; import java.util.*; import java.util.concurrent.locks.*; -import sun.awt.AppContext; /** * ImageCache - A fixed pixel count sized cache of Images keyed by arbitrary @@ -37,8 +36,6 @@ import sun.awt.AppContext; * dropped by the GC if heap memory gets tight. When our size hits max pixel * count least recently requested images are removed first. * - * The ImageCache must be used from the thread with an AppContext only. - * */ public final class ImageCache { @@ -56,9 +53,10 @@ public final class ImageCache { // Reference queue for tracking lost softreferences to images in the cache private final ReferenceQueue referenceQueue = new ReferenceQueue<>(); + private static final ImageCache instance = new ImageCache(); + public static ImageCache getInstance() { - return AppContext.getSoftReferenceValue(ImageCache.class, - () -> new ImageCache()); + return instance; } ImageCache(final int maxPixelCount) { diff --git a/src/java.desktop/share/classes/sun/awt/image/PNGImageDecoder.java b/src/java.desktop/share/classes/sun/awt/image/PNGImageDecoder.java index 357ad3be43f..08909b802c5 100644 --- a/src/java.desktop/share/classes/sun/awt/image/PNGImageDecoder.java +++ b/src/java.desktop/share/classes/sun/awt/image/PNGImageDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2026, 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 @@ -302,8 +302,16 @@ public class PNGImageDecoder extends ImageDecoder int bitsPerPixel = samplesPerPixel*bitDepth; int bytesPerPixel = (bitsPerPixel+7)>>3; int pass, passLimit; - if(interlaceMethod==0) { pass = -1; passLimit = 0; } - else { pass = 0; passLimit = 7; } + boolean isDirectByteCopy; + if(interlaceMethod==0) { + pass = -1; + passLimit = 0; + isDirectByteCopy = bPixels != null && bitDepth == 8; + } else { + pass = 0; + passLimit = 7; + isDirectByteCopy = false; + } while(++pass<=passLimit) { int row = startingRow[pass]; int rowInc = rowIncrement[pass]; @@ -334,7 +342,11 @@ public class PNGImageDecoder extends ImageDecoder int spos=0; int pixel = 0; while (col < width) { - if(wPixels !=null) { + if (isDirectByteCopy) { + System.arraycopy(rowByteBuffer, spos, bPixels, col + rowOffset, width); + spos += width; + break; + } else if(wPixels !=null) { switch(combinedType) { case COLOR|ALPHA|(8<<3): wPixels[col+rowOffset] = diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java index 3a8b4e5dbea..773c573c201 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Lint.java @@ -37,11 +37,7 @@ import java.util.Set; import java.util.stream.Stream; import com.sun.tools.javac.main.Option; -import com.sun.tools.javac.tree.JCTree.*; -import com.sun.tools.javac.util.Assert; import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition; -import com.sun.tools.javac.util.JCDiagnostic.LintWarning; import com.sun.tools.javac.util.Log; import com.sun.tools.javac.util.Names; import com.sun.tools.javac.util.Options; @@ -178,6 +174,14 @@ public class Lint { /** * Categories of warnings that can be generated by the compiler. + * Each lint category has a logical name (a string), which is the string used e.g. in a {@code SuppressWarning} annotation. + * To ensure automation, the enum field name for a lint category string {@code C} should be obtained by: + *
    + *
  1. capitalize all the letters in {@code C}, and
  2. + *
  3. replacing any occurrence of {@code -} with {@code _}
  4. + *
+ * For instance, the lint category string {@code dangling-doc-comments} corresponds to the enum field + * {@code DANGLING_DOC_COMMENTS}. */ public enum LintCategory { /** @@ -320,7 +324,7 @@ public class Lint { /** * Warn about unchecked operations on raw types. */ - RAW("rawtypes"), + RAWTYPES("rawtypes"), /** * Warn about use of deprecated-for-removal items. diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Instrumentation.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Instrumentation.java index dbafca4ed3c..e06d361b203 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Instrumentation.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Instrumentation.java @@ -58,7 +58,13 @@ final class Instrumentation { } public void addMethod(long methodId, String name, String signature, int modification) { - modificationMap.put(name + signature, new Method(methodId, Modification.valueOf(modification), className + "::" + name)); + Method method = new Method( + methodId, + Modification.valueOf(modification), + name.equals(""), + className + "::" + name + ); + modificationMap.put(name + signature, method); } public List getMethods() { @@ -71,7 +77,7 @@ final class Instrumentation { ClassModel classModel = classFile.parse(bytecode); byte[] generated = classFile.build(classModel.thisClass().asSymbol(), classBuilder -> { for (var ce : classModel) { - if (modifyClassElement(classBuilder, ce)) { + if (modifyClassElement(classModel, classBuilder, ce)) { modified[0] = true; } else { classBuilder.with(ce); @@ -93,7 +99,7 @@ final class Instrumentation { } } - private boolean modifyClassElement(ClassBuilder classBuilder, ClassElement ce) { + private boolean modifyClassElement(ClassModel classModel, ClassBuilder classBuilder, ClassElement ce) { if (ce instanceof MethodModel mm) { String method = mm.methodName().stringValue(); String signature = mm.methodType().stringValue(); @@ -102,15 +108,15 @@ final class Instrumentation { if (tm != null) { Modification m = tm.modification(); if (m.tracing() || m.timing()) { - return modifyMethod(classBuilder, mm, tm); + return modifyMethod(classModel, classBuilder, mm, tm); } } } return false; } - private boolean modifyMethod(ClassBuilder classBuilder, MethodModel m, Method method) { - var code = m.code(); + private boolean modifyMethod(ClassModel classModel, ClassBuilder classBuilder, MethodModel methodModel, Method method) { + var code = methodModel.code(); if (code.isPresent()) { if (classLoader == null && ExcludeList.containsMethod(method.name())) { String msg = "Risk of recursion, skipping bytecode generation of " + method.name(); @@ -118,9 +124,9 @@ final class Instrumentation { return false; } MethodTransform s = MethodTransform.ofStateful( - () -> MethodTransform.transformingCode(new Transform(method)) + () -> MethodTransform.transformingCode(new Transform(classModel, code.get(), method)) ); - classBuilder.transformMethod(m, s); + classBuilder.transformMethod(methodModel, s); return true; } return false; diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Method.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Method.java index d685083153d..d85e458e9d5 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Method.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Method.java @@ -31,7 +31,7 @@ import jdk.jfr.internal.Logger; /** * Class that holds information about an instrumented method. */ -record Method(long methodId, Modification modification, String name) { +record Method(long methodId, Modification modification, boolean constructor, String name) { @Override public String toString() { return name + (modification.timing() ? " +timing" : " -timing") + (modification.tracing() ? " +tracing" : " -tracing") + " (Method ID: " + String.format("0x%08X)", methodId); diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Transform.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Transform.java index cd65a119cee..377eede7925 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Transform.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/tracing/Transform.java @@ -24,13 +24,19 @@ */ package jdk.jfr.internal.tracing; +import java.lang.classfile.ClassModel; import java.lang.classfile.CodeBuilder; import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeModel; import java.lang.classfile.CodeTransform; +import java.lang.classfile.Label; import java.lang.classfile.TypeKind; +import java.lang.classfile.instruction.InvokeInstruction; import java.lang.classfile.instruction.ReturnInstruction; import java.lang.classfile.instruction.ThrowInstruction; import java.lang.constant.ClassDesc; +import java.util.ArrayList; +import java.util.List; import jdk.jfr.internal.util.Bytecode; import jdk.jfr.internal.util.Bytecode.MethodDesc; @@ -43,59 +49,208 @@ import jdk.jfr.tracing.MethodTracer; * The method ID is determined by native code. */ final class Transform implements CodeTransform { + private static class TryBlock { + Label start; + Label end; + } private static final ClassDesc METHOD_TRACER_CLASS = ClassDesc.of(MethodTracer.class.getName()); private static final MethodDesc TRACE_METHOD = MethodDesc.of("trace", "(JJ)V"); private static final MethodDesc TIMING_METHOD = MethodDesc.of("timing", "(JJ)V"); private static final MethodDesc TRACE_TIMING_METHOD = MethodDesc.of("traceTiming", "(JJ)V"); private static final MethodDesc TIMESTAMP_METHOD = MethodDesc.of("timestamp", "()J"); + private final List tryBlocks = new ArrayList<>(); + private final boolean simplifiedInstrumentation; + private final ClassModel classModel; private final Method method; private int timestampSlot = -1; - Transform(Method method) { + Transform(ClassModel classModel, CodeModel model, Method method) { this.method = method; + this.classModel = classModel; + // The JVMS (not the JLS) allows multiple mutually exclusive super/this. + // invocations in a constructor body as long as only one lies on any given + // execution path. For example, this is valid bytecode: + // + // Foo(boolean value) { + // if (value) { + // staticMethodThatMayThrow(); + // super(); + // } else { + // try { + // if (value == 0) { + // throw new Exception(""); + // } + // } catch (Throwable t) { + // throw t; + // } + // super(); + // } + // } + // + // If such a method is found, instrumentation falls back to instrumenting only + // RET and ATHROW. This can cause exceptions to be missed or counted twice. + // + // An effect of this heuristic is that constructors like the one below + // will also trigger simplified instrumentation. + // + // class Bar { + // } + // + // class Foo extends Bar { + // Foo() { + // new Bar(); + // } + // } + // + // java.lang.Object:: with zero constructor invocations should use simplified instrumentation + this.simplifiedInstrumentation = method.constructor() && constructorInvocations(model.elementList()) != 1; + } + + private int constructorInvocations(List elementList) { + int count = 0; + for (CodeElement e : elementList) { + if (isConstructorInvocation(e)) { + count++; + } + } + return count; + } + + private boolean isConstructorInvocation(CodeElement element) { + if (element instanceof InvokeInstruction inv && inv.name().equalsString("")) { + if (classModel.thisClass().equals(inv.owner())) { + return true; + } + if (classModel.superclass().isPresent()) { + return classModel.superclass().get().equals(inv.owner()); + } + } + return false; } @Override - public final void accept(CodeBuilder builder, CodeElement element) { + public void accept(CodeBuilder builder, CodeElement element) { + if (simplifiedInstrumentation) { + acceptSimplifiedInstrumentation(builder, element); + return; + } + if (method.constructor()) { + acceptConstructor(builder, element, isConstructorInvocation(element)); + } else { + acceptMethod(builder, element); + } + } + + @Override + public void atEnd(CodeBuilder builder) { + endTryBlock(builder); + for (TryBlock block : tryBlocks) { + addCatchHandler(block, builder); + } + } + + private void acceptConstructor(CodeBuilder builder, CodeElement element, boolean isConstructorInvocation) { + if (timestampSlot == -1) { + timestampSlot = invokeTimestamp(builder); + builder.lstore(timestampSlot); + if (!isConstructorInvocation) { + beginTryBlock(builder); + } + } + if (isConstructorInvocation) { + endTryBlock(builder); + builder.with(element); + beginTryBlock(builder); + return; + } + if (element instanceof ReturnInstruction) { + addTracing(builder); + } + builder.with(element); + } + + private void endTryBlock(CodeBuilder builder) { + if (tryBlocks.isEmpty()) { + return; + } + TryBlock last = tryBlocks.getLast(); + if (last.end == null) { + last.end = builder.newBoundLabel(); + } + } + + private void beginTryBlock(CodeBuilder builder) { + TryBlock block = new TryBlock(); + block.start = builder.newBoundLabel(); + tryBlocks.add(block); + } + + private void acceptSimplifiedInstrumentation(CodeBuilder builder, CodeElement element) { if (timestampSlot == -1) { timestampSlot = invokeTimestamp(builder); builder.lstore(timestampSlot); } if (element instanceof ReturnInstruction || element instanceof ThrowInstruction) { - builder.lload(timestampSlot); - builder.ldc(method.methodId()); - Modification modification = method.modification(); - boolean objectInit = method.name().equals("java.lang.Object::"); - String suffix = objectInit ? "ObjectInit" : ""; - if (modification.timing()) { - if (modification.tracing()) { - invokeTraceTiming(builder, suffix); - } else { - invokeTiming(builder, suffix); - } - } else { - if (modification.tracing()) { - invokeTrace(builder, suffix); - } - } + addTracing(builder); } builder.with(element); } - public static void invokeTiming(CodeBuilder builder, String suffix) { + private void acceptMethod(CodeBuilder builder, CodeElement element) { + if (timestampSlot == -1) { + timestampSlot = invokeTimestamp(builder); + builder.lstore(timestampSlot); + beginTryBlock(builder); + } + if (element instanceof ReturnInstruction) { + addTracing(builder); + } + builder.with(element); + } + + private void addCatchHandler(TryBlock block, CodeBuilder builder) { + Label catchHandler = builder.newBoundLabel(); + int exceptionSlot = builder.allocateLocal(TypeKind.REFERENCE); + builder.astore(exceptionSlot); + addTracing(builder); + builder.aload(exceptionSlot); + builder.athrow(); + builder.exceptionCatchAll(block.start, block.end, catchHandler); + } + + private void addTracing(CodeBuilder builder) { + builder.lload(timestampSlot); + builder.ldc(method.methodId()); + Modification modification = method.modification(); + boolean objectInit = method.name().equals("java.lang.Object::"); + String suffix = objectInit ? "ObjectInit" : ""; + if (modification.timing()) { + if (modification.tracing()) { + invokeTraceTiming(builder, suffix); + } else { + invokeTiming(builder, suffix); + } + } else { + if (modification.tracing()) { + invokeTrace(builder, suffix); + } + } + } + + private static void invokeTiming(CodeBuilder builder, String suffix) { builder.invokestatic(METHOD_TRACER_CLASS, TIMING_METHOD.name() + suffix, TIMING_METHOD.descriptor()); } - public static void invokeTrace(CodeBuilder builder, String suffix) { + private static void invokeTrace(CodeBuilder builder, String suffix) { builder.invokestatic(METHOD_TRACER_CLASS, TRACE_METHOD.name() + suffix, TRACE_METHOD.descriptor()); } - public static void invokeTraceTiming(CodeBuilder builder, String suffix) { + private static void invokeTraceTiming(CodeBuilder builder, String suffix) { builder.invokestatic(METHOD_TRACER_CLASS, TRACE_TIMING_METHOD.name() + suffix, TRACE_TIMING_METHOD.descriptor()); } - public static int invokeTimestamp(CodeBuilder builder) { + private static int invokeTimestamp(CodeBuilder builder) { Bytecode.invokestatic(builder, METHOD_TRACER_CLASS, TIMESTAMP_METHOD); return builder.allocateLocal(TypeKind.LONG); } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java index dbaa5e3eec6..523b6c4821c 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/DesktopIntegration.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -50,6 +50,7 @@ import jdk.jpackage.internal.model.LinuxLauncher; import jdk.jpackage.internal.model.LinuxPackage; import jdk.jpackage.internal.model.Package; import jdk.jpackage.internal.util.CompositeProxy; +import jdk.jpackage.internal.util.Enquoter; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.XmlUtils; diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java index 55200b908cd..6faacbca528 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LibProvidersLookup.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -27,13 +27,12 @@ package jdk.jpackage.internal; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; import java.util.Collection; -import java.util.Objects; import java.util.Collections; -import java.util.Set; -import java.util.ArrayList; import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -48,9 +47,6 @@ public final class LibProvidersLookup { return (new ToolValidator(TOOL_LDD).validate() == null); } - public LibProvidersLookup() { - } - LibProvidersLookup setPackageLookup(PackageLookup v) { packageLookup = v; return this; @@ -87,23 +83,20 @@ public final class LibProvidersLookup { } private static List getNeededLibsForFile(Path path) throws IOException { - List result = new ArrayList<>(); - int ret = Executor.of(TOOL_LDD, path.toString()).setOutputConsumer(lines -> { - lines.map(line -> { - Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line); - if (matcher.find()) { - return matcher.group(1); - } - return null; - }).filter(Objects::nonNull).map(Path::of).forEach(result::add); - }).execute(); + final var result = Executor.of(TOOL_LDD, path.toString()).saveOutput().execute(); - if (ret != 0) { + if (result.getExitCode() != 0) { // objdump failed. This is OK if the tool was applied to not a binary file return Collections.emptyList(); } - return result; + return result.stdout().stream().map(line -> { + Matcher matcher = LIB_IN_LDD_OUTPUT_REGEX.matcher(line); + if (matcher.find()) { + return matcher.group(1); + } + return null; + }).filter(Objects::nonNull).map(Path::of).toList(); } private static Collection getNeededLibsForFiles(List paths) { diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java index 6e438e66a26..d2169ede461 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxBundlingEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -33,6 +33,7 @@ import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_LINUX_R import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import jdk.jpackage.internal.cli.Options; import jdk.jpackage.internal.cli.StandardBundlingOperation; @@ -44,19 +45,34 @@ import jdk.jpackage.internal.util.Result; public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment { public LinuxBundlingEnvironment() { - super(build() - .defaultOperation(() -> { - return LazyLoad.SYS_ENV.value().map(LinuxSystemEnvironment::nativePackageType).map(DESCRIPTORS::get); - }) - .bundler(CREATE_LINUX_APP_IMAGE, LinuxBundlingEnvironment::createAppImage) - .bundler(CREATE_LINUX_DEB, LazyLoad::debSysEnv, LinuxBundlingEnvironment::createDebPackage) - .bundler(CREATE_LINUX_RPM, LazyLoad::rpmSysEnv, LinuxBundlingEnvironment::createRpmPackage)); + super(build().mutate(builder -> { + + // Wrap the generic Linux system environment supplier in the run-once wrapper + // as this supplier is called from both RPM and DEB Linux system environment suppliers. + var sysEnv = runOnce(() -> { + return LinuxSystemEnvironment.create(); + }); + + Supplier> debSysEnv = () -> { + return LinuxDebSystemEnvironment.create(sysEnv.get()); + }; + + Supplier> rpmSysEnv = () -> { + return LinuxRpmSystemEnvironment.create(sysEnv.get()); + }; + + builder.defaultOperation(() -> { + return sysEnv.get().value().map(LinuxSystemEnvironment::nativePackageType).map(DESCRIPTORS::get); + }) + .bundler(CREATE_LINUX_DEB, debSysEnv, LinuxBundlingEnvironment::createDebPackage) + .bundler(CREATE_LINUX_RPM, rpmSysEnv, LinuxBundlingEnvironment::createRpmPackage); + }).bundler(CREATE_LINUX_APP_IMAGE, LinuxBundlingEnvironment::createAppImage)); } private static void createDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) { createNativePackage(options, - LinuxFromOptions.createLinuxDebPackage(options), + LinuxFromOptions.createLinuxDebPackage(options, sysEnv), buildEnv()::create, LinuxBundlingEnvironment::buildPipeline, (env, pkg, outputDir) -> { @@ -67,7 +83,7 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment { private static void createRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) { createNativePackage(options, - LinuxFromOptions.createLinuxRpmPackage(options), + LinuxFromOptions.createLinuxRpmPackage(options, sysEnv), buildEnv()::create, LinuxBundlingEnvironment::buildPipeline, (env, pkg, outputDir) -> { @@ -90,23 +106,6 @@ public class LinuxBundlingEnvironment extends DefaultBundlingEnvironment { return new BuildEnvFromOptions().predefinedAppImageLayout(APPLICATION_LAYOUT); } - private static final class LazyLoad { - - static Result debSysEnv() { - return DEB_SYS_ENV; - } - - static Result rpmSysEnv() { - return RPM_SYS_ENV; - } - - private static final Result SYS_ENV = LinuxSystemEnvironment.create(); - - private static final Result DEB_SYS_ENV = LinuxDebSystemEnvironment.create(SYS_ENV); - - private static final Result RPM_SYS_ENV = LinuxRpmSystemEnvironment.create(SYS_ENV); - } - private static final Map DESCRIPTORS = Stream.of( CREATE_LINUX_DEB, CREATE_LINUX_RPM diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java index 64a0368e9a0..0ec6a77e683 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebPackager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -25,7 +25,6 @@ package jdk.jpackage.internal; -import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import java.io.IOException; @@ -76,11 +75,11 @@ final class LinuxDebPackager extends LinuxPackager { try { // Try the real path first as it works better on newer Ubuntu versions - return findProvidingPackages(realPath, sysEnv.dpkg()); + return findProvidingPackages(realPath, sysEnv); } catch (IOException ex) { // Try the default path if differ if (!realPath.equals(file)) { - return findProvidingPackages(file, sysEnv.dpkg()); + return findProvidingPackages(file, sysEnv); } else { throw ex; } @@ -107,7 +106,7 @@ final class LinuxDebPackager extends LinuxPackager { properties.forEach(property -> cmdline.add(property.name)); - Map actualValues = Executor.of(cmdline.toArray(String[]::new)) + Map actualValues = Executor.of(cmdline) .saveOutput(true) .executeExpectSuccess() .getOutput().stream() @@ -158,9 +157,8 @@ final class LinuxDebPackager extends LinuxPackager { cmdline.addAll(List.of("-b", env.appImageDir().toString(), debFile.toAbsolutePath().toString())); // run dpkg - RetryExecutor.retryOnKnownErrorMessage( - "semop(1): encountered an error: Invalid argument").execute( - cmdline.toArray(String[]::new)); + Executor.of(cmdline).retryOnKnownErrorMessage( + "semop(1): encountered an error: Invalid argument").execute(); Log.verbose(I18N.format("message.output-to-location", debFile.toAbsolutePath())); } @@ -233,7 +231,7 @@ final class LinuxDebPackager extends LinuxPackager { } } - private static Stream findProvidingPackages(Path file, Path dpkg) throws IOException { + private static Stream findProvidingPackages(Path file, LinuxDebSystemEnvironment sysEnv) throws IOException { // // `dpkg -S` command does glob pattern lookup. If not the absolute path // to the file is specified it might return mltiple package names. @@ -279,9 +277,9 @@ final class LinuxDebPackager extends LinuxPackager { Set archPackages = new HashSet<>(); Set otherPackages = new HashSet<>(); - var debArch = LinuxPackageArch.getValue(LINUX_DEB); + var debArch = sysEnv.packageArch().value(); - Executor.of(dpkg.toString(), "-S", file.toString()) + Executor.of(sysEnv.dpkg().toString(), "-S", file.toString()) .saveOutput(true).executeExpectSuccess() .getOutput().forEach(line -> { Matcher matcher = PACKAGE_NAME_REGEX.matcher(line); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java index 5b5decb7a67..d5480361452 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -28,7 +28,7 @@ import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin; import jdk.jpackage.internal.util.Result; -public interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin { +interface LinuxDebSystemEnvironment extends LinuxSystemEnvironment, LinuxDebSystemEnvironmentMixin { static Result create(Result base) { return mixin(LinuxDebSystemEnvironment.class, base, LinuxDebSystemEnvironmentMixin::create); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java index 8688327b353..2cf3e9e36e8 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxDebSystemEnvironmentMixin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -29,7 +29,7 @@ import java.util.Objects; import java.util.stream.Stream; import jdk.jpackage.internal.util.Result; -public interface LinuxDebSystemEnvironmentMixin { +interface LinuxDebSystemEnvironmentMixin { Path dpkg(); Path dpkgdeb(); Path fakeroot(); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java index 799c92ce2e1..0791c79c662 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxFromOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -70,9 +70,9 @@ final class LinuxFromOptions { return LinuxApplication.create(appBuilder.create()); } - static LinuxRpmPackage createLinuxRpmPackage(Options options) { + static LinuxRpmPackage createLinuxRpmPackage(Options options, LinuxRpmSystemEnvironment sysEnv) { - final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_RPM); + final var superPkgBuilder = createLinuxPackageBuilder(options, sysEnv, LINUX_RPM); final var pkgBuilder = new LinuxRpmPackageBuilder(superPkgBuilder); @@ -81,9 +81,9 @@ final class LinuxFromOptions { return pkgBuilder.create(); } - static LinuxDebPackage createLinuxDebPackage(Options options) { + static LinuxDebPackage createLinuxDebPackage(Options options, LinuxDebSystemEnvironment sysEnv) { - final var superPkgBuilder = createLinuxPackageBuilder(options, LINUX_DEB); + final var superPkgBuilder = createLinuxPackageBuilder(options, sysEnv, LINUX_DEB); final var pkgBuilder = new LinuxDebPackageBuilder(superPkgBuilder); @@ -99,7 +99,7 @@ final class LinuxFromOptions { return pkg; } - private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, StandardPackageType type) { + private static LinuxPackageBuilder createLinuxPackageBuilder(Options options, LinuxSystemEnvironment sysEnv, StandardPackageType type) { final var app = createLinuxApplication(options); @@ -107,6 +107,8 @@ final class LinuxFromOptions { final var pkgBuilder = new LinuxPackageBuilder(superPkgBuilder); + pkgBuilder.arch(sysEnv.packageArch()); + LINUX_PACKAGE_DEPENDENCIES.ifPresentIn(options, pkgBuilder::additionalDependencies); LINUX_APP_CATEGORY.ifPresentIn(options, pkgBuilder::category); LINUX_MENU_GROUP.ifPresentIn(options, pkgBuilder::menuGroupName); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java index b14404d67b1..40ff26bcfac 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxLaunchersAsServices.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.Package; +import jdk.jpackage.internal.util.Enquoter; /** * Helper to install launchers as services using "systemd". diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java index 836d1fb2c37..b1df92ae312 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageArch.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -25,18 +25,20 @@ package jdk.jpackage.internal; import java.io.IOException; -import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import java.util.ArrayList; import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.CommandOutputControl; +import jdk.jpackage.internal.util.Result; -final class LinuxPackageArch { +record LinuxPackageArch(String value) { - static String getValue(StandardPackageType pkgType) { + static Result create(StandardPackageType pkgType) { switch (pkgType) { case LINUX_RPM -> { - return RpmPackageArch.VALUE; + return rpm().map(LinuxPackageArch::new); } case LINUX_DEB -> { - return DebPackageArch.VALUE; + return deb().map(LinuxPackageArch::new); } default -> { throw new IllegalArgumentException(); @@ -44,62 +46,51 @@ final class LinuxPackageArch { } } - private static class DebPackageArch { - - static final String VALUE = toSupplier(DebPackageArch::getValue).get(); - - private static String getValue() throws IOException { - return Executor.of("dpkg", "--print-architecture").saveOutput(true) - .executeExpectSuccess().getOutput().get(0); - } + private static Result deb() { + var exec = Executor.of("dpkg", "--print-architecture").saveOutput(true); + return Result.of(exec::executeExpectSuccess, IOException.class) + .flatMap(LinuxPackageArch::getStdoutFirstLine); } - private static class RpmPackageArch { - - /* - * Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is mandatory for - * rpm packaging, try it first. rpm is optional and may not be available, use as the last - * resort. - */ - private static enum RpmArchReader { - Rpmbuild("rpmbuild", "--eval=%{_target_cpu}"), - Rpm("rpm", "--eval=%{_target_cpu}"); - - RpmArchReader(String... cmdline) { - this.cmdline = cmdline; + private static Result rpm() { + var errors = new ArrayList(); + for (var tool : RpmArchReader.values()) { + var result = tool.getRpmArch(); + if (result.hasValue()) { + return result; + } else { + errors.addAll(result.errors()); } - - String getRpmArch() throws IOException { - Executor exec = Executor.of(cmdline).saveOutput(true); - switch (this) { - case Rpm -> { - exec.executeExpectSuccess(); - } - case Rpmbuild -> { - if (exec.execute() != 0) { - return null; - } - } - default -> { - throw new UnsupportedOperationException(); - } - } - return exec.getOutput().get(0); - } - - private final String[] cmdline; } - static final String VALUE = toSupplier(RpmPackageArch::getValue).get(); + return Result.ofErrors(errors); + } - private static String getValue() throws IOException { - for (var rpmArchReader : RpmArchReader.values()) { - var rpmArchStr = rpmArchReader.getRpmArch(); - if (rpmArchStr != null) { - return rpmArchStr; - } - } - throw new RuntimeException("error.rpm-arch-not-detected"); + /* + * Various ways to get rpm arch. Needed to address JDK-8233143. rpmbuild is mandatory for + * rpm packaging, try it first. rpm is optional and may not be available, use as the last + * resort. + */ + private enum RpmArchReader { + RPMBUILD("rpmbuild", "--eval=%{_target_cpu}"), + RPM("rpm", "--eval=%{_target_cpu}"); + + RpmArchReader(String... cmdline) { + this.cmdline = cmdline; } + + Result getRpmArch() { + var exec = Executor.of(cmdline).saveOutput(true); + return Result.of(exec::executeExpectSuccess, IOException.class) + .flatMap(LinuxPackageArch::getStdoutFirstLine); + } + + private final String[] cmdline; + } + + private static Result getStdoutFirstLine(CommandOutputControl.Result result) { + return Result.of(() -> { + return result.stdout().stream().findFirst().orElseThrow(result::unexpected); + }, IOException.class); } } diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBuilder.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBuilder.java index bc7c301ace2..cd4d674432e 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBuilder.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxPackageBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -83,7 +83,7 @@ final class LinuxPackageBuilder { category(), Optional.ofNullable(additionalDependencies), release(), - pkg.asStandardPackageType().map(LinuxPackageArch::getValue).orElseThrow())); + arch.value())); } LinuxPackageBuilder literalName(String v) { @@ -119,6 +119,11 @@ final class LinuxPackageBuilder { return Optional.ofNullable(release); } + LinuxPackageBuilder arch(LinuxPackageArch v) { + arch = v; + return this; + } + private static LinuxApplicationLayout usrTreePackageLayout(Path prefix, String packageName) { final var lib = prefix.resolve(Path.of("lib", packageName)); return LinuxApplicationLayout.create( @@ -184,6 +189,7 @@ final class LinuxPackageBuilder { private String category; private String additionalDependencies; private String release; + private LinuxPackageArch arch; private final PackageBuilder pkgBuilder; diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java index 58c10668227..e56551be325 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -28,7 +28,7 @@ import static jdk.jpackage.internal.LinuxSystemEnvironment.mixin; import jdk.jpackage.internal.util.Result; -public interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin { +interface LinuxRpmSystemEnvironment extends LinuxSystemEnvironment, LinuxRpmSystemEnvironmentMixin { static Result create(Result base) { return mixin(LinuxRpmSystemEnvironment.class, base, LinuxRpmSystemEnvironmentMixin::create); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java index b741495f5ed..4cbd3ce4a9c 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxRpmSystemEnvironmentMixin.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -32,7 +32,7 @@ import java.util.stream.Stream; import jdk.jpackage.internal.model.DottedVersion; import jdk.jpackage.internal.util.Result; -public interface LinuxRpmSystemEnvironmentMixin { +interface LinuxRpmSystemEnvironmentMixin { Path rpm(); Path rpmbuild(); diff --git a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java index 1a70cc938b8..e347c58ae21 100644 --- a/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java +++ b/src/jdk.jpackage/linux/classes/jdk/jpackage/internal/LinuxSystemEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -27,7 +27,6 @@ package jdk.jpackage.internal; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; import jdk.jpackage.internal.model.PackageType; @@ -35,9 +34,10 @@ import jdk.jpackage.internal.model.StandardPackageType; import jdk.jpackage.internal.util.CompositeProxy; import jdk.jpackage.internal.util.Result; -public interface LinuxSystemEnvironment extends SystemEnvironment { +interface LinuxSystemEnvironment extends SystemEnvironment { boolean soLookupAvailable(); PackageType nativePackageType(); + LinuxPackageArch packageArch(); static Result create() { return detectNativePackageType().map(LinuxSystemEnvironment::create).orElseGet(() -> { @@ -45,7 +45,7 @@ public interface LinuxSystemEnvironment extends SystemEnvironment { }); } - static Optional detectNativePackageType() { + static Optional detectNativePackageType() { if (Internal.isDebian()) { return Optional.of(StandardPackageType.LINUX_DEB); } else if (Internal.isRpm()) { @@ -55,13 +55,14 @@ public interface LinuxSystemEnvironment extends SystemEnvironment { } } - static Result create(PackageType nativePackageType) { - return Result.ofValue(new Stub(LibProvidersLookup.supported(), - Objects.requireNonNull(nativePackageType))); + static Result create(StandardPackageType nativePackageType) { + return LinuxPackageArch.create(nativePackageType).map(arch -> { + return new Stub(LibProvidersLookup.supported(), nativePackageType, arch); + }); } static U createWithMixin(Class type, LinuxSystemEnvironment base, T mixin) { - return CompositeProxy.create(type, base, mixin); + return CompositeProxy.build().invokeTunnel(CompositeProxyTunnel.INSTANCE).create(type, base, mixin); } static Result mixin(Class type, @@ -79,7 +80,7 @@ public interface LinuxSystemEnvironment extends SystemEnvironment { } } - record Stub(boolean soLookupAvailable, PackageType nativePackageType) implements LinuxSystemEnvironment { + record Stub(boolean soLookupAvailable, PackageType nativePackageType, LinuxPackageArch packageArch) implements LinuxSystemEnvironment { } static final class Internal { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageInfoPListFile.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageInfoPListFile.java index 4787d1297bb..602e147a970 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageInfoPListFile.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageInfoPListFile.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -24,8 +24,6 @@ */ package jdk.jpackage.internal; -import static jdk.jpackage.internal.util.XmlUtils.initDocumentBuilder; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -50,7 +48,7 @@ record AppImageInfoPListFile(String bundleIdentifier, String bundleName, String static AppImageInfoPListFile loadFromInfoPList(Path infoPListFile) throws IOException, InvalidPlistFileException, SAXException { - final var plistReader = new PListReader(initDocumentBuilder().parse(Files.newInputStream(infoPListFile))); + final var plistReader = new PListReader(Files.readAllBytes(infoPListFile)); try { return new AppImageInfoPListFile( diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java index 71f87dd8705..81e04ad7ed1 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/AppImageSigner.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -48,6 +48,7 @@ import jdk.jpackage.internal.model.Launcher; import jdk.jpackage.internal.model.MacApplication; import jdk.jpackage.internal.model.RuntimeLayout; import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.internal.util.Result; import jdk.jpackage.internal.util.function.ExceptionBox; @@ -188,11 +189,9 @@ final class AppImageSigner { } private static boolean isXcodeDevToolsInstalled() { - try { - return Executor.of("/usr/bin/xcrun", "--help").setQuiet(true).execute() == 0; - } catch (IOException ex) { - return false; - } + return Result.of( + Executor.of("/usr/bin/xcrun", "--help").setQuiet(true)::executeExpectSuccess, + IOException.class).hasValue(); } private static void unsign(Path path) throws IOException { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java index 920b75df398..a7cd17b06b9 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/Codesign.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -34,7 +34,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; -import java.util.stream.Stream; public final class Codesign { @@ -94,14 +93,12 @@ public final class Codesign { public void applyTo(Path path) throws IOException, CodesignException { - var exec = Executor.of(Stream.concat( - cmdline.stream(), - Stream.of(path.toString())).toArray(String[]::new) - ).saveOutput(true); + var exec = Executor.of(cmdline).args(path.toString()).saveOutput(true); configureExecutor.ifPresent(configure -> configure.accept(exec)); - if (exec.execute() != 0) { - throw new CodesignException(exec.getOutput().toArray(String[]::new)); + var result = exec.execute(); + if (result.getExitCode() != 0) { + throw new CodesignException(result.getOutput().toArray(String[]::new)); } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java index 3cecb2cd243..0531559e052 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacBundlingEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -36,7 +36,6 @@ import java.util.Optional; import jdk.jpackage.internal.cli.Options; import jdk.jpackage.internal.model.MacPackage; import jdk.jpackage.internal.model.Package; -import jdk.jpackage.internal.util.Result; public class MacBundlingEnvironment extends DefaultBundlingEnvironment { @@ -45,7 +44,7 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment { .defaultOperation(CREATE_MAC_DMG) .bundler(SIGN_MAC_APP_IMAGE, MacBundlingEnvironment::signAppImage) .bundler(CREATE_MAC_APP_IMAGE, MacBundlingEnvironment::createAppImage) - .bundler(CREATE_MAC_DMG, LazyLoad::dmgSysEnv, MacBundlingEnvironment::createDmdPackage) + .bundler(CREATE_MAC_DMG, MacDmgSystemEnvironment::create, MacBundlingEnvironment::createDmdPackage) .bundler(CREATE_MAC_PKG, MacBundlingEnvironment::createPkgPackage)); } @@ -98,13 +97,4 @@ public class MacBundlingEnvironment extends DefaultBundlingEnvironment { .predefinedAppImageLayout(APPLICATION_LAYOUT) .predefinedRuntimeImageLayout(MacPackage::guessRuntimeLayout); } - - private static final class LazyLoad { - - static Result dmgSysEnv() { - return DMG_SYS_ENV; - } - - private static final Result DMG_SYS_ENV = MacDmgSystemEnvironment.create(); - } } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java index fe593e347fc..24a236ae15d 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacCertificateUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -53,7 +53,7 @@ public final class MacCertificateUtils { keychain.map(Keychain::asCliArg).ifPresent(args::add); return toSupplier(() -> { - final var output = Executor.of(args.toArray(String[]::new)) + final var output = Executor.of(args) .setQuiet(true).saveOutput(true).executeExpectSuccess() .getOutput(); diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java index 4ccc459109f..20a687487ef 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgPackager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -33,11 +33,15 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.text.MessageFormat; +import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import java.util.function.Function; import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.MacDmgPackage; @@ -105,6 +109,10 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, return env.configDir().resolve(pkg.app().name() + "-license.plist"); } + private Path finalDmg() { + return outputDir.resolve(pkg.packageFileNameWithSuffix()); + } + Path protoDmg() { return dmgWorkdir().resolve("proto.dmg"); } @@ -128,6 +136,10 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, } } + private Executor hdiutil(String... args) { + return Executor.of(sysEnv.hdiutil().toString()).args(args).storeOutputInFiles(); + } + private void prepareDMGSetupScript() throws IOException { Path dmgSetup = volumeScript(); Log.verbose(MessageFormat.format( @@ -211,13 +223,17 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, } } + private String hdiUtilVerbosityFlag() { + return env.verbose() ? "-verbose" : "-quiet"; + } + private void buildDMG() throws IOException { boolean copyAppImage = false; - Path protoDMG = protoDmg(); - Path finalDMG = outputDir.resolve(pkg.packageFileNameWithSuffix()); + final Path protoDMG = protoDmg(); + final Path finalDMG = finalDmg(); - Path srcFolder = env.appImageDir(); + final Path srcFolder = env.appImageDir(); Log.verbose(MessageFormat.format(I18N.getString( "message.creating-dmg-file"), finalDMG.toAbsolutePath())); @@ -233,21 +249,17 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, Files.createDirectories(protoDMG.getParent()); Files.createDirectories(finalDMG.getParent()); - String hdiUtilVerbosityFlag = env.verbose() ? - "-verbose" : "-quiet"; + final String hdiUtilVerbosityFlag = hdiUtilVerbosityFlag(); // create temp image - ProcessBuilder pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "create", - hdiUtilVerbosityFlag, - "-srcfolder", normalizedAbsolutePathString(srcFolder), - "-volname", volumeName(), - "-ov", normalizedAbsolutePathString(protoDMG), - "-fs", "HFS+", - "-format", "UDRW"); try { - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); + hdiutil("create", + hdiUtilVerbosityFlag, + "-srcfolder", normalizedAbsolutePathString(srcFolder), + "-volname", volumeName(), + "-ov", normalizedAbsolutePathString(protoDMG), + "-fs", "HFS+", + "-format", "UDRW").executeExpectSuccess(); } catch (IOException ex) { Log.verbose(ex); // Log exception @@ -260,31 +272,26 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, // not be bigger, but it will able to hold additional 50 megabytes of data. // We need extra room for icons and background image. When we providing // actual files to hdiutil, it will create DMG with ~50 megabytes extra room. - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "create", - hdiUtilVerbosityFlag, - "-size", String.valueOf(size), - "-volname", volumeName(), - "-ov", normalizedAbsolutePathString(protoDMG), - "-fs", "HFS+"); - new RetryExecutor() - .setMaxAttemptsCount(10) - .setAttemptTimeoutMillis(3000) - .setWriteOutputToFile(true) - .execute(pb); + hdiutil( + "create", + hdiUtilVerbosityFlag, + "-size", String.valueOf(size), + "-volname", volumeName(), + "-ov", normalizedAbsolutePathString(protoDMG), + "-fs", "HFS+" + ).retry() + .setMaxAttemptsCount(10) + .setAttemptTimeout(3, TimeUnit.SECONDS) + .execute(); } + final Path mountedVolume = volumePath(); + // mount temp image - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "attach", + hdiutil("attach", normalizedAbsolutePathString(protoDMG), hdiUtilVerbosityFlag, - "-mountroot", protoDMG.getParent().toString()); - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); - - final Path mountedVolume = volumePath(); + "-mountroot", mountedVolume.getParent().toString()).executeExpectSuccess(); // Copy app image, since we did not create DMG with it, but instead we created // empty one. @@ -302,9 +309,13 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, // to install-dir in DMG as critical error, since it can fail in // headless environment. try { - pb = new ProcessBuilder(sysEnv.osascript().toString(), - normalizedAbsolutePathString(volumeScript())); - IOUtils.exec(pb, 180); // Wait 3 minutes. See JDK-8248248. + Executor.of( + sysEnv.osascript().toString(), + normalizedAbsolutePathString(volumeScript()) + ) + // Wait 3 minutes. See JDK-8248248. + .timeout(3, TimeUnit.MINUTES) + .executeExpectSuccess(); } catch (IOException ex) { Log.verbose(ex); } @@ -325,18 +336,18 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, // but it seems Finder excepts these bytes to be // "icnC" for the volume icon // (might not work on Mac 10.13 with old XCode) - pb = new ProcessBuilder( + Executor.of( sysEnv.setFileUtility().orElseThrow().toString(), "-c", "icnC", - normalizedAbsolutePathString(volumeIconFile)); - IOUtils.exec(pb); + normalizedAbsolutePathString(volumeIconFile) + ).executeExpectSuccess(); volumeIconFile.toFile().setReadOnly(); - pb = new ProcessBuilder( + Executor.of( sysEnv.setFileUtility().orElseThrow().toString(), "-a", "C", - normalizedAbsolutePathString(mountedVolume)); - IOUtils.exec(pb); + normalizedAbsolutePathString(mountedVolume) + ).executeExpectSuccess(); } catch (IOException ex) { Log.error(ex.getMessage()); Log.verbose("Cannot enable custom icon using SetFile utility"); @@ -347,85 +358,23 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, } finally { // Detach the temporary image - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "detach", - hdiUtilVerbosityFlag, - normalizedAbsolutePathString(mountedVolume)); - // "hdiutil detach" might not work right away due to resource busy error, so - // repeat detach several times. - RetryExecutor retryExecutor = new RetryExecutor(); - // Image can get detach even if we got resource busy error, so stop - // trying to detach it if it is no longer attached. - retryExecutor.setExecutorInitializer(exec -> { - if (!Files.exists(mountedVolume)) { - retryExecutor.abort(); - } - }); - try { - // 10 times with 6 second delays. - retryExecutor.setMaxAttemptsCount(10).setAttemptTimeoutMillis(6000) - .execute(pb); - } catch (IOException ex) { - if (!retryExecutor.isAborted()) { - // Now force to detach if it still attached - if (Files.exists(mountedVolume)) { - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "detach", - "-force", - hdiUtilVerbosityFlag, - normalizedAbsolutePathString(mountedVolume)); - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); - } - } - } + detachVolume(); } // Compress it to a new image - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "convert", - normalizedAbsolutePathString(protoDMG), - hdiUtilVerbosityFlag, - "-format", "UDZO", - "-o", normalizedAbsolutePathString(finalDMG)); - try { - new RetryExecutor() - .setMaxAttemptsCount(10) - .setAttemptTimeoutMillis(3000) - .execute(pb); - } catch (Exception ex) { - // Convert might failed if something holds file. Try to convert copy. - Path protoCopyDMG = protoCopyDmg(); - Files.copy(protoDMG, protoCopyDMG); - try { - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), - "convert", - normalizedAbsolutePathString(protoCopyDMG), - hdiUtilVerbosityFlag, - "-format", "UDZO", - "-o", normalizedAbsolutePathString(finalDMG)); - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); - } finally { - Files.deleteIfExists(protoCopyDMG); - } - } + convertProtoDmg(); //add license if needed if (pkg.licenseFile().isPresent()) { - pb = new ProcessBuilder( - sysEnv.hdiutil().toString(), + hdiutil( "udifrez", normalizedAbsolutePathString(finalDMG), "-xml", normalizedAbsolutePathString(licenseFile()) - ); - new RetryExecutor() - .setMaxAttemptsCount(10) - .setAttemptTimeoutMillis(3000) - .execute(pb); + ).retry() + .setMaxAttemptsCount(10) + .setAttemptTimeout(3, TimeUnit.SECONDS) + .execute(); } try { @@ -441,6 +390,69 @@ record MacDmgPackager(BuildEnv env, MacDmgPackage pkg, Path outputDir, } + private void detachVolume() throws IOException { + var mountedVolume = volumePath(); + + // "hdiutil detach" might not work right away due to resource busy error, so + // repeat detach several times. + Globals.instance().objectFactory().retryExecutor(IOException.class).setExecutable(context -> { + + List cmdline = new ArrayList<>(); + cmdline.add("detach"); + + if (context.isLastAttempt()) { + // The last attempt, force detach. + cmdline.add("-force"); + } + + cmdline.addAll(List.of( + hdiUtilVerbosityFlag(), + normalizedAbsolutePathString(mountedVolume) + )); + + // The image can get detached even if we get a resource busy error, + // so execute the detach command without checking the exit code. + var result = hdiutil(cmdline.toArray(String[]::new)).execute(); + + if (result.getExitCode() == 0 || !Files.exists(mountedVolume)) { + // Detached successfully! + return null; + } else { + throw result.unexpected(); + } + }).setMaxAttemptsCount(10).setAttemptTimeout(6, TimeUnit.SECONDS).execute(); + } + + private void convertProtoDmg() throws IOException { + + Function convert = srcDmg -> { + return hdiutil( + "convert", + normalizedAbsolutePathString(srcDmg), + hdiUtilVerbosityFlag(), + "-format", "UDZO", + "-o", normalizedAbsolutePathString(finalDmg())); + }; + + // Convert it to a new image. + try { + convert.apply(protoDmg()).retry() + .setMaxAttemptsCount(10) + .setAttemptTimeout(3, TimeUnit.SECONDS) + .execute(); + } catch (IOException ex) { + Log.verbose(ex); + // Something holds the file, try to convert a copy. + Path copyDmg = protoCopyDmg(); + Files.copy(protoDmg(), copyDmg); + try { + convert.apply(copyDmg).executeExpectSuccess(); + } finally { + Files.deleteIfExists(copyDmg); + } + } + } + // Background image name in resources private static final String DEFAULT_BACKGROUND_IMAGE = "background_dmg.tiff"; private static final String DEFAULT_DMG_SETUP_SCRIPT = "DMGsetup.scpt"; diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java index 54eb0c6f4fe..12d105b99b5 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacDmgSystemEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -25,10 +25,11 @@ package jdk.jpackage.internal; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.Result; @@ -54,41 +55,31 @@ record MacDmgSystemEnvironment(Path hdiutil, Path osascript, Optional setF // Location of SetFile utility may be different depending on MacOS version // We look for several known places and if none of them work will // try to find it - private static Optional findSetFileUtility() { - String typicalPaths[] = {"/Developer/Tools/SetFile", - "/usr/bin/SetFile", "/Developer/usr/bin/SetFile"}; + static Optional findSetFileUtility() { + return SETFILE_KNOWN_PATHS.stream().filter(setFilePath -> { + // Validate SetFile, if Xcode is not installed it will run, but exit with error code + return Result.of( + Executor.of(setFilePath.toString(), "-h").setQuiet(true)::executeExpectSuccess, + IOException.class).hasValue(); + }).findFirst().or(() -> { + // generic find attempt + final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile").setQuiet(true).saveFirstLineOfOutput(); - final var setFilePath = Stream.of(typicalPaths).map(Path::of).filter(Files::isExecutable).findFirst(); - if (setFilePath.isPresent()) { - // Validate SetFile, if Xcode is not installed it will run, but exit with error - // code - try { - if (Executor.of(setFilePath.orElseThrow().toString(), "-h").setQuiet(true).execute() == 0) { - return setFilePath; - } - } catch (Exception ignored) { - // No need for generic find attempt. We found it, but it does not work. - // Probably due to missing xcode. - return Optional.empty(); - } - } - - // generic find attempt - try { - final var executor = Executor.of("/usr/bin/xcrun", "-find", "SetFile"); - final var code = executor.setQuiet(true).saveOutput(true).execute(); - if (code == 0 && !executor.getOutput().isEmpty()) { - final var firstLine = executor.getOutput().getFirst(); - Path f = Path.of(firstLine); - if (new ToolValidator(f).checkExistsOnly().validate() == null) { - return Optional.of(f.toAbsolutePath()); - } - } - } catch (IOException ignored) {} - - return Optional.empty(); + return Result.of(executor::executeExpectSuccess, IOException.class).flatMap(execResult -> { + return Result.of(() -> { + return execResult.stdout().stream().findFirst().map(Path::of).orElseThrow(execResult::unexpected); + }, Exception.class); + }).value().filter(v -> { + return new ToolValidator(v).checkExistsOnly().validate() == null; + }).map(Path::toAbsolutePath); + }); } + static final List SETFILE_KNOWN_PATHS = Stream.of( + "/Developer/Tools/SetFile", + "/usr/bin/SetFile", + "/Developer/usr/bin/SetFile").map(Path::of).collect(Collectors.toUnmodifiableList()); + private static final Path HDIUTIL = Path.of("/usr/bin/hdiutil"); private static final Path OSASCRIPT = Path.of("/usr/bin/osascript"); } diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java index 127b3232661..126248e2330 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/MacPkgPackager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -32,7 +32,6 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.text.MessageFormat; @@ -57,6 +56,7 @@ import jdk.jpackage.internal.PackagingPipeline.PackageTaskID; import jdk.jpackage.internal.PackagingPipeline.TaskID; import jdk.jpackage.internal.model.MacPkgPackage; import jdk.jpackage.internal.resources.ResourceLocator; +import jdk.jpackage.internal.util.Enquoter; import jdk.jpackage.internal.util.XmlUtils; import org.xml.sax.SAXException; @@ -108,7 +108,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional servic cmdline.addAll(allPkgbuildArgs()); try { Files.createDirectories(path.getParent()); - IOUtils.exec(new ProcessBuilder(cmdline), false, null, true, Executor.INFINITE_TIMEOUT); + Executor.of(cmdline).executeExpectSuccess(); } catch (IOException ex) { throw new UncheckedIOException(ex); } @@ -487,15 +487,13 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional servic Files.createDirectories(cpl.getParent()); - final var pb = new ProcessBuilder("/usr/bin/pkgbuild", + Executor.of("/usr/bin/pkgbuild", "--root", normalizedAbsolutePathString(env.appImageDir()), "--install-location", normalizedAbsolutePathString(installLocation()), "--analyze", - normalizedAbsolutePathString(cpl)); - - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); + normalizedAbsolutePathString(cpl)).executeExpectSuccess(); patchCPLFile(cpl); } @@ -544,8 +542,7 @@ record MacPkgPackager(BuildEnv env, MacPkgPackage pkg, Optional servic } commandLine.add(normalizedAbsolutePathString(finalPkg)); - final var pb = new ProcessBuilder(commandLine); - IOUtils.exec(pb, false, null, true, Executor.INFINITE_TIMEOUT); + Executor.of(commandLine).executeExpectSuccess(); } private static Optional createServices(BuildEnv env, MacPkgPackage pkg) { diff --git a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/TempKeychain.java b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/TempKeychain.java index b38faecd96f..2f616aafba1 100644 --- a/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/TempKeychain.java +++ b/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/TempKeychain.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -27,15 +27,17 @@ package jdk.jpackage.internal; import java.io.Closeable; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import jdk.internal.util.OSVersion; -import jdk.jpackage.internal.util.function.ThrowingConsumer; final class TempKeychain implements Closeable { - static void withKeychains(ThrowingConsumer, ? extends Exception> keychainConsumer, List keychains) throws Exception { + static void withKeychains(Consumer> keychainConsumer, List keychains) { + keychains.forEach(Objects::requireNonNull); if (keychains.isEmpty() || OSVersion.current().compareTo(new OSVersion(10, 12)) < 0) { keychainConsumer.accept(keychains); @@ -43,11 +45,14 @@ final class TempKeychain implements Closeable { // we need this for OS X 10.12+ try (var tempKeychain = new TempKeychain(keychains)) { keychainConsumer.accept(tempKeychain.keychains); + } catch (IOException ex) { + throw new UncheckedIOException(ex); } } } - static void withKeychain(ThrowingConsumer keychainConsumer, Keychain keychain) throws Exception { + static void withKeychain(Consumer keychainConsumer, Keychain keychain) { + Objects.requireNonNull(keychainConsumer); withKeychains(keychains -> { keychainConsumer.accept(keychains.getFirst()); @@ -78,7 +83,7 @@ final class TempKeychain implements Closeable { args.addAll(missingKeychains.stream().map(Keychain::asCliArg).toList()); - Executor.of(args.toArray(String[]::new)).executeExpectSuccess(); + Executor.of(args).executeExpectSuccess(); } } @@ -89,7 +94,7 @@ final class TempKeychain implements Closeable { @Override public void close() throws IOException { if (!restoreKeychainsCmd.isEmpty()) { - Executor.of(restoreKeychainsCmd.toArray(String[]::new)).executeExpectSuccess(); + Executor.of(restoreKeychainsCmd).executeExpectSuccess(); } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java index 5f473b554be..75ead9d08ad 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/AppImageFile.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -28,11 +28,12 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toUnmodifiableMap; import static java.util.stream.Collectors.toUnmodifiableSet; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.APP_VERSION; -import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_AS_SERVICE; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.DESCRIPTION; +import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_AS_SERVICE; import static jdk.jpackage.internal.cli.StandardAppImageFileOption.LAUNCHER_NAME; import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -162,7 +163,23 @@ final class AppImageFile { final var relativeAppImageFilePath = appImageDir.relativize(appImageFilePath); try { - final Document doc = XmlUtils.initDocumentBuilder().parse(Files.newInputStream(appImageFilePath)); + // + // Use javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream). + // Don't use javax.xml.parsers.DocumentBuilder#parse(java.io.File) as this will introduce + // dependency on how the XML parser reports filesystem I/O errors. + // E.g.: the default JDK XML parser throws java.io.FileNotFoundException if the supplied + // directory is not found and throws org.xml.sax.SAXParseException if the supplied file is a directory. + // Another DOM XML parser (a different version of Xerces?) may behave differently. + // + // The use of javax.xml.parsers.DocumentBuilder#parse(java.io.InputStream) eliminates + // differences in how XML parsers handle file system I/O errors. + // Filesystem I/O is delegated to java.nio.file.Files#readAllBytes(java.nio.file.Path), + // XML parser deals with the byte stream in memory and the error handling code + // doesn't depend on how XML parser reports filesystem I/O errors because + // it reads data from memory, not from the filesystem. + // + final Document doc = XmlUtils.initDocumentBuilder().parse( + new ByteArrayInputStream(Files.readAllBytes(appImageFilePath))); final XPath xPath = XPathFactory.newInstance().newXPath(); diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java index 05e080f240a..e4473b1e5ce 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/DefaultBundlingEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -65,10 +65,10 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { Map>>> bundlers) { this.bundlers = bundlers.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> { - return new CachingSupplier<>(e.getValue()); + return runOnce(e.getValue()); })); - this.defaultOperationSupplier = Objects.requireNonNull(defaultOperationSupplier).map(CachingSupplier::new); + this.defaultOperationSupplier = Objects.requireNonNull(defaultOperationSupplier).map(DefaultBundlingEnvironment::runOnce); } @@ -98,6 +98,11 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { return bundler(op, () -> Result.ofValue(bundler)); } + Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + private Supplier> defaultOperationSupplier; private final Map>>> bundlers = new HashMap<>(); } @@ -107,6 +112,10 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { return new Builder(); } + static Supplier runOnce(Supplier supplier) { + return new CachingSupplier<>(supplier); + } + static Supplier>> createBundlerSupplier( Supplier> sysEnvResultSupplier, BiConsumer bundler) { Objects.requireNonNull(sysEnvResultSupplier); @@ -279,5 +288,5 @@ class DefaultBundlingEnvironment implements CliBundlingEnvironment { private final Map>>> bundlers; - private final Optional>> defaultOperationSupplier; + private final Optional>> defaultOperationSupplier; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java index dd1cc4a24b4..ca7a630b6d1 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Executor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -25,53 +25,153 @@ package jdk.jpackage.internal; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; +import java.io.PrintStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.spi.ToolProvider; import java.util.stream.Stream; +import jdk.jpackage.internal.util.CommandLineFormat; +import jdk.jpackage.internal.util.CommandOutputControl; +import jdk.jpackage.internal.util.CommandOutputControl.ProcessAttributes; +import jdk.jpackage.internal.util.CommandOutputControl.Result; +import jdk.jpackage.internal.util.RetryExecutor; +import jdk.jpackage.internal.util.function.ExceptionBox; -public final class Executor { +final class Executor { - Executor() { + static Executor of(String... cmdline) { + return of(List.of(cmdline)); } - Executor setOutputConsumer(Consumer> v) { - outputConsumer = v; - return this; + static Executor of(List cmdline) { + return of(new ProcessBuilder(cmdline)); + } + + static Executor of(ProcessBuilder pb) { + return Globals.instance().objectFactory().executor().processBuilder(pb); + } + + public Executor() { + commandOutputControl = new CommandOutputControl(); + args = new ArrayList<>(); + } + + private Executor(Executor other) { + commandOutputControl = other.commandOutputControl.copy(); + quietCommand = other.quietCommand; + args = new ArrayList<>(other.args); + processBuilder = other.processBuilder; + toolProvider = other.toolProvider; + timeout = other.timeout; + mapper = other.mapper; } Executor saveOutput(boolean v) { - saveOutput = v; + commandOutputControl.saveOutput(v); return this; } - Executor setWriteOutputToFile(boolean v) { - writeOutputToFile = v; + Executor saveOutput() { + return saveOutput(true); + } + + Executor saveFirstLineOfOutput() { + commandOutputControl.saveFirstLineOfOutput(); return this; } - Executor setTimeout(long v) { + Executor charset(Charset v) { + commandOutputControl.charset(v); + return this; + } + + Executor storeOutputInFiles(boolean v) { + commandOutputControl.storeOutputInFiles(v); + return this; + } + + Executor storeOutputInFiles() { + return storeOutputInFiles(true); + } + + Executor binaryOutput(boolean v) { + commandOutputControl.binaryOutput(v); + return this; + } + + Executor binaryOutput() { + return binaryOutput(true); + } + + Executor discardStdout(boolean v) { + commandOutputControl.discardStdout(v); + return this; + } + + Executor discardStdout() { + return discardStdout(true); + } + + Executor discardStderr(boolean v) { + commandOutputControl.discardStderr(v); + return this; + } + + Executor discardStderr() { + return discardStderr(true); + } + + Executor timeout(long v, TimeUnit unit) { + return timeout(Duration.of(v, unit.toChronoUnit())); + } + + Executor timeout(Duration v) { timeout = v; - if (timeout != INFINITE_TIMEOUT) { - // Redirect output to file if timeout is requested, otherwise we will - // reading until process ends and timeout will never be reached. - setWriteOutputToFile(true); - } return this; } - Executor setProcessBuilder(ProcessBuilder v) { - pb = v; + Executor toolProvider(ToolProvider v) { + toolProvider = Objects.requireNonNull(v); + processBuilder = null; return this; } - Executor setCommandLine(String... cmdline) { - return setProcessBuilder(new ProcessBuilder(cmdline)); + Optional toolProvider() { + return Optional.ofNullable(toolProvider); + } + + Executor processBuilder(ProcessBuilder v) { + processBuilder = Objects.requireNonNull(v); + toolProvider = null; + return this; + } + + Optional processBuilder() { + return Optional.ofNullable(processBuilder); + } + + Executor args(List v) { + args.addAll(v); + return this; + } + + Executor args(String... args) { + return args(List.of(args)); + } + + List args() { + return args; } Executor setQuiet(boolean v) { @@ -79,159 +179,207 @@ public final class Executor { return this; } - List getOutput() { - return output; - } - - Executor executeExpectSuccess() throws IOException { - int ret = execute(); - if (0 != ret) { - throw new IOException( - String.format("Command %s exited with %d code", - createLogMessage(pb, false), ret)); - } + Executor mapper(UnaryOperator v) { + mapper = v; return this; } - int execute() throws IOException { - output = null; + Optional> mapper() { + return Optional.ofNullable(mapper); + } - boolean needProcessOutput = outputConsumer != null || Log.isVerbose() || saveOutput; - Path outputFile = null; - if (needProcessOutput) { - pb.redirectErrorStream(true); - if (writeOutputToFile) { - outputFile = Files.createTempFile("jpackageOutputTempFile", ".tmp"); - pb.redirectOutput(outputFile.toFile()); + Executor copy() { + return new Executor(this); + } + + Result execute() throws IOException { + if (mapper != null) { + var mappedExecutor = Objects.requireNonNull(mapper.apply(this)); + if (mappedExecutor != this) { + return mappedExecutor.execute(); } + } + + var coc = commandOutputControl.copy(); + + final CommandOutputControl.Executable exec; + if (processBuilder != null) { + exec = coc.createExecutable(copyProcessBuilder()); + } else if (toolProvider != null) { + exec = coc.createExecutable(toolProvider, args.toArray(String[]::new)); } else { - // We are not going to read process output, so need to notify - // ProcessBuilder about this. Otherwise some processes might just - // hang up (`ldconfig -p`). - pb.redirectError(ProcessBuilder.Redirect.DISCARD); - pb.redirectOutput(ProcessBuilder.Redirect.DISCARD); + throw new IllegalStateException("No target to execute"); } - if (!quietCommand) { - Log.verbose(String.format("Running %s", createLogMessage(pb, true))); + PrintableOutputBuilder printableOutputBuilder; + if (dumpOutput()) { + printableOutputBuilder = new PrintableOutputBuilder(coc); + } else { + printableOutputBuilder = null; } - Process p = pb.start(); - - int code = 0; - if (writeOutputToFile) { - try { - code = waitForProcess(p); - } catch (InterruptedException ex) { - Log.verbose(ex); - throw new RuntimeException(ex); - } - } - - if (needProcessOutput) { - final List savedOutput; - Supplier> outputStream; - - if (writeOutputToFile) { - output = savedOutput = Files.readAllLines(outputFile); - Files.delete(outputFile); - outputStream = () -> { - if (savedOutput != null) { - return savedOutput.stream(); - } - return null; - }; - if (outputConsumer != null) { - outputConsumer.accept(outputStream.get()); - } - } else { - try (var br = new BufferedReader(new InputStreamReader( - p.getInputStream()))) { - - if ((outputConsumer != null || Log.isVerbose()) - || saveOutput) { - savedOutput = br.lines().toList(); - } else { - savedOutput = null; - } - output = savedOutput; - - outputStream = () -> { - if (savedOutput != null) { - return savedOutput.stream(); - } - return br.lines(); - }; - if (outputConsumer != null) { - outputConsumer.accept(outputStream.get()); - } - - if (savedOutput == null) { - // For some processes on Linux if the output stream - // of the process is opened but not consumed, the process - // would exit with code 141. - // It turned out that reading just a single line of process - // output fixes the problem, but let's process - // all of the output, just in case. - br.lines().forEach(x -> {}); - } - } - } + if (dumpOutput()) { + Log.verbose(String.format("Running %s", CommandLineFormat.DEFAULT.apply(List.of(commandLine().getFirst())))); } + Result result; try { - if (!writeOutputToFile) { - code = p.waitFor(); - } - if (!quietCommand) { - Log.verbose(pb.command(), getOutput(), code, IOUtils.getPID(p)); - } - return code; - } catch (InterruptedException ex) { - Log.verbose(ex); - throw new RuntimeException(ex); - } - } - - private int waitForProcess(Process p) throws InterruptedException { - if (timeout == INFINITE_TIMEOUT) { - return p.waitFor(); - } else { - if (p.waitFor(timeout, TimeUnit.SECONDS)) { - return p.exitValue(); + if (timeout == null) { + result = exec.execute(); } else { - Log.verbose(String.format("Command %s timeout after %d seconds", - createLogMessage(pb, false), timeout)); - p.destroy(); - return -1; + result = exec.execute(timeout.toMillis(), TimeUnit.MILLISECONDS); + } + } catch (InterruptedException ex) { + throw ExceptionBox.toUnchecked(ex); + } + + if (dumpOutput()) { + log(result, printableOutputBuilder.create()); + } + + return result; + } + + Result executeExpectSuccess() throws IOException { + return execute().expectExitCode(0); + } + + Result executeExpect(int mainExitCode, int... otherExitCodes) throws IOException { + return execute().expectExitCode(mainExitCode, otherExitCodes); + } + + RetryExecutor retry() { + return Globals.instance().objectFactory().retryExecutor(IOException.class) + .setExecutable(this::executeExpectSuccess); + } + + RetryExecutor retryOnKnownErrorMessage(String msg) { + Objects.requireNonNull(msg); + return saveOutput().retry().setExecutable(() -> { + // Execute it without exit code check. + var result = execute(); + if (result.stderr().stream().anyMatch(msg::equals)) { + throw result.unexpected(); + } + return result; + }); + } + + List commandLine() { + if (processBuilder != null) { + return Stream.of(processBuilder.command(), args).flatMap(Collection::stream).toList(); + } else if (toolProvider != null) { + return Stream.concat(Stream.of(toolProvider.name()), args.stream()).toList(); + } else { + throw new IllegalStateException("No target to execute"); + } + } + + private ProcessBuilder copyProcessBuilder() { + if (processBuilder == null) { + throw new IllegalStateException(); + } + + var copy = new ProcessBuilder(commandLine()); + copy.directory(processBuilder.directory()); + var env = copy.environment(); + env.clear(); + env.putAll(processBuilder.environment()); + + return copy; + } + + private boolean dumpOutput() { + return Log.isVerbose() && !quietCommand; + } + + private static void log(Result result, String printableOutput) throws IOException { + Objects.requireNonNull(result); + Objects.requireNonNull(printableOutput); + + Optional pid; + if (result.execAttrs() instanceof ProcessAttributes attrs) { + pid = attrs.pid(); + } else { + pid = Optional.empty(); + } + + var sb = new StringBuilder(); + sb.append("Command"); + pid.ifPresent(p -> { + sb.append(" [PID: ").append(p).append("]"); + }); + sb.append(":\n ").append(result.execAttrs()); + Log.verbose(sb.toString()); + + if (!printableOutput.isEmpty()) { + sb.delete(0, sb.length()); + sb.append("Output:"); + try (var lines = new BufferedReader(new StringReader(printableOutput)).lines()) { + lines.forEach(line -> { + sb.append("\n ").append(line); + }); + } + Log.verbose(sb.toString()); + } + + result.exitCode().ifPresentOrElse(exitCode -> { + Log.verbose("Returned: " + exitCode + "\n"); + }, () -> { + Log.verbose("Aborted: timed-out" + "\n"); + }); + } + + private static final class PrintableOutputBuilder { + + PrintableOutputBuilder(CommandOutputControl coc) { + coc.dumpOutput(true); + charset = coc.charset(); + if (coc.isBinaryOutput()) { + // Assume binary output goes into stdout and text error messages go into stderr, so keep them separated. + sinks = new ByteArrayOutputStream[2]; + sinks[0] = new ByteArrayOutputStream(); + sinks[1] = new ByteArrayOutputStream(); + coc.dumpStdout(new PrintStream(sinks[0], false, charset)) + .dumpStderr(new PrintStream(sinks[1], false, charset)); + } else { + sinks = new ByteArrayOutputStream[1]; + sinks[0] = new ByteArrayOutputStream(); + var ps = new PrintStream(sinks[0], false, charset); + // Redirect stderr in stdout. + coc.dumpStdout(ps).dumpStderr(ps); } } - } - static Executor of(String... cmdline) { - return new Executor().setCommandLine(cmdline); - } - - static Executor of(ProcessBuilder pb) { - return new Executor().setProcessBuilder(pb); - } - - private static String createLogMessage(ProcessBuilder pb, boolean quiet) { - StringBuilder sb = new StringBuilder(); - sb.append((quiet) ? pb.command().get(0) : pb.command()); - if (pb.directory() != null) { - sb.append(String.format(" in %s", pb.directory().getAbsolutePath())); + String create() { + if (isBinaryOutput()) { + // In case of binary output: + // - Convert binary stdout to text using ISO-8859-1 encoding and + // replace non-printable characters with the question mark symbol (?). + // - Convert binary stderr to text using designated encoding (assume stderr is always a character stream). + // - Merge text stdout and stderr into a single string; + // stderr first, stdout follows, with the aim to present user error messages first. + var sb = new StringBuilder(); + var stdout = sinks[0].toString(StandardCharsets.ISO_8859_1).replaceAll("[^\\p{Print}\\p{Space}]", "?"); + return sb.append(sinks[1].toString(charset)).append(stdout).toString(); + } else { + return sinks[0].toString(charset); + } } - return sb.toString(); + + private boolean isBinaryOutput() { + return sinks.length == 2; + } + + private final ByteArrayOutputStream sinks[]; + private final Charset charset; } - public static final int INFINITE_TIMEOUT = -1; - - private ProcessBuilder pb; - private boolean saveOutput; - private boolean writeOutputToFile; + private final CommandOutputControl commandOutputControl; private boolean quietCommand; - private long timeout = INFINITE_TIMEOUT; - private List output; - private Consumer> outputConsumer; + private final List args; + private ProcessBuilder processBuilder; + private ToolProvider toolProvider; + private Duration timeout; + private UnaryOperator mapper; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ExecutorFactory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ExecutorFactory.java new file mode 100644 index 00000000000..ce703358b82 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ExecutorFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal; + +@FunctionalInterface +interface ExecutorFactory { + + Executor executor(); + + static final ExecutorFactory DEFAULT = Executor::new; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java new file mode 100644 index 00000000000..c1b56b24e0a --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Globals.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal; + +import java.util.Optional; +import java.util.function.Supplier; + +public final class Globals { + + private Globals() { + } + + Globals objectFactory(ObjectFactory v) { + checkMutable(); + objectFactory = Optional.ofNullable(v).orElse(ObjectFactory.DEFAULT); + return this; + } + + ObjectFactory objectFactory() { + return objectFactory; + } + + Globals executorFactory(ExecutorFactory v) { + return objectFactory(ObjectFactory.build(objectFactory).executorFactory(v).create()); + } + + public static int main(Supplier mainBody) { + if (INSTANCE.isBound()) { + return mainBody.get(); + } else { + return ScopedValue.where(INSTANCE, new Globals()).call(mainBody::get); + } + } + + public static Globals instance() { + return INSTANCE.orElse(DEFAULT); + } + + private void checkMutable() { + if (this == DEFAULT) { + throw new UnsupportedOperationException("Can't modify immutable instance"); + } + } + + private ObjectFactory objectFactory = ObjectFactory.DEFAULT; + + private static final ScopedValue INSTANCE = ScopedValue.newInstance(); + private static final Globals DEFAULT = new Globals(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java index aac113d7777..08cf0c10982 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/IOUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -26,12 +26,9 @@ package jdk.jpackage.internal; import java.io.IOException; -import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.ArrayList; -import java.util.List; import jdk.jpackage.internal.model.JPackageException; /** @@ -50,46 +47,6 @@ final class IOUtils { StandardCopyOption.COPY_ATTRIBUTES); } - public static void exec(ProcessBuilder pb) - throws IOException { - exec(pb, false, null, false, Executor.INFINITE_TIMEOUT); - } - - // timeout in seconds. -1 will be return if process timeouts. - public static void exec(ProcessBuilder pb, long timeout) - throws IOException { - exec(pb, false, null, false, timeout); - } - - static void exec(ProcessBuilder pb, boolean testForPresenceOnly, - PrintStream consumer, boolean writeOutputToFile, long timeout) - throws IOException { - exec(pb, testForPresenceOnly, consumer, writeOutputToFile, - timeout, false); - } - - static void exec(ProcessBuilder pb, boolean testForPresenceOnly, - PrintStream consumer, boolean writeOutputToFile, - long timeout, boolean quiet) throws IOException { - List output = new ArrayList<>(); - Executor exec = Executor.of(pb) - .setWriteOutputToFile(writeOutputToFile) - .setTimeout(timeout) - .setQuiet(quiet) - .setOutputConsumer(lines -> { - lines.forEach(output::add); - if (consumer != null) { - output.forEach(consumer::println); - } - }); - - if (testForPresenceOnly) { - exec.execute(); - } else { - exec.executeExpectSuccess(); - } - } - static void writableOutputDir(Path outdir) { if (!Files.isDirectory(outdir)) { try { @@ -103,15 +60,4 @@ final class IOUtils { throw new JPackageException(I18N.format("error.cannot-write-to-output-dir", outdir.toAbsolutePath())); } } - - public static long getPID(Process p) { - try { - return p.pid(); - } catch (UnsupportedOperationException ex) { - Log.verbose(ex); // Just log exception and ignore it. This method - // is used for verbose output, so not a problem - // if unsupported. - return -1; - } - } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java index 37f166a4e7c..f960f8dc2bf 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/JLinkRuntimeBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -22,13 +22,14 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ + package jdk.jpackage.internal; + import static jdk.jpackage.internal.model.RuntimeBuilder.getDefaultModulePath; +import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; import java.io.File; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; @@ -50,7 +51,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.internal.module.ModulePath; import jdk.jpackage.internal.model.AppImageLayout; -import jdk.jpackage.internal.model.JPackageException; import jdk.jpackage.internal.model.LauncherModularStartupInfo; import jdk.jpackage.internal.model.LauncherStartupInfo; import jdk.jpackage.internal.model.RuntimeBuilder; @@ -58,27 +58,15 @@ import jdk.jpackage.internal.model.RuntimeBuilder; final class JLinkRuntimeBuilder implements RuntimeBuilder { private JLinkRuntimeBuilder(List jlinkCmdLine) { - this.jlinkCmdLine = jlinkCmdLine; + this.jlinkCmdLine = Objects.requireNonNull(jlinkCmdLine); } @Override public void create(AppImageLayout appImageLayout) { - var args = new ArrayList(); - args.add("--output"); - args.add(appImageLayout.runtimeDirectory().toString()); - args.addAll(jlinkCmdLine); - - StringWriter writer = new StringWriter(); - PrintWriter pw = new PrintWriter(writer); - - int retVal = LazyLoad.JLINK_TOOL.run(pw, pw, args.toArray(String[]::new)); - String jlinkOut = writer.toString(); - - args.add(0, "jlink"); - Log.verbose(args, List.of(jlinkOut), retVal, -1); - if (retVal != 0) { - throw new JPackageException(I18N.format("error.jlink.failed", jlinkOut)); - } + toRunnable(Executor.of() + .toolProvider(LazyLoad.JLINK_TOOL) + .args("--output", appImageLayout.runtimeDirectory().toString()) + .args(jlinkCmdLine)::executeExpectSuccess).run(); } @Override diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java index b14b4ab22ca..0f51fa166f9 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Log.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2026, 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 @@ -28,7 +28,6 @@ package jdk.jpackage.internal; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.List; /** * Log @@ -105,29 +104,6 @@ public class Log { } } - public void verbose(List strings, - List output, int returnCode, long pid) { - if (verbose) { - StringBuilder sb = new StringBuilder(); - sb.append("Command [PID: "); - sb.append(pid); - sb.append("]:\n "); - - for (String s : strings) { - sb.append(" " + s); - } - verbose(sb.toString()); - if (output != null && !output.isEmpty()) { - sb = new StringBuilder("Output:"); - for (String s : output) { - sb.append("\n " + s); - } - verbose(sb.toString()); - } - verbose("Returned: " + returnCode + "\n"); - } - } - private String addTimestamp(String msg) { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS"); Date time = new Date(System.currentTimeMillis()); @@ -177,9 +153,4 @@ public class Log { public static void verbose(Throwable t) { instance.get().verbose(t); } - - public static void verbose(List strings, List out, - int ret, long pid) { - instance.get().verbose(strings, out, ret, pid); - } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ObjectFactory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ObjectFactory.java new file mode 100644 index 00000000000..f1a83eb9eab --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ObjectFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal; + +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.util.CompositeProxy; + +interface ObjectFactory extends ExecutorFactory, RetryExecutorFactory { + + static ObjectFactory.Builder build() { + return new Builder(); + } + + static ObjectFactory.Builder build(ObjectFactory from) { + return build().initFrom(from); + } + + static final class Builder { + private Builder() { + } + + ObjectFactory create() { + return CompositeProxy.build().invokeTunnel(CompositeProxyTunnel.INSTANCE).create( + ObjectFactory.class, + Optional.ofNullable(executorFactory).orElse(ExecutorFactory.DEFAULT), + Optional.ofNullable(retryExecutorFactory).orElse(RetryExecutorFactory.DEFAULT)); + } + + Builder initFrom(ObjectFactory of) { + Objects.requireNonNull(of); + return executorFactory(of).retryExecutorFactory(of); + } + + Builder executorFactory(ExecutorFactory v) { + executorFactory = v; + return this; + } + + Builder retryExecutorFactory(RetryExecutorFactory v) { + retryExecutorFactory = v; + return this; + } + + private ExecutorFactory executorFactory; + private RetryExecutorFactory retryExecutorFactory; + } + + static final ObjectFactory DEFAULT = build().create(); +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutor.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutor.java deleted file mode 100644 index f806217e8f9..00000000000 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutor.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2020, 2023, 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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. - */ -package jdk.jpackage.internal; - -import java.io.IOException; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; - -public final class RetryExecutor { - public RetryExecutor() { - setMaxAttemptsCount(5); - setAttemptTimeoutMillis(2 * 1000); - setWriteOutputToFile(false); - } - - public RetryExecutor setMaxAttemptsCount(int v) { - attempts = v; - return this; - } - - public RetryExecutor setAttemptTimeoutMillis(int v) { - timeoutMillis = v; - return this; - } - - public RetryExecutor saveOutput(boolean v) { - saveOutput = v; - return this; - } - - public List getOutput() { - return output; - } - - public RetryExecutor setWriteOutputToFile(boolean v) { - writeOutputToFile = v; - return this; - } - - public RetryExecutor setExecutorInitializer(Consumer v) { - executorInitializer = v; - return this; - } - - public void abort() { - aborted = true; - } - - public boolean isAborted() { - return aborted; - } - - static RetryExecutor retryOnKnownErrorMessage(String v) { - RetryExecutor result = new RetryExecutor(); - return result.setExecutorInitializer(exec -> { - exec.setOutputConsumer(output -> { - if (!output.anyMatch(v::equals)) { - result.abort(); - } - }); - }); - } - - public void execute(String cmdline[]) throws IOException { - executeLoop(() -> - Executor.of(cmdline).setWriteOutputToFile(writeOutputToFile)); - } - - public void execute(ProcessBuilder pb) throws IOException { - executeLoop(() -> - Executor.of(pb).setWriteOutputToFile(writeOutputToFile)); - } - - private void executeLoop(Supplier execSupplier) throws IOException { - aborted = false; - for (;;) { - if (aborted) { - break; - } - - try { - Executor exec = execSupplier.get().saveOutput(saveOutput); - if (executorInitializer != null) { - executorInitializer.accept(exec); - } - exec.executeExpectSuccess(); - if (saveOutput) { - output = exec.getOutput(); - } - break; - } catch (IOException ex) { - if (aborted || (--attempts) <= 0) { - throw ex; - } - } - - try { - Thread.sleep(timeoutMillis); - } catch (InterruptedException ex) { - Log.verbose(ex); - throw new RuntimeException(ex); - } - } - } - - private Consumer executorInitializer; - private boolean aborted; - private int attempts; - private int timeoutMillis; - private boolean saveOutput; - private List output; - private boolean writeOutputToFile; -} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutorFactory.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutorFactory.java new file mode 100644 index 00000000000..3efb522abd4 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/RetryExecutorFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal; + +import jdk.jpackage.internal.util.RetryExecutor; + +@FunctionalInterface +interface RetryExecutorFactory { + + RetryExecutor retryExecutor(Class exceptionType); + + static final RetryExecutorFactory DEFAULT = RetryExecutor::new; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java index de98e97c922..5d9a1d6d147 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/SystemEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -24,5 +24,5 @@ */ package jdk.jpackage.internal; -public interface SystemEnvironment { +interface SystemEnvironment { } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java index 13e87d5cfa6..9440aff3a53 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/ToolValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -121,32 +121,32 @@ final class ToolValidator { cmdline.addAll(args); } - boolean canUseTool[] = new boolean[1]; + boolean canUseTool = false; if (minimalVersion == null) { // No version check. - canUseTool[0] = true; + canUseTool = true; } - String[] version = new String[1]; + String version = null; try { - Executor.of(cmdline.toArray(String[]::new)).setQuiet(true).setOutputConsumer(lines -> { - if (versionParser != null && minimalVersion != null) { - version[0] = versionParser.apply(lines); - if (version[0] != null && minimalVersion.compareTo(version[0]) <= 0) { - canUseTool[0] = true; - } + var result = Executor.of(cmdline).setQuiet(true).saveOutput().execute(); + var lines = result.content(); + if (versionParser != null && minimalVersion != null) { + version = versionParser.apply(lines.stream()); + if (version != null && minimalVersion.compareTo(version) <= 0) { + canUseTool = true; } - }).execute(); + } } catch (IOException e) { return new ConfigException(I18N.format("error.tool-error", toolPath, e.getMessage()), null, e); } - if (canUseTool[0]) { + if (canUseTool) { // All good. Tool can be used. return null; } else if (toolOldVersionErrorHandler != null) { - return toolOldVersionErrorHandler.apply(toolPath, version[0]); + return toolOldVersionErrorHandler.apply(toolPath, version); } else { return new ConfigException( I18N.format("error.tool-old-version", toolPath, minimalVersion), diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java index 9feb5b40944..31be2bb33c5 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -45,6 +45,7 @@ import java.util.function.Supplier; import java.util.spi.ToolProvider; import jdk.internal.opt.CommandLine; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.Globals; import jdk.jpackage.internal.Log; import jdk.jpackage.internal.model.ConfigException; import jdk.jpackage.internal.model.JPackageException; @@ -56,7 +57,15 @@ import jdk.jpackage.internal.util.function.ExceptionBox; */ public final class Main { - public static final class Provider implements ToolProvider { + public record Provider(Supplier bundlingEnvSupplier) implements ToolProvider { + + public Provider { + Objects.requireNonNull(bundlingEnvSupplier); + } + + public Provider() { + this(DefaultBundlingEnvironmentLoader.INSTANCE); + } @Override public String name() { @@ -65,7 +74,7 @@ public final class Main { @Override public int run(PrintWriter out, PrintWriter err, String... args) { - return Main.run(out, err, args); + return Main.run(bundlingEnvSupplier, out, err, args); } @Override @@ -94,7 +103,23 @@ public final class Main { System.exit(run(out, err, args)); } - public static int run(PrintWriter out, PrintWriter err, String... args) { + static int run(PrintWriter out, PrintWriter err, String... args) { + return run(DefaultBundlingEnvironmentLoader.INSTANCE, out, err, args); + } + + static int run(Supplier bundlingEnvSupplier, PrintWriter out, PrintWriter err, String... args) { + return Globals.main(() -> { + return runWithGlobals(bundlingEnvSupplier, out, err, args); + }); + } + + private static int runWithGlobals( + Supplier bundlingEnvSupplier, + PrintWriter out, + PrintWriter err, + String... args) { + + Objects.requireNonNull(bundlingEnvSupplier); Objects.requireNonNull(args); for (String arg : args) { Objects.requireNonNull(arg); @@ -128,8 +153,7 @@ public final class Main { return preprocessStatus; } - final var bundlingEnv = ServiceLoader.load(CliBundlingEnvironment.class, - CliBundlingEnvironment.class.getClassLoader()).findFirst().orElseThrow(); + final var bundlingEnv = bundlingEnvSupplier.get(); final var parseResult = Utils.buildParser(OperatingSystem.current(), bundlingEnv).create().apply(mappedArgs.get()); @@ -285,4 +309,15 @@ public final class Main { private static String getVersion() { return System.getProperty("java.version"); } + + private enum DefaultBundlingEnvironmentLoader implements Supplier { + INSTANCE; + + @Override + public CliBundlingEnvironment get() { + return ServiceLoader.load( + CliBundlingEnvironment.class, + CliBundlingEnvironment.class.getClassLoader()).findFirst().orElseThrow(); + } + } } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java index ec63c1fb498..0fa0af296dc 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/cli/StandardOption.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -727,8 +727,15 @@ public final class StandardOption { // // regexp for parsing args (for example, for additional launchers) - private static Pattern pattern = Pattern.compile( - "(?:(?:([\"'])(?:\\\\\\1|.)*?(?:\\1|$))|(?:\\\\[\"'\\s]|[^\\s]))++"); + private static Pattern PATTERN = Pattern.compile(String.format( + "(?:(?:%s|%s)|(?:\\\\[\"'\\s]|\\S))++", + createPatternComponent('\''), + createPatternComponent('\"'))); + + private static String createPatternComponent(char quoteChar) { + var str = Character.toString(quoteChar); + return String.format("(?:%s(?:\\\\%s|[^%s])*+(?:%s|$))", str, str, str, str); + } static List getArgumentList(String inputString) { Objects.requireNonNull(inputString); @@ -741,7 +748,7 @@ public final class StandardOption { // The "pattern" regexp attempts to abide to the rule that // strings are delimited by whitespace unless surrounded by // quotes, then it is anything (including spaces) in the quotes. - Matcher m = pattern.matcher(inputString); + Matcher m = PATTERN.matcher(inputString); while (m.find()) { String s = inputString.substring(m.start(), m.end()).trim(); // Ensure we do not have an empty string. trim() will take care of diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties index c5ea583514f..e97bee79e6e 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/resources/MainResources.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2017, 2026, 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 @@ -96,7 +96,6 @@ error.tool-not-found.advice=Please install "{0}" error.tool-old-version=Can not find "{0}" {1} or newer error.tool-old-version.advice=Please install "{0}" {1} or newer -error.jlink.failed=jlink failed with: {0} error.blocked.option=jlink option [{0}] is not permitted in --jlink-options error.no.name=Name not specified with --name and cannot infer one from app-image error.no.name.advice=Specify name with --name diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandLineFormat.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandLineFormat.java new file mode 100644 index 00000000000..4d64030876f --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandLineFormat.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal.util; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Formats command line arguments. + */ +public final class CommandLineFormat { + + public String format(List cmdline) { + return cmdline.stream().map(enquoter::applyTo).collect(Collectors.joining(" ")); + } + + public static CommandLineFormat platform() { + var format = new CommandLineFormat(); + format.enquoter = Enquoter.identity().setEnquotePredicate(Enquoter.QUOTE_IF_WHITESPACES).setQuoteChar('\''); + return format; + } + + private CommandLineFormat() { + } + + private Enquoter enquoter; + + public static final Function, String> DEFAULT = platform()::format; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandOutputControl.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandOutputControl.java new file mode 100644 index 00000000000..e35439815b1 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/CommandOutputControl.java @@ -0,0 +1,1904 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal.util; + +import static java.util.stream.Collectors.joining; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Predicate; +import java.util.spi.ToolProvider; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.internal.util.function.ThrowingRunnable; + +/** + * Runs commands and processes their stdout and stderr streams. + *

+ * A command is either a subprocess represented by {@link ProcessBuilder} or a + * tool provided by {@link ToolProvider}. + *

+ * A command is executed synchronously, and the result of its execution is + * stored in a {@link Result} instance which captures the exit code and any + * saved output streams. + *

+ * Depending on the configuration, it can save the entire output stream, only + * the first line, or not save the output at all. Stdout and stderr streams can + * be configured independently. + *

+ * Output streams can be treated as either byte streams or character streams. + * + *

+ * The table below shows how different parameter combinations affect the content + * written to streams returned by {@link #dumpStdout()} and + * {@link #dumpStderr()} for subsequently executed tools, regardless of whether + * their output streams are saved, or for subprocesses when the output streams + * are saved: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
discardStdout(false) and discardStderr(false)discardStdout(false) and discardStderr(true)discardStdout(true) and discardStderr(false)
redirectStderr(true) and dumpOutput(true) + *

+ * dumpStdout(): STDOUT and STDERR interleaved + *

+ * dumpStderr(): unchanged

+ *

+ * dumpStdout(): STDOUT + *

+ * dumpStderr(): unchanged

+ *

+ * dumpStdout(): STDERR; + *

+ * dumpStderr(): unchanged

redirectStderr(false) and dumpOutput(true) + *

+ * dumpStdout(): STDOUT + *

+ * dumpStderr(): STDERR

+ *

+ * dumpStdout(): STDOUT + *

+ * dumpStderr(): unchanged

+ *

+ * dumpStdout(): unchanged + *

+ * dumpStderr(): STDERR

+ * + *

+ * The table below shows how different parameter combinations affect the content + * written to the native file descriptors associated with {@link System#out} and + * {@link System#err} for subsequently executed subprocesses when the output + * streams are not saved: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
discardStdout(false) and discardStderr(false)discardStdout(false) and discardStderr(true)discardStdout(true) and discardStderr(false)
redirectStderr(true) and dumpOutput(true) + *

+ * System.out: STDOUT and STDERR interleaved + *

+ * System.err: unchanged

+ *

+ * System.out: STDOUT + *

+ * System.err: unchanged

+ *

+ * System.out: STDERR; + *

+ * The command's STDERR will be written to the stream referenced by + * {@link #dumpStdout()} rather than to the underlying file descriptor + * associated with the Java process's STDOUT + *

+ * System.err: unchanged

redirectStderr(false) and dumpOutput(true) + *

+ * System.out: STDOUT + *

+ * System.err: STDERR

+ *

+ * System.out: STDOUT + *

+ * System.err: unchanged

+ *

+ * System.out: unchanged + *

+ * System.err: STDERR

+ * + *

+ * The table below shows how different parameter combinations affect the + * properties of {@link Result} objects returned by + * {@link #execute(ProcessBuilder, long)} or + * {@link #execute(ToolProvider, long, String...)} when processing character + * streams: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
saveOutput(true)saveFirstLineOfOutput()
redirectStderr(true) and discardStdout(false) and + * discardStderr(false) + *

+ * content(): STDOUT and STDERR interleaved + *

+ * findStdout(): {@code Optional.empty()} + *

+ * findStderr(): {@code Optional.empty()}

+ *

+ * content(): a single-item list containing the first line of interleaved STDOUT + * and STDERR if the command produced any output; otherwise, an empty list + *

+ * findStdout(): {@code Optional.empty()} + *

+ * findStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(false) and + * discardStderr(false) + *

+ * content(): STDOUT followed by STDERR + *

+ * stdout(): STDOUT + *

+ * stderr(): STDERR

+ *

+ * content(): a list containing at most two items: the first line of STDOUT (if + * the command produced any), followed by the first line of STDERR (if the + * command produced any) + *

+ * stdout(): The first line of STDOUT (if the command produced any); otherwise + * an empty list + *

+ * findStderr(): The first line of STDERR (if the command produced any); + * otherwise an empty list + *

redirectStderr(true) and discardStdout(false) and + * discardStderr(true) + *

+ * content(): STDOUT + *

+ * stdout(): The same as content() + *

+ * findStderr(): {@code Optional.empty()}

+ *

+ * content(): The first line of STDOUT (if the command produced any); otherwise + * an empty list + *

+ * stdout(): The same as content() + *

+ * findStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(false) and + * discardStderr(true) + *

+ * content(): STDOUT + *

+ * stdout(): The same as content() + *

+ * stderr(): an empty list

+ *

+ * content(): The first line of STDOUT (if the command produced any); otherwise + * an empty list + *

+ * stdout(): The same as content() + *

+ * stderr(): an empty list

redirectStderr(true) and discardStdout(true) and + * discardStderr(false) + *

+ * content(): STDERR + *

+ * stdout(): The same as content() + *

+ * findStderr(): {@code Optional.empty()}

+ *

+ * content(): The first line of STDERR (if the command produced any); otherwise + * an empty list + *

+ * stdout(): The same as content() + *

+ * findStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(true) and + * discardStderr(false) + *

+ * content(): STDERR + *

+ * findStdout(): an empty list + *

+ * stderr(): The same as content()

+ *

+ * content(): The first line of STDERR (if the command produced any); otherwise + * an empty list + *

+ * findStdout(): an empty list + *

+ * stderr(): The same as content()

+ *

+ * The table below shows how different parameter combinations affect the + * properties of {@link Result} objects returned by + * {@link #execute(ProcessBuilder, long)} or + * {@link #execute(ToolProvider, long, String...)} when processing byte streams: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
saveOutput(true) or saveFirstLineOfOutput()
redirectStderr(true) and discardStdout(false) and + * discardStderr(false) + *

+ * byteContent(): STDOUT and STDERR interleaved + *

+ * findByteStdout(): {@code Optional.empty()} + *

+ * findByteStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(false) and + * discardStderr(false) + *

+ * byteContent(): STDOUT followed by STDERR + *

+ * byteStdout(): STDOUT + *

+ * byteStderr(): STDERR

redirectStderr(true) and discardStdout(false) and + * discardStderr(true) + *

+ * byteContent(): STDOUT + *

+ * byteStdout(): The same as byteContent() + *

+ * findByteStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(false) and + * discardStderr(true) + *

+ * byteContent(): STDOUT + *

+ * byteStdout(): The same as byteContent() + *

+ * byteStderr(): an empty array

redirectStderr(true) and discardStdout(true) and + * discardStderr(false) + *

+ * byteContent(): STDERR + *

+ * byteStdout(): The same as byteContent() + *

+ * findByteStderr(): {@code Optional.empty()}

redirectStderr(false) and discardStdout(true) and + * discardStderr(false) + *

+ * byteContent(): STDERR + *

+ * findByteStdout(): an empty array + *

+ * byteStderr(): The same as byteContent()

+ */ +public final class CommandOutputControl { + + public CommandOutputControl() { + outputStreamsControl = new OutputStreamsControl(); + } + + private CommandOutputControl(CommandOutputControl other) { + flags = other.flags; + outputStreamsControl = other.outputStreamsControl.copy(); + dumpStdout = other.dumpStdout; + dumpStderr = other.dumpStderr; + charset = other.charset; + processListener = other.processListener; + } + + /** + * Specifies whether the full output produced by commands subsequently executed + * by this object will be saved. + *

+ * If {@code v} is {@code true}, both stdout and stderr streams will be saved; + * otherwise, they will not be saved. + *

+ * This setting is mutually exclusive with {@link #saveFirstLineOfOutput()}. + * + * @param v {@code true} to save the full stdout and stderr streams; + * {@code false} otherwise + * @return this + */ + public CommandOutputControl saveOutput(boolean v) { + return setOutputControl(v, OutputControlOption.SAVE_ALL); + } + + /** + * Returns whether this object will save the complete output of commands + * subsequently executed. + * + * @return {@code true} if this object will save the full output of commands it + * executes subsequently; {@code false} otherwise + */ + public boolean isSaveOutput() { + return outputStreamsControl.stdout().saveAll(); + } + + /** + * Specifies whether the first line of the output, combined from the stdout and + * stderr streams of commands subsequently executed by this object, will be + * saved. + *

+ * This setting is mutually exclusive with {@link #saveOutput(boolean)}. + * + * @return this + */ + public CommandOutputControl saveFirstLineOfOutput() { + return setOutputControl(true, OutputControlOption.SAVE_FIRST_LINE); + } + + /** + * Returns whether this object will save the first line of the output of + * commands subsequently executed. + * + * @return {@code true} if this object will save the first line of the output of + * commands it executes subsequently; {@code false} otherwise + */ + public boolean isSaveFirstLineOfOutput() { + return outputStreamsControl.stdout().saveFirstLine(); + } + + /** + * Specifies whether this object will dump the output streams from + * subsequently executed commands into the streams returned by + * {@link #dumpStdout()} and {@link #dumpStdout()} methods respectively. + *

+ * If this object is configured to redirect stderr of subsequently executed + * commands into their stdout ({@code redirectStderr(true)}), their output + * streams will be dumped into the stream returned by {@code dumpStdout()} + * method. Otherwise, their stdout and stderr streams will be dumped into the + * stream returned by {@code dumpStdout()} and {@code dumpStderr()} methods + * respectively. + * + * @param v if output streams from subsequently executed commands will be + * dumped into streams returned by {@code dumpStdout()} and + * {@code dumpStderr()} methods respectively + * + * @return this + * + * @see #redirectStderr(boolean) + * @see #dumpStdout() + * @see #dumpStderr() + */ + public CommandOutputControl dumpOutput(boolean v) { + setFlag(Flag.DUMP, v); + return setOutputControl(v, OutputControlOption.DUMP); + } + + /** + * Returns the value passed in the last call of {@link #dumpOutput(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #dumpOutput(boolean)} + */ + public boolean isDumpOutput() { + return Flag.DUMP.isSet(flags); + } + + /** + * Specifies whether this object will treat output streams of subsequently + * executed commands as byte streams rather than character streams. + * + * @param v {@code true} if this object will treat the output streams of + * subsequently executed commands as byte streams, and {@code false} + * otherwise + * + * @return this + */ + public CommandOutputControl binaryOutput(boolean v) { + return setFlag(Flag.BINARY_OUTPUT, v); + } + + /** + * Returns the value passed in the last call of {@link #binaryOutput(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #binaryOutput(boolean)} + */ + public boolean isBinaryOutput() { + return Flag.BINARY_OUTPUT.isSet(flags); + } + + /** + * Sets character encoding that will be applied to the stdout and the stderr + * streams of commands (subprocesses and {@code ToolProvider}-s) subsequently + * executed by this object. The default encoding is {@code UTF-8}. + *

+ * The value will be ignored if this object is configured for byte output + * streams. + * + * @param v character encoding for output streams of subsequently executed + * commands + * + * @see #binaryOutput(boolean) + * + * @return this + */ + public CommandOutputControl charset(Charset v) { + charset = v; + return this; + } + + /** + * Returns the value passed in the last call of + * {@link #charset(Charset)} method on this object, or + * {@link StandardCharsets#UTF_8} if the method has not been called. + * + * @return the character encoding that will be applied to the stdout and stderr + * streams of commands subsequently executed by this object + */ + public Charset charset() { + return Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8); + } + + /** + * Specifies whether the stderr stream will be redirected into the stdout stream + * for commands subsequently executed by this object. + * + * @see ProcessBuilder#redirectErrorStream(boolean) + * + * @param v {@code true} if the stderr stream of commands subsequently executed + * by this object will be redirected into the stdout stream; + * {@code false} otherwise + * + * @return this + */ + public CommandOutputControl redirectStderr(boolean v) { + return setFlag(Flag.REDIRECT_STDERR, v); + } + + /** + * Returns the value passed in the last call of {@link #redirectStderr(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #redirectStderr(boolean)} + */ + public boolean isRedirectStderr() { + return Flag.REDIRECT_STDERR.isSet(flags); + } + + /** + * Specifies whether stderr and stdout streams for subprocesses subsequently + * executed by this object will be stored in files. + *

+ * By default, if an output stream of a subprocess is configured for saving, + * this object will retrieve the content using {@link Process#getInputStream()} + * function for stdout and {@link Process#getErrorStream()} function for stderr. + * However, these functions don't always work correctly due to a + * JDK-8236825 bug + * still reproducible on macOS JDK26. The alternative way to get the content of + * output streams of a subprocess is to redirect them into files and read these + * files when the subprocess terminates. + *

+ * It will use {@code Files.createTempFile("jpackageOutputTempFile", ".tmp")} to + * create a file for each subprocess's output stream configured for saving. All + * created files will be automatically deleted at the exit of + * {@link #execute(ProcessBuilder, long)} method. + *

+ * Doesn't apply to executing {@code ToolProvider}-s. + *

+ * Storing output streams in files takes longer than managing them in memory and + * should be avoided if possible. + * + * @param v {@code true} if this object will use files to store saved output + * streams of subsequently executed subprocesses; {@code false} + * otherwise + * @return this + */ + public CommandOutputControl storeOutputInFiles(boolean v) { + return setFlag(Flag.STORE_OUTPUT_IN_FILES, v); + } + + /** + * Returns the value passed in the last call of {@link #storeOutputInFiles(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #storeOutputInFiles(boolean)} + */ + public boolean isStoreOutputInFiles() { + return Flag.STORE_OUTPUT_IN_FILES.isSet(flags); + } + + /** + * Specifies whether stdout streams from commands subsequently executed by this + * object will be discarded. + * + * @param v {@code true} if this object will discard stdout streams from + * commands subsequently executed by this object; {@code false} + * otherwise + * @return this + */ + public CommandOutputControl discardStdout(boolean v) { + setFlag(Flag.DISCARD_STDOUT, v); + outputStreamsControl.stdout().discard(v); + return this; + } + + /** + * Returns the value passed in the last call of {@link #discardStdout(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #discardStdout(boolean)} + */ + public boolean isDiscardStdout() { + return Flag.DISCARD_STDOUT.isSet(flags); + } + + /** + * Specifies whether stderr streams from commands subsequently executed by this + * object will be discarded. + * + * @param v {@code true} if this object will discard stderr streams from + * commands subsequently executed by this object; {@code false} + * otherwise + * @return this + */ + public CommandOutputControl discardStderr(boolean v) { + setFlag(Flag.DISCARD_STDERR, v); + outputStreamsControl.stderr().discard(v); + return this; + } + + /** + * Returns the value passed in the last call of {@link #discardStderr(boolean)} + * method on this object, or {@code false} if the method has not been called. + * + * @return the value passed in the last call of {@link #discardStderr(boolean)} + */ + public boolean isDiscardStderr() { + return Flag.DISCARD_STDERR.isSet(flags); + } + + /** + * Specifies the stream where stdout streams from commands subsequently executed + * by this object will be dumped. + *

+ * If the {@code null} is specified and this object configuration is equivalent + * to {@code dumpOutput(true).saveOutput(false).discardStdout(false)} the stdout + * streams from commands subsequently executed by this object will be written + * into the file descriptor associated with the {@code Systsem.out} stream. If + * you want them to be written into the {@code Systsem.out} object, pass the + * {@code Systsem.out} reference into this function. + * + * @param v the stream where stdout streams from commands subsequently executed + * by this object will be dumped; {@code null} permitted + * @return this + */ + public CommandOutputControl dumpStdout(PrintStream v) { + dumpStdout = v; + return this; + } + + /** + * Returns the value passed in the last call of {@link #dumpStdout(PrintStream)} + * method on this object, or {@link System#out} if the method has not been + * called. + * + * @return the stream where stdout streams from commands subsequently executed + * by this object will be dumped + */ + public PrintStream dumpStdout() { + return Optional.ofNullable(dumpStdout).orElse(System.out); + } + + /** + * Specifies the stream where stderr streams from commands subsequently executed + * by this object will be dumped. + *

+ * If the {@code null} is specified and this object configuration is equivalent + * to + * {@code dumpOutput(true).saveOutput(false).redirectStderr(false).discardStderr(false)} + * the stderr streams from commands subsequently executed by this object will be + * written into the file descriptor associated with the {@code Systsem.err} + * stream. If you want them to be written into the {@code Systsem.err} object, + * pass the {@code Systsem.err} reference into this function. + * + * @param v the stream where stderr streams from commands subsequently executed + * by this object will be dumped; {@code null} permitted + * @return this + */ + public CommandOutputControl dumpStderr(PrintStream v) { + dumpStderr = v; + return this; + } + + /** + * Returns the value passed in the last call of {@link #dumpStderr(PrintStream)} + * method on this object, or {@link System#err} if the method has not been + * called. + * + * @return the stream where stderr streams from commands subsequently executed + * by this object will be dumped + */ + public PrintStream dumpStderr() { + return Optional.ofNullable(dumpStderr).orElse(System.err); + } + + /** + * Sets the callback to be invoked when this object starts a subprocess from + * subsequent {@link #execute(ProcessBuilder, long)} calls. + * + *

+ * This object maintains a single callback. Calling this method replaces any + * previously set callback. + * + *

+ * The callback is invoked on the thread that calls + * {@link #execute(ProcessBuilder, long)} after the subprocess's output streams + * begin being pumped. + * + * @param v the callback for notifying a subprocess being started or + * {@code null} + * @return this + */ + public CommandOutputControl processListener(Consumer v) { + processListener = v; + return this; + } + + /** + * Returns an {@code Optional} wrapping the value passed in the last call of + * {@link #processListener(Consumer)} method on this object, or an empty + * {@code Optional} if the method has not been called or {@code null} was passed in the last call. + * + * @return an {@code Optional} wrapping the value passed in the last call of + * {@link #processListener(Consumer)} + */ + public Optional> processListener() { + return Optional.ofNullable(processListener); + } + + /** + * Returns a deep copy of this object. Changes to the copy will not affect the + * original. + * + * @return a deep copy of this object + */ + public CommandOutputControl copy() { + return new CommandOutputControl(this); + } + + public interface ExecutableAttributes { + List commandLine(); + } + + public sealed interface Executable { + + ExecutableAttributes attributes(); + + Result execute() throws IOException, InterruptedException; + + Result execute(long timeout, TimeUnit unit) throws IOException, InterruptedException; + } + + public record ProcessAttributes(Optional pid, List commandLine) implements ExecutableAttributes { + public ProcessAttributes { + Objects.requireNonNull(pid); + commandLine.forEach(Objects::requireNonNull); + } + + @Override + public String toString() { + return CommandLineFormat.DEFAULT.apply(commandLine()); + } + } + + public record ToolProviderAttributes(String name, List args) implements ExecutableAttributes { + public ToolProviderAttributes { + Objects.requireNonNull(name); + args.forEach(Objects::requireNonNull); + } + + @Override + public String toString() { + return CommandLineFormat.DEFAULT.apply(commandLine()); + } + + @Override + public List commandLine() { + return Stream.concat(Stream.of(name), args.stream()).toList(); + } + } + + public static ExecutableAttributes EMPTY_EXECUTABLE_ATTRIBUTES = new ExecutableAttributes() { + @Override + public String toString() { + return ""; + } + + @Override + public List commandLine() { + return List.of(); + } + }; + + public Executable createExecutable(ToolProvider tp, String... args) { + return new ToolProviderExecutable(tp, List.of(args), this); + } + + public Executable createExecutable(ProcessBuilder pb) { + return new ProcessExecutable(pb, this); + } + + public record Result( + Optional exitCode, + Optional>> output, + Optional> byteOutput, + ExecutableAttributes execAttrs) { + + public Result { + Objects.requireNonNull(exitCode); + Objects.requireNonNull(output); + Objects.requireNonNull(byteOutput); + Objects.requireNonNull(execAttrs); + } + + public Result(int exitCode) { + this(Optional.of(exitCode), Optional.empty(), Optional.empty(), EMPTY_EXECUTABLE_ATTRIBUTES); + } + + public int getExitCode() { + return exitCode.orElseThrow(() -> { + return new IllegalStateException("Exit code is unavailable for timed-out command"); + }); + } + + public Result expectExitCode(int main, int... other) throws UnexpectedExitCodeException { + return expectExitCode(v -> { + return IntStream.concat(IntStream.of(main), IntStream.of(other)).boxed().anyMatch(Predicate.isEqual(v)); + }); + } + + public Result expectExitCode(Collection expected) throws UnexpectedExitCodeException { + return expectExitCode(expected::contains); + } + + public Result expectExitCode(IntPredicate expected) throws UnexpectedExitCodeException { + if (!expected.test(getExitCode())) { + throw new UnexpectedExitCodeException(this); + } + return this; + } + + public UnexpectedResultException unexpected() { + return new UnexpectedResultException(this); + } + + public UnexpectedResultException unexpected(String message) { + return new UnexpectedResultException(this, message); + } + + public Optional> findContent() { + return output.flatMap(CommandOutput::combined); + } + + public Optional> findStdout() { + return output.flatMap(CommandOutput::stdout); + } + + public Optional> findStderr() { + return output.flatMap(CommandOutput::stderr); + } + + // For backward compatibility + public List getOutput() { + return content(); + } + + public List content() { + return findContent().orElseThrow(); + } + + public List stdout() { + return findStdout().orElseThrow(); + } + + public List stderr() { + return findStderr().orElseThrow(); + } + + public Optional findByteContent() { + return byteOutput.flatMap(CommandOutput::combined); + } + + public Optional findByteStdout() { + return byteOutput.flatMap(CommandOutput::stdout); + } + + public Optional findByteStderr() { + return byteOutput.flatMap(CommandOutput::stderr); + } + + public byte[] byteContent() { + return findByteContent().orElseThrow(); + } + + public byte[] byteStdout() { + return findByteStdout().orElseThrow(); + } + + public byte[] byteStderr() { + return findByteStderr().orElseThrow(); + } + + public Result toCharacterResult(Charset charset, boolean keepByteContent) throws IOException { + Objects.requireNonNull(charset); + + if (byteOutput.isEmpty()) { + return this; + } + + var theByteOutput = byteOutput.get(); + + try { + Optional>> out; + if (theByteOutput.content().isEmpty()) { + // The content is unavailable. + out = Optional.empty(); + } else if (theByteOutput.stdoutContentSize() == 0) { + // The content is available, but empty. + out = Optional.of(new StringListContent(List.of())); + } else if (theByteOutput.interleaved()) { + // STDOUT and STDERR streams are interleaved. + out = theByteOutput.combined().map(data -> { + return toStringList(data, charset); + }); + } else { + // Non-empty STDOUT not interleaved with STDERR. + out = findByteStdout().map(data -> { + return toStringList(data, charset); + }); + } + + var err = findByteStderr().map(data -> { + return toStringList(data, charset); + }); + + var newOutput = combine(out, err, theByteOutput.interleaved); + + return new Result(exitCode, Optional.of(newOutput), byteOutput.filter(_ -> keepByteContent), execAttrs); + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + public Result copyWithExecutableAttributes(ExecutableAttributes execAttrs) { + return new Result(exitCode, output, byteOutput, Objects.requireNonNull(execAttrs)); + } + + private static StringListContent toStringList(byte[] data, Charset charset) { + try (var bufReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data), charset))) { + return new StringListContent(bufReader.lines().toList()); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + + public static sealed class UnexpectedResultException extends IOException { + + private UnexpectedResultException(Result value, String message) { + super(Objects.requireNonNull(message)); + this.value = Objects.requireNonNull(value); + } + + private UnexpectedResultException(Result value) { + this(value, String.format("Unexpected result from executing the command %s", value.execAttrs())); + } + + public Result getResult() { + return value; + } + + private final transient Result value; + + private static final long serialVersionUID = 1L; + } + + public static final class UnexpectedExitCodeException extends UnexpectedResultException { + + public UnexpectedExitCodeException(Result value, String message) { + super(value, message); + } + + public UnexpectedExitCodeException(Result value) { + this(value, String.format("Unexpected exit code %d from executing the command %s", value.getExitCode(), value.execAttrs())); + } + + private static final long serialVersionUID = 1L; + } + + public String description() { + var tokens = outputStreamsControl.descriptionTokens(); + if (isBinaryOutput()) { + tokens.add("byte"); + } + if (redirectRetainedStderr()) { + tokens.add("interleave"); + } + return String.join("; ", tokens); + } + + private Result execute(ProcessBuilder pb, long timeoutMillis) + throws IOException, InterruptedException { + + Objects.requireNonNull(pb); + + var theCharset = charset(); + + configureProcessBuilder(pb); + + var csc = new CachingStreamsConfig(); + + var process = pb.start(); + + BiConsumer gobbler = (in, ps) -> { + try { + if (isBinaryOutput()) { + try (in) { + in.transferTo(ps); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } else { + try (var bufReader = new BufferedReader(new InputStreamReader(in, theCharset))) { + bufReader.lines().forEach(ps::println); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } finally { + suppressIOException(ps::flush); + } + }; + + // Start fetching process output streams. + // Do it before waiting for the process termination to avoid deadlocks. + + final Optional> stdoutGobbler; + if (mustReadOutputStream(pb.redirectOutput())) { + stdoutGobbler = Optional.of(CompletableFuture.runAsync(() -> { + gobbler.accept(process.getInputStream(), csc.out()); + }, gobblerExecutor)); + } else { + stdoutGobbler = Optional.empty(); + } + + final Optional> stderrGobbler; + if (!pb.redirectErrorStream() && mustReadOutputStream(pb.redirectError())) { + stderrGobbler = Optional.of(CompletableFuture.runAsync(() -> { + gobbler.accept(process.getErrorStream(), csc.err()); + }, gobblerExecutor)); + } else { + stderrGobbler = Optional.empty(); + } + + processListener().ifPresent(c -> { + c.accept(process); + }); + + final Optional exitCode; + if (timeoutMillis < 0) { + exitCode = Optional.of(process.waitFor()); + } else if (!process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) { + // Destroy the process and cancel the process output stream gobblers. + process.destroy(); + for (var g : List.of(stdoutGobbler, stderrGobbler)) { + g.ifPresent(future -> { + future.cancel(true); + }); + } + exitCode = Optional.empty(); + } else { + exitCode = Optional.of(process.exitValue()); + } + + try { + if (isStoreOutputInFiles()) { + var stdoutStorage = streamFileSink(pb.redirectOutput()); + var stderrStorage = streamFileSink(pb.redirectError()); + + Function toInputStream = path -> { + try { + return Files.newInputStream(path); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }; + + try { + stdoutStorage.map(toInputStream).ifPresent(in -> { + gobbler.accept(in, csc.out()); + }); + + stderrStorage.map(toInputStream).ifPresent(in -> { + gobbler.accept(in, csc.err()); + }); + } finally { + Consumer silentDeleter = path -> { + suppressIOException(Files::delete, path); + }; + + stdoutStorage.ifPresent(silentDeleter); + stderrStorage.ifPresent(silentDeleter); + } + } else { + stdoutGobbler.ifPresent(CommandOutputControl::join); + stderrGobbler.ifPresent(CommandOutputControl::join); + } + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + + return csc.createResult(exitCode, new ProcessAttributes(getPID(process), pb.command())); + } + + private Result execute(ToolProvider tp, long timeoutMillis, String... args) + throws IOException, InterruptedException { + + var csc = new CachingStreamsConfig(); + + Optional exitCode; + var out = csc.out(); + var err = csc.err(); + try { + if (timeoutMillis < 0) { + exitCode = Optional.of(tp.run(out, err, args)); + } else { + var future = new CompletableFuture>(); + + var workerThread = Thread.ofVirtual().start(() -> { + Optional result = Optional.empty(); + try { + result = Optional.of(tp.run(out, err, args)); + } catch (Exception ex) { + future.completeExceptionally(ex); + return; + } + future.complete(result); + }); + + try { + exitCode = future.get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (ExecutionException ex) { + // Rethrow the cause (ex.getCause()) as a RuntimeException. + // If `ex.getCause()` returns an Error, ExceptionBox.unbox() will throw it. + throw ExceptionBox.toUnchecked(ExceptionBox.unbox(ex.getCause())); + } catch (TimeoutException ex) { + workerThread.interrupt(); + exitCode = Optional.empty(); + } + } + } finally { + suppressIOException(out::flush); + suppressIOException(err::flush); + } + + return csc.createResult(exitCode, new ToolProviderAttributes(tp.name(), List.of(args))); + } + + private CommandOutputControl setOutputControl(boolean set, OutputControlOption v) { + outputStreamsControl.stdout().set(set, v); + outputStreamsControl.stderr().set(set, v); + return this; + } + + private CommandOutputControl setFlag(Flag flag, boolean v) { + flags = flag.set(flags, v); + return this; + } + + private Optional streamFileSink(ProcessBuilder.Redirect redirect) { + return Optional.of(redirect) + .filter(Predicate.isEqual(ProcessBuilder.Redirect.DISCARD).negate()) + .map(ProcessBuilder.Redirect::file) + .map(File::toPath); + } + + private void configureProcessBuilder(ProcessBuilder pb) throws IOException { + + var stdoutRedirect = outputStreamsControl.stdout().asProcessBuilderRedirect(); + var stderrRedirect = outputStreamsControl.stderr().asProcessBuilderRedirect(); + + if (!stdoutRedirect.equals(stderrRedirect) && Stream.of( + stdoutRedirect, + stderrRedirect + ).noneMatch(Predicate.isEqual(ProcessBuilder.Redirect.DISCARD)) && redirectRetainedStderr()) { + throw new IllegalStateException(String.format( + "Can't redirect stderr into stdout because they have different redirects: stdout=%s; stderr=%s", + stdoutRedirect, stderrRedirect)); + } + + pb.redirectErrorStream(redirectRetainedStderr()); + if (replaceStdoutWithStderr()) { + if (stderrRedirect.equals(ProcessBuilder.Redirect.INHERIT)) { + stderrRedirect = ProcessBuilder.Redirect.PIPE; + } + pb.redirectErrorStream(false); + } + + stdoutRedirect = mapRedirect(stdoutRedirect); + stderrRedirect = mapRedirect(stderrRedirect); + + if (dumpStdout != null && stdoutRedirect.equals(ProcessBuilder.Redirect.INHERIT)) { + stdoutRedirect = ProcessBuilder.Redirect.PIPE; + } + + if (dumpStderr != null && stderrRedirect.equals(ProcessBuilder.Redirect.INHERIT)) { + stderrRedirect = ProcessBuilder.Redirect.PIPE; + } + + pb.redirectOutput(stdoutRedirect); + pb.redirectError(stderrRedirect); + } + + private ProcessBuilder.Redirect mapRedirect(ProcessBuilder.Redirect redirect) throws IOException { + if (isStoreOutputInFiles() && redirect.equals(ProcessBuilder.Redirect.PIPE)) { + var sink = Files.createTempFile("jpackageOutputTempFile", ".tmp"); + return ProcessBuilder.Redirect.to(sink.toFile()); + } else { + return redirect; + } + } + + /** + * Returns {@code true} if STDERR is not discarded and will be redirected to STDOUT, and {@code false} otherwise. + */ + private boolean redirectRetainedStderr() { + return isRedirectStderr() && !outputStreamsControl.stderr().discard(); + } + + /** + * Returns {@code true} if STDERR will replace STDOUT, and {@code false} otherwise. + *

+ * STDERR will replace STDOUT if it is redirected and not discarded, and if STDOUT is discarded. + */ + private boolean replaceStdoutWithStderr() { + return redirectRetainedStderr() && outputStreamsControl.stdout().discard(); + } + + private static T join(CompletableFuture future, T cancelledValue) { + Objects.requireNonNull(future); + try { + return future.join(); + } catch (CancellationException ex) { + return cancelledValue; + } catch (CompletionException ex) { + switch (ExceptionBox.unbox(ex.getCause())) { + case IOException cause -> { + throw new UncheckedIOException(cause); + } + case UncheckedIOException cause -> { + throw cause; + } + case Exception cause -> { + throw ExceptionBox.toUnchecked(cause); + } + } + } + } + + private static void join(CompletableFuture future) { + join(future, null); + } + + private static boolean mustReadOutputStream(ProcessBuilder.Redirect redirect) { + return redirect.equals(ProcessBuilder.Redirect.PIPE); + } + + private static Optional> read(OutputControl outputControl, CachingPrintStream cps) throws IOException { + final var bufferAsString = cps.bufferContents(); + try (final var bufReader = new BufferedReader(new StringReader(bufferAsString.orElse("")))) { + if (outputControl.saveFirstLine()) { + return Optional.of(bufReader.lines().findFirst().map(List::of).orElseGet(List::of)); + } else if (outputControl.saveAll()) { + return Optional.of(bufReader.lines().toList()); + } else { + return Optional.empty(); + } + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + private static Optional readBinary(OutputControl outputControl, CachingPrintStream cps) { + if (outputControl.save()) { + return cps.buf().map(ByteArrayOutputStream::toByteArray).or(() -> { + return Optional.of(new byte[0]); + }); + } else { + return Optional.empty(); + } + } + + private static CommandOutput combine( + Optional> out, + Optional> err, + boolean interleaved) { + + if (out.isEmpty() && err.isEmpty()) { + return CommandOutput.empty(); + } else if (out.isEmpty()) { + // This branch is unreachable because it is impossible to make it save stderr without saving stdout. + // If streams are configured for saving and stdout is discarded, + // its saved contents will be an Optional instance wrapping an empty content, not an empty Optional. + throw ExceptionBox.reachedUnreachable(); + } else if (err.isEmpty()) { + return new CommandOutput<>(out, Integer.MAX_VALUE, interleaved); + } else { + final var combined = out.get().append(err.get()); + return new CommandOutput<>(Optional.of(combined), out.orElseThrow().size(), interleaved); + } + } + + private static PrintStream nullPrintStream() { + return new PrintStream(OutputStream.nullOutputStream()); + } + + private sealed interface Content { + T data(); + int size(); + Content slice(int from, int to); + Content append(Content other); + } + + private record StringListContent(List data) implements Content> { + StringListContent { + Objects.requireNonNull(data); + } + + @Override + public int size() { + return data.size(); + } + + @Override + public StringListContent slice(int from, int to) { + return new StringListContent(data.subList(from, to)); + } + + @Override + public StringListContent append(Content> other) { + return new StringListContent(Stream.of(data, other.data()).flatMap(List::stream).toList()); + } + } + + private record ByteContent(byte[] data) implements Content { + ByteContent { + Objects.requireNonNull(data); + } + + @Override + public int size() { + return data.length; + } + + @Override + public ByteContent slice(int from, int to) { + return new ByteContent(Arrays.copyOfRange(data, from, to)); + } + + @Override + public ByteContent append(Content other) { + byte[] combined = new byte[size() + other.size()]; + System.arraycopy(data, 0, combined, 0, data.length); + System.arraycopy(other.data(), 0, combined, data.length, other.size()); + return new ByteContent(combined); + } + } + + private record OutputStreamsControl(OutputControl stdout, OutputControl stderr) { + OutputStreamsControl { + Objects.requireNonNull(stdout); + Objects.requireNonNull(stderr); + } + + OutputStreamsControl() { + this(new OutputControl(), new OutputControl()); + } + + OutputStreamsControl copy() { + return new OutputStreamsControl(stdout.copy(), stderr.copy()); + } + + List descriptionTokens() { + final List tokens = new ArrayList<>(); + if (stdout.save()) { // Save flags are the same for stdout and stderr, checking stdout is sufficient. + streamsLabel("save ", true).ifPresent(tokens::add); + } + if (stdout.dump() || stderr.dump()) { + streamsLabel("echo ", true).ifPresent(tokens::add); + } + streamsLabel("discard ", false).ifPresent(tokens::add); + if (tokens.isEmpty()) { + // Unreachable because there is always at least one token in the description. + throw ExceptionBox.reachedUnreachable(); + } else { + return tokens; + } + } + + private Optional streamsLabel(String prefix, boolean negate) { + Objects.requireNonNull(prefix); + final var str = Stream.of(stdoutLabel(negate), stderrLabel(negate)) + .filter(Optional::isPresent) + .map(Optional::orElseThrow) + .collect(joining("+")); + if (str.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(prefix + str); + } + } + + private Optional stdoutLabel(boolean negate) { + if ((stdout.discard() && !negate) || (!stdout.discard() && negate)) { + return Optional.of("out"); + } else { + return Optional.empty(); + } + } + + private Optional stderrLabel(boolean negate) { + if ((stderr.discard() && !negate) || (!stderr.discard() && negate)) { + return Optional.of("err"); + } else { + return Optional.empty(); + } + } + } + + private record CachingPrintStream(PrintStream ps, Optional buf) { + CachingPrintStream { + Objects.requireNonNull(ps); + Objects.requireNonNull(buf); + } + + Optional bufferContents() { + return buf.map(ByteArrayOutputStream::toString); + } + + static Builder build(Charset charset) { + return new Builder(charset); + } + + static final class Builder { + + private Builder(Charset charset) { + this.charset = Objects.requireNonNull(charset); + } + + Builder save(boolean v) { + save = v; + return this; + } + + Builder discard(boolean v) { + discard = v; + return this; + } + + Builder dumpStream(PrintStream v) { + dumpStream = v; + return this; + } + + Builder buffer(ByteArrayOutputStream v) { + externalBuffer = v; + return this; + } + + CachingPrintStream create() { + final Optional buf; + if (save && !discard) { + buf = Optional.ofNullable(externalBuffer).or(() -> { + return Optional.of(new ByteArrayOutputStream()); + }); + } else { + buf = Optional.empty(); + } + + final PrintStream ps; + if (buf.isPresent() && dumpStream != null) { + ps = new PrintStream(new TeeOutputStream(List.of(buf.get(), dumpStream)), true, dumpStream.charset()); + } else if (!discard) { + ps = buf.map(in -> { + return new PrintStream(in, false, charset); + }).or(() -> { + return Optional.ofNullable(dumpStream); + }).orElseGet(CommandOutputControl::nullPrintStream); + } else { + ps = nullPrintStream(); + } + + return new CachingPrintStream(ps, buf); + } + + private boolean save; + private boolean discard; + private PrintStream dumpStream; + private ByteArrayOutputStream externalBuffer; + private final Charset charset; + } + } + + private final class CachingStreamsConfig { + + CachingStreamsConfig() { + out = outputStreamsControl.stdout().buildCachingPrintStream(dumpStdout(), charset()).create(); + if (isRedirectStderr()) { + var builder = outputStreamsControl.stderr().buildCachingPrintStream(dumpStdout(), charset()); + out.buf().ifPresent(builder::buffer); + err = builder.create(); + } else { + err = outputStreamsControl.stderr().buildCachingPrintStream(dumpStderr(), charset()).create(); + } + } + + Result createResult(Optional exitCode, ExecutableAttributes execAttrs) throws IOException { + + CommandOutput> output; + CommandOutput byteOutput; + + CachingPrintStream effectiveOut; + if (out.buf().isEmpty() && isRedirectStderr()) { + effectiveOut = new CachingPrintStream(nullPrintStream(), err.buf()); + } else { + effectiveOut = out; + } + + if (isBinaryOutput()) { + Optional outContent, errContent; + if (isRedirectStderr()) { + outContent = readBinary(outputStreamsControl.stdout(), effectiveOut).map(ByteContent::new); + errContent = Optional.empty(); + } else { + outContent = readBinary(outputStreamsControl.stdout(), out).map(ByteContent::new); + errContent = readBinary(outputStreamsControl.stderr(), err).map(ByteContent::new); + } + + byteOutput = combine(outContent, errContent, redirectRetainedStderr()); + output = null; + } else { + Optional outContent, errContent; + if (isRedirectStderr()) { + outContent = read(outputStreamsControl.stdout(), effectiveOut).map(StringListContent::new); + errContent = Optional.empty(); + } else { + outContent = read(outputStreamsControl.stdout(), out).map(StringListContent::new); + errContent = read(outputStreamsControl.stderr(), err).map(StringListContent::new); + } + + output = combine(outContent, errContent, redirectRetainedStderr()); + byteOutput = null; + } + + return new Result(exitCode, Optional.ofNullable(output), Optional.ofNullable(byteOutput), execAttrs); + } + + PrintStream out() { + return out.ps(); + } + + PrintStream err() { + return err.ps(); + } + + private final CachingPrintStream out; + private final CachingPrintStream err; + } + + private static final class OutputControl { + + OutputControl() { + } + + private OutputControl(OutputControl other) { + dump = other.dump; + discard = other.discard; + save = other.save; + } + + boolean save() { + return save.isPresent(); + } + + boolean saveAll() { + return save.orElse(null) == OutputControlOption.SAVE_ALL; + } + + boolean saveFirstLine() { + return save.orElse(null) == OutputControlOption.SAVE_FIRST_LINE; + } + + boolean discard() { + return discard || (!dump && save.isEmpty()); + } + + boolean dump() { + return !discard && dump; + } + + OutputControl dump(boolean v) { + this.dump = v; + return this; + } + + OutputControl discard(boolean v) { + this.discard = v; + return this; + } + + OutputControl saveAll(boolean v) { + if (v) { + save = Optional.of(OutputControlOption.SAVE_ALL); + } else { + save = Optional.empty(); + } + return this; + } + + OutputControl saveFirstLine(boolean v) { + if (v) { + save = Optional.of(OutputControlOption.SAVE_FIRST_LINE); + } else { + save = Optional.empty(); + } + return this; + } + + OutputControl set(boolean set, OutputControlOption v) { + switch (v) { + case DUMP -> dump(set); + case SAVE_ALL -> saveAll(set); + case SAVE_FIRST_LINE -> saveFirstLine(set); + } + return this; + } + + OutputControl copy() { + return new OutputControl(this); + } + + ProcessBuilder.Redirect asProcessBuilderRedirect() { + if (discard()) { + return ProcessBuilder.Redirect.DISCARD; + } else if (dump && !save()) { + return ProcessBuilder.Redirect.INHERIT; + } else { + return ProcessBuilder.Redirect.PIPE; + } + } + + CachingPrintStream.Builder buildCachingPrintStream(PrintStream dumpStream, Charset charset) { + Objects.requireNonNull(dumpStream); + final var builder = CachingPrintStream.build(charset).save(save()).discard(discard()); + if (dump()) { + builder.dumpStream(dumpStream); + } + return builder; + } + + private boolean dump; + private boolean discard; + private Optional save = Optional.empty(); + } + + private record CommandOutput(Optional> content, int stdoutContentSize, boolean interleaved) { + + CommandOutput { + Objects.requireNonNull(content); + if (interleaved) { + stdoutContentSize = content.map(Content::size).orElse(-1); + } + } + + CommandOutput() { + this(Optional.empty(), 0, false); + } + + Optional combined() { + return content.map(Content::data); + } + + /** + * Returns non-empty {@code Optional} if stdout is available and stdout and stderr are not interleaved. + * @return stdout if it can be extracted from the combined output + */ + Optional stdout() { + if (withoutExtractableStdout()) { + return Optional.empty(); + } + + final var theContent = content.orElseThrow(); + if (stdoutContentSize == theContent.size()) { + return combined(); + } else { + return Optional.of(theContent.slice(0, Integer.min(stdoutContentSize, theContent.size())).data()); + } + } + + /** + * Returns non-empty {@code Optional} if stderr is available and stdout and stderr are not interleaved. + * @return stderr if it can be extracted from the combined output + */ + Optional stderr() { + if (withoutExtractableStderr()) { + return Optional.empty(); + } else if (stdoutContentSize <= 0) { + return combined(); + } else { + final var theContent = content.orElseThrow(); + return Optional.of(theContent.slice(stdoutContentSize, theContent.size()).data()); + } + } + + @SuppressWarnings("unchecked") + static CommandOutput empty() { + return (CommandOutput)EMPTY; + } + + private boolean withoutExtractableStdout() { + return interleaved || content.isEmpty() || stdoutContentSize < 0; + } + + private boolean withoutExtractableStderr() { + return interleaved || content.isEmpty() || stdoutContentSize > content.get().size(); + } + + private static final CommandOutput EMPTY = new CommandOutput<>(); + } + + private record ToolProviderExecutable(ToolProvider tp, List args, CommandOutputControl coc) implements Executable { + + ToolProviderExecutable { + Objects.requireNonNull(tp); + Objects.requireNonNull(args); + Objects.requireNonNull(coc); + } + + @Override + public Result execute() throws IOException, InterruptedException { + return coc.execute(tp, -1, args.toArray(String[]::new)); + } + + @Override + public Result execute(long timeout, TimeUnit unit) throws IOException, InterruptedException { + return coc.execute(tp, unit.toMillis(timeout), args.toArray(String[]::new)); + } + + @Override + public ExecutableAttributes attributes() { + return new ToolProviderAttributes(tp.name(), args); + } + } + + private record ProcessExecutable(ProcessBuilder pb, CommandOutputControl coc) implements Executable { + + ProcessExecutable { + Objects.requireNonNull(pb); + Objects.requireNonNull(coc); + } + + @Override + public Result execute() throws IOException, InterruptedException { + return coc.execute(pb, -1L); + } + + @Override + public Result execute(long timeout, TimeUnit unit) throws IOException, InterruptedException { + return coc.execute(pb, unit.toMillis(timeout)); + } + + @Override + public ExecutableAttributes attributes() { + return new ProcessAttributes(Optional.empty(), pb.command()); + } + } + + private static Optional getPID(Process p) { + try { + return Optional.of(p.pid()); + } catch (UnsupportedOperationException ex) { + return Optional.empty(); + } + } + + private static void suppressIOException(ThrowingRunnable r) { + try { + r.run(); + } catch (IOException ex) {} + } + + private static void suppressIOException(ThrowingConsumer c, T value) { + suppressIOException(() -> { + c.accept(value); + }); + } + + private int flags; + private final OutputStreamsControl outputStreamsControl; + private PrintStream dumpStdout; + private PrintStream dumpStderr; + private Charset charset; + private Consumer processListener; + + // Executor to run subprocess output stream gobblers. + // Output stream gobblers should start fetching output streams ASAP after the process starts. + // No pooling, no waiting. + // CompletableFuture#runAsync() method starts an output stream gobbler. + // If used with the default executor, it is known to make WiX3 light.exe create + // a locked msi file when multiple jpackage tool providers are executed asynchronously. + // The AsyncTest fails with cryptic java.nio.file.FileSystemException error: + // jtreg_open_test_jdk_tools_jpackage_share_AsyncTest_java\\tmp\\jdk.jpackage8108811639097525318\\msi\\Foo-1.0.msi: The process cannot access the file because it is being used by another process. + // The remedy for the problem is to use non-pooling executor to run subprocess output stream gobblers. + private final java.util.concurrent.Executor gobblerExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + private enum OutputControlOption { + SAVE_ALL, SAVE_FIRST_LINE, DUMP + } + + private enum Flag { + DUMP (0x01), + REDIRECT_STDERR (0x02), + BINARY_OUTPUT (0x04), + STORE_OUTPUT_IN_FILES (0x08), + DISCARD_STDOUT (0x10), + DISCARD_STDERR (0x20), + ; + + Flag(int value) { + this.value = value; + } + + int set(int flags, boolean set) { + if (set) { + return flags | value; + } else { + return flags & ~value; + } + } + + boolean isSet(int flags) { + return (flags & value) != 0; + } + + private final int value; + } +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Enquoter.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Enquoter.java similarity index 77% rename from src/jdk.jpackage/share/classes/jdk/jpackage/internal/Enquoter.java rename to src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Enquoter.java index 51f97ad2cd6..d9ededcbadb 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/Enquoter.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/Enquoter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -22,7 +22,7 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ -package jdk.jpackage.internal; +package jdk.jpackage.internal.util; import java.util.Optional; import java.util.function.BiConsumer; @@ -32,39 +32,43 @@ import java.util.regex.Pattern; /** * Add quotes to the given string in a configurable way. */ -final class Enquoter { +public final class Enquoter { private Enquoter() { setQuoteChar('"'); } - static Enquoter forPropertyValues() { + public static Enquoter identity() { + return new Enquoter(); + } + + public static Enquoter forPropertyValues() { return new Enquoter() .setEnquotePredicate(QUOTE_IF_WHITESPACES) .setEscaper(PREPEND_BACKSLASH); } - static Enquoter forShellLiterals() { + public static Enquoter forShellLiterals() { return forShellLiterals('\''); } - static Enquoter forShellLiterals(char quoteChar) { + public static Enquoter forShellLiterals(char quoteChar) { return new Enquoter() .setQuoteChar(quoteChar) .setEnquotePredicate(x -> true) .setEscaper(PREPEND_BACKSLASH); } - String applyTo(String v) { + public String applyTo(String v) { if (!needQuotes.test(v)) { return v; } else { var buf = new StringBuilder(); buf.appendCodePoint(beginQuoteChr); - Optional.of(escaper).ifPresentOrElse(op -> { + Optional.ofNullable(escaper).ifPresentOrElse(op -> { v.codePoints().forEachOrdered(chr -> { if (chr == beginQuoteChr || chr == endQuoteChr) { - escaper.accept(chr, buf); + op.accept(chr, buf); } else { buf.appendCodePoint(chr); } @@ -77,28 +81,23 @@ final class Enquoter { } } - Enquoter setQuoteChar(char chr) { + public Enquoter setQuoteChar(char chr) { beginQuoteChr = chr; endQuoteChr = chr; return this; } - Enquoter setEscaper(BiConsumer v) { + public Enquoter setEscaper(BiConsumer v) { escaper = v; return this; } - Enquoter setEnquotePredicate(Predicate v) { + public Enquoter setEnquotePredicate(Predicate v) { needQuotes = v; return this; } - private int beginQuoteChr; - private int endQuoteChr; - private BiConsumer escaper; - private Predicate needQuotes = str -> false; - - private static final Predicate QUOTE_IF_WHITESPACES = new Predicate() { + public static final Predicate QUOTE_IF_WHITESPACES = new Predicate() { @Override public boolean test(String t) { return pattern.matcher(t).find(); @@ -106,8 +105,13 @@ final class Enquoter { private final Pattern pattern = Pattern.compile("\\s"); }; - private static final BiConsumer PREPEND_BACKSLASH = (chr, buf) -> { + public static final BiConsumer PREPEND_BACKSLASH = (chr, buf) -> { buf.append('\\'); buf.appendCodePoint(chr); }; + + private int beginQuoteChr; + private int endQuoteChr; + private BiConsumer escaper; + private Predicate needQuotes = str -> false; } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java index 2c693939dab..4c85358d424 100644 --- a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/PListReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -35,7 +35,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; -import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.dom.DOMSource; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; @@ -311,7 +310,7 @@ public final class PListReader { } } - public PListReader(byte[] xmlData) throws ParserConfigurationException, SAXException, IOException { + public PListReader(byte[] xmlData) throws SAXException, IOException { this(XmlUtils.initDocumentBuilder().parse(new ByteArrayInputStream(xmlData))); } diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/RetryExecutor.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/RetryExecutor.java new file mode 100644 index 00000000000..c67373e8233 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/RetryExecutor.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2020, 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal.util; + +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; + +import java.time.Duration; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.internal.util.function.ThrowingFunction; +import jdk.jpackage.internal.util.function.ThrowingSupplier; + +public class RetryExecutor { + + public RetryExecutor(Class exceptionType) { + this.exceptionType = Objects.requireNonNull(exceptionType); + setMaxAttemptsCount(5); + setAttemptTimeout(2, TimeUnit.SECONDS); + } + + final public Class exceptionType() { + return exceptionType; + } + + public RetryExecutor setExecutable(ThrowingFunction>, T, E> v) { + executable = v; + return this; + } + + final public RetryExecutor setExecutable(ThrowingSupplier v) { + if (v != null) { + setExecutable(_ -> { + return v.get(); + }); + } else { + executable = null; + } + return this; + } + + public RetryExecutor setMaxAttemptsCount(int v) { + attempts = v; + return this; + } + + final public RetryExecutor setAttemptTimeout(long v, TimeUnit unit) { + return setAttemptTimeout(Duration.of(v, unit.toChronoUnit())); + } + + public RetryExecutor setAttemptTimeout(Duration v) { + timeout = v; + return this; + } + + public RetryExecutor setExceptionMapper(Function v) { + toUnchecked = v; + return this; + } + + public RetryExecutor setSleepFunction(Consumer v) { + sleepFunction = v; + return this; + } + + final public RetryExecutor mutate(Consumer> mutator) { + mutator.accept(this); + return this; + } + + public T execute() throws E { + var curExecutable = executable(); + T result = null; + var attemptIter = new DefaultContext(); + while (attemptIter.hasNext()) { + attemptIter.next(); + try { + result = curExecutable.apply(attemptIter); + break; + } catch (Exception ex) { + if (!exceptionType.isInstance(ex)) { + throw ExceptionBox.toUnchecked(ex); + } else if (attemptIter.isLastAttempt()) { + // No more attempts left. This is fatal. + throw exceptionType.cast(ex); + } else { + curExecutable = executable(); + } + } + + sleep(); + } + + return result; + } + + final public T executeUnchecked() { + try { + return execute(); + } catch (Error | RuntimeException t) { + throw t; + } catch (Exception ex) { + if (exceptionType.isInstance(ex)) { + throw Optional.ofNullable(toUnchecked).orElse(ExceptionBox::toUnchecked).apply(exceptionType.cast(ex)); + } else { + // Unreachable unless it is a direct subclass of Throwable, + // which is not Error or Exception which should not happen. + throw ExceptionBox.reachedUnreachable(); + } + } + } + + public interface Context { + boolean isLastAttempt(); + int attempt(); + T executor(); + } + + private final class DefaultContext implements Context>, Iterator { + + @Override + public boolean isLastAttempt() { + return !hasNext(); + } + + @Override + public int attempt() { + return attempt; + } + + @Override + public boolean hasNext() { + return (attempts - attempt) > 1; + } + + @Override + public Void next() { + attempt++; + return null; + } + + @Override + public RetryExecutor executor() { + return RetryExecutor.this; + } + + private int attempt = -1; + } + + private ThrowingFunction>, T, E> executable() { + return Optional.ofNullable(executable).orElseThrow(() -> { + return new IllegalStateException("No executable"); + }); + } + + private void sleep() { + Optional.ofNullable(timeout).ifPresent(Optional.ofNullable(sleepFunction).orElseGet(() -> { + return toConsumer(Thread::sleep); + })); + } + + private final Class exceptionType; + private ThrowingFunction>, T, E> executable; + private int attempts; + private Duration timeout; + private Function toUnchecked; + private Consumer sleepFunction; +} diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TeeOutputStream.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TeeOutputStream.java new file mode 100644 index 00000000000..db098eb2b48 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/TeeOutputStream.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2026, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ +package jdk.jpackage.internal.util; + +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Objects; +import jdk.jpackage.internal.util.function.ThrowingConsumer; + +public final class TeeOutputStream extends OutputStream { + + public TeeOutputStream(Iterable items) { + items.forEach(Objects::requireNonNull); + this.items = items; + } + + @Override + public void write(int b) throws IOException { + for (final var item : items) { + item.write(b); + } + } + + @Override + public void write(byte[] b) throws IOException { + for (final var item : items) { + item.write(b); + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (final var item : items) { + item.write(b, off, len); + } + } + + @Override + public void flush() throws IOException { + forEach(Flushable::flush); + } + + @Override + public void close() throws IOException { + forEach(Closeable::close); + } + + private void forEach(ThrowingConsumer c) throws IOException { + IOException firstEx = null; + for (final var item : items) { + try { + c.accept(item); + } catch (IOException e) { + if (firstEx == null) { + firstEx = e; + } + } + } + if (firstEx != null) { + throw firstEx; + } + } + + private final Iterable items; +} diff --git a/src/jdk.jpackage/unix/classes/jdk/jpackage/internal/UnixLaunchersAsServices.java b/src/jdk.jpackage/unix/classes/jdk/jpackage/internal/UnixLaunchersAsServices.java index e1e02ba7850..b196a5805a0 100644 --- a/src/jdk.jpackage/unix/classes/jdk/jpackage/internal/UnixLaunchersAsServices.java +++ b/src/jdk.jpackage/unix/classes/jdk/jpackage/internal/UnixLaunchersAsServices.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -24,8 +24,6 @@ */ package jdk.jpackage.internal; -import jdk.jpackage.internal.model.Launcher; -import jdk.jpackage.internal.model.Application; import java.io.IOException; import java.nio.file.Path; import java.util.Collections; @@ -36,6 +34,9 @@ import java.util.Objects; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; +import jdk.jpackage.internal.model.Application; +import jdk.jpackage.internal.model.Launcher; +import jdk.jpackage.internal.util.Enquoter; /** * Helper to install launchers as services for Unix installers. diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java index dc9891f154a..de52a222d7d 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WinBundlingEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -31,16 +31,17 @@ import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_WIN_EXE import static jdk.jpackage.internal.cli.StandardBundlingOperation.CREATE_WIN_MSI; import jdk.jpackage.internal.cli.Options; -import jdk.jpackage.internal.util.Result; public class WinBundlingEnvironment extends DefaultBundlingEnvironment { public WinBundlingEnvironment() { - super(build() - .defaultOperation(CREATE_WIN_EXE) - .bundler(CREATE_WIN_APP_IMAGE, WinBundlingEnvironment::createAppImage) - .bundler(CREATE_WIN_EXE, LazyLoad::sysEnv, WinBundlingEnvironment::createExePackage) - .bundler(CREATE_WIN_MSI, LazyLoad::sysEnv, WinBundlingEnvironment::createMsiPackage)); + super(build().mutate(builder -> { + var sysEnv = runOnce(WinSystemEnvironment::create); + + builder + .bundler(CREATE_WIN_EXE, sysEnv, WinBundlingEnvironment::createExePackage) + .bundler(CREATE_WIN_MSI, sysEnv, WinBundlingEnvironment::createMsiPackage); + }).defaultOperation(CREATE_WIN_EXE).bundler(CREATE_WIN_APP_IMAGE, WinBundlingEnvironment::createAppImage)); } private static void createMsiPackage(Options options, WinSystemEnvironment sysEnv) { @@ -98,12 +99,4 @@ public class WinBundlingEnvironment extends DefaultBundlingEnvironment { } } - private static final class LazyLoad { - - static Result sysEnv() { - return SYS_ENV; - } - - private static final Result SYS_ENV = WinSystemEnvironment.create(); - } } diff --git a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java index e2ed175fbf3..c4f8610312a 100644 --- a/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java +++ b/src/jdk.jpackage/windows/classes/jdk/jpackage/internal/WixTool.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -233,10 +233,10 @@ public enum WixTool { // Detect FIPS mode var fips = false; try { - final var exec = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true); - final var exitCode = exec.execute(); + final var result = Executor.of(toolPath.toString(), "-?").setQuiet(true).saveOutput(true).execute(); + final var exitCode = result.getExitCode(); if (exitCode != 0 /* 308 */) { - final var output = exec.getOutput(); + final var output = result.getOutput(); if (!output.isEmpty() && output.get(0).contains("error CNDL0308")) { fips = true; } diff --git a/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlPage.java b/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlPage.java index d8fd13fdd7c..af28135e673 100644 --- a/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlPage.java +++ b/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlPage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -23,19 +23,50 @@ package jdk.test.failurehandler; +import java.io.FileWriter; +import java.io.IOException; import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Objects; public class HtmlPage implements AutoCloseable { + static final String STYLE_SHEET_FILENAME = "failure-handler-style.css"; + static final String SCRIPT_FILENAME = "failure-handler-script.js"; + private final PrintWriter writer; private final HtmlSection rootSection; - public HtmlPage(PrintWriter writer) { - Objects.requireNonNull(writer, "writer cannot be null"); - this.writer = writer; + /** + * Constructs a {@code HtmlPage} + * + * @param dir The directory into which the HTML file and related resources will be created + * @param htmlFileName The HTML file name + * @param append if {@code true} then the content will be appended to the file represented + * by the {@code htmlFileName}, else the {@code htmlFileName} will be overwritten + * with the new content + * @throws IllegalArgumentException if {@code dir} is not a directory or if the + * {@code htmlFileName} is {@linkplain String#isBlank() blank} + * @throws IOException if there is an error constructing file resource(s) for this HTML page + */ + public HtmlPage(final Path dir, final String htmlFileName, final boolean append) + throws IOException { + Objects.requireNonNull(dir, "directory cannot be null"); + Objects.requireNonNull(htmlFileName, "HTML file name cannot be null"); + if (!Files.isDirectory(dir)) { + throw new IllegalArgumentException(dir + " is not a directory"); + } + if (htmlFileName.isBlank()) { + throw new IllegalArgumentException("HTML file name cannot be blank"); + } + final FileWriter fileWriter = new FileWriter(dir.resolve(htmlFileName).toFile(), append); + this.writer = new PrintWriter(fileWriter, true); + createScriptFile(dir); + createStyleSheetFile(dir); rootSection = new HtmlSection(writer); } + @Override public void close() { writer.close(); @@ -44,4 +75,71 @@ public class HtmlPage implements AutoCloseable { public HtmlSection getRootSection() { return rootSection; } + + private static void createStyleSheetFile(final Path destDir) throws IOException { + final Path styleSheet = destDir.resolve(STYLE_SHEET_FILENAME); + if (Files.exists(styleSheet)) { + return; + } + final String content = """ + div { display:none;} + """; + Files.writeString(styleSheet, content); + } + + private static void createScriptFile(final Path destDir) throws IOException { + final Path script = destDir.resolve(SCRIPT_FILENAME); + if (Files.exists(script)) { + return; + } + final String content = """ + function doShow(e) { + while (e != null) { + if (e.tagName == 'DIV') { + e.style.display = 'block'; + } + e = e.parentNode; + } + } + + function showHandler(event) { + elementId = this.dataset.show; + elementToShow = document.getElementById(elementId); + doShow(elementToShow); + } + + function toggleHandler(event) { + toggleElementId = this.dataset.toggle; + elementToToggle = document.getElementById(toggleElementId); + d = elementToToggle.style.display; + if (d == 'block') { + elementToToggle.style.display = 'none'; + } else { + doShow(elementToToggle); + } + } + + function bodyLoadHandler() { + const index = location.href.indexOf("#"); + if (index != -1) { + doShow(document.getElementById(location.href.substring(index + 1))); + } + // elements that require the "toggleHandler" function to be registered + // as an event handler for the onclick event + const requiringToggleHandler = document.querySelectorAll("[data-toggle]"); + for (const e of requiringToggleHandler) { + e.addEventListener("click", toggleHandler); + } + // elements that require the "showHandler" function to be registered + // as an event handler for the onclick event + const requiringShowHandler = document.querySelectorAll("[data-show]"); + for (const e of requiringShowHandler) { + e.addEventListener("click", showHandler); + } + } + // register a onload event handler + window.addEventListener("DOMContentLoaded", bodyLoadHandler); + """; + Files.writeString(script, content); + } } diff --git a/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlSection.java b/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlSection.java index 141caf450bd..53ffab95a1e 100644 --- a/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlSection.java +++ b/test/failure_handler/src/share/classes/jdk/test/failurehandler/HtmlSection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -57,41 +57,15 @@ public class HtmlSection { if (rootSection == null) { this.rootSection = this; this.pw.println(""); - this.pw.println("\n" - + "\n" - + "\n" - + ""); - this.pw.println(""); + this.pw.println(""); + this.pw.println( + ""); + this.pw.println( + ""); + this.pw.println(""); + + this.pw.println(""); } else { this.rootSection = rootSection; this.pw.print("

    "); @@ -146,7 +120,7 @@ public class HtmlSection { } else if (child != null) { path = String.format("%s.%s", path, child); } - pw.printf("%2$s%n", + pw.printf("%2$s%n", path, name); } @@ -188,7 +162,7 @@ public class HtmlSection { : String.format("%s.%s", parent.id, name), name, rootSection); this.parent = parent; - pw.printf("
  • %2$s
    ",
    +            pw.printf("
  • %2$s
    ",
                         id, name);
             }
     
    diff --git a/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherDiagnosticInfoObserver.java b/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherDiagnosticInfoObserver.java
    index 96c5f4f2858..e63e55888d7 100644
    --- a/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherDiagnosticInfoObserver.java
    +++ b/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherDiagnosticInfoObserver.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2015, 2021, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2015, 2026, 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
    @@ -105,9 +105,7 @@ public class GatherDiagnosticInfoObserver implements Harness.Observer {
     
         private void gatherCoreInfo(Path workDir, String name, Path core, PrintWriter log,
                                    CoreInfoGatherer gatherer) {
    -        File output = workDir.resolve(CORES_OUTPUT).toFile();
    -        try (HtmlPage html = new HtmlPage(new PrintWriter(
    -                new FileWriter(output, true), true))) {
    +        try (HtmlPage html = new HtmlPage(workDir, CORES_OUTPUT, true)) {
                 try (ElapsedTimePrinter timePrinter
                              = new ElapsedTimePrinter(new Stopwatch(), name, log)) {
                     gatherer.gatherCoreInfo(html.getRootSection(), core);
    @@ -121,9 +119,7 @@ public class GatherDiagnosticInfoObserver implements Harness.Observer {
     
         private void gatherEnvInfo(Path workDir, String name, PrintWriter log,
                                    EnvironmentInfoGatherer gatherer) {
    -        File output = workDir.resolve(ENVIRONMENT_OUTPUT).toFile();
    -        try (HtmlPage html = new HtmlPage(new PrintWriter(
    -                new FileWriter(output, true), true))) {
    +        try (HtmlPage html = new HtmlPage(workDir, ENVIRONMENT_OUTPUT, true)) {
                 try (ElapsedTimePrinter timePrinter
                              = new ElapsedTimePrinter(new Stopwatch(), name, log)) {
                     gatherer.gatherEnvironmentInfo(html.getRootSection());
    diff --git a/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherProcessInfoTimeoutHandler.java b/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherProcessInfoTimeoutHandler.java
    index 63ed7e5f2c7..c1f1ddfb69f 100644
    --- a/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherProcessInfoTimeoutHandler.java
    +++ b/test/failure_handler/src/share/classes/jdk/test/failurehandler/jtreg/GatherProcessInfoTimeoutHandler.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2015, 2026, 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
    @@ -69,16 +69,7 @@ public class GatherProcessInfoTimeoutHandler extends TimeoutHandler {
             }
             try {
                 actionsLog.printf("%s ---%n", name);
    -
    -            File output = workDir.resolve(OUTPUT_FILENAME).toFile();
    -            try {
    -                PrintWriter pw = new PrintWriter(new FileWriter(output, true), true);
    -                runGatherer(name, workDir, actionsLog, pw, pid);
    -            } catch (IOException e) {
    -                actionsLog.printf("IOException: cannot open output file[%s] : %s",
    -                        output, e.getMessage());
    -                e.printStackTrace(actionsLog);
    -            }
    +            runGatherer(name, actionsLog, pid);
             } finally {
                 actionsLog.printf("--- %s%n", name);
                 // don't close jtreg log
    @@ -90,9 +81,9 @@ public class GatherProcessInfoTimeoutHandler extends TimeoutHandler {
             }
         }
     
    -    private void runGatherer(String name, Path workDir, PrintWriter log,
    -                             PrintWriter out, long pid) {
    -        try (HtmlPage html = new HtmlPage(out)) {
    +    private void runGatherer(String name, PrintWriter log, long pid) {
    +        Path workDir = outputDir.toPath();
    +        try (HtmlPage html = new HtmlPage(workDir, OUTPUT_FILENAME, true)) {
                 ProcessInfoGatherer gatherer = new GathererFactory(
                         OS.current().family,
                         workDir, log, testJdk.toPath()).getProcessInfoGatherer();
    diff --git a/test/hotspot/gtest/gtestMain.cpp b/test/hotspot/gtest/gtestMain.cpp
    index 842b6547b48..a1869ab5499 100644
    --- a/test/hotspot/gtest/gtestMain.cpp
    +++ b/test/hotspot/gtest/gtestMain.cpp
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2016, 2026, 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
    @@ -22,6 +22,7 @@
      *
      */
     
    +#include "cppstdlib/cstdlib.hpp"
     #include "jni.h"
     #include "runtime/os.hpp"
     #include "runtime/thread.inline.hpp"
    @@ -31,7 +32,6 @@
     
     #include 
     #include 
    -#include 
     #ifdef __APPLE__
     #include 
     #endif
    diff --git a/test/hotspot/gtest/unittest.hpp b/test/hotspot/gtest/unittest.hpp
    index 336a2f0766e..6a2cd400cb6 100644
    --- a/test/hotspot/gtest/unittest.hpp
    +++ b/test/hotspot/gtest/unittest.hpp
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2016, 2026, 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
    @@ -24,9 +24,9 @@
     #ifndef UNITTEST_HPP
     #define UNITTEST_HPP
     
    +#include "cppstdlib/cstdlib.hpp"
     #include "utilities/globalDefinitions.hpp"
     
    -#include 
     #include 
     
     #define GTEST_DONT_DEFINE_TEST 1
    diff --git a/test/hotspot/gtest/utilities/test_bitMap_setops.cpp b/test/hotspot/gtest/utilities/test_bitMap_setops.cpp
    index 2b1e67533df..1def9c89cfc 100644
    --- a/test/hotspot/gtest/utilities/test_bitMap_setops.cpp
    +++ b/test/hotspot/gtest/utilities/test_bitMap_setops.cpp
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2016, 2026, 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
    @@ -21,6 +21,7 @@
      * questions.
      */
     
    +#include "cppstdlib/cstdlib.hpp"
     #include "runtime/os.hpp"
     #include "utilities/align.hpp"
     #include "utilities/bitMap.inline.hpp"
    @@ -29,8 +30,6 @@
     #include "utilities/globalDefinitions.hpp"
     #include "unittest.hpp"
     
    -#include 
    -
     typedef BitMap::idx_t idx_t;
     typedef BitMap::bm_word_t bm_word_t;
     
    diff --git a/test/hotspot/jtreg/ProblemList.txt b/test/hotspot/jtreg/ProblemList.txt
    index 4e16e328ab4..13e1ea30a34 100644
    --- a/test/hotspot/jtreg/ProblemList.txt
    +++ b/test/hotspot/jtreg/ProblemList.txt
    @@ -77,8 +77,6 @@ compiler/interpreter/Test6833129.java 8335266 generic-i586
     
     compiler/c2/aarch64/TestStaticCallStub.java 8359963 linux-aarch64,macosx-aarch64
     
    -serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java 8369150 generic-all
    -
     #############################################################################
     
     # :hotspot_gc
    diff --git a/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java b/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java
    index 5d20ce659b9..825d5ca60bc 100644
    --- a/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java
    +++ b/test/hotspot/jtreg/compiler/loopopts/superword/TestAliasingFuzzer.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2024, 2026, 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
    @@ -1052,35 +1052,20 @@ public class TestAliasingFuzzer {
                         case Aliasing.CONTAINER_DIFFERENT,
                              Aliasing.CONTAINER_SAME_ALIASING_NEVER,
                              Aliasing.CONTAINER_UNKNOWN_ALIASING_NEVER ->
    -                        // We would have liked to check that there is no multiversioning.
    -                        //
    -                        // But sadly there are some cases that have issues with RCE and/or
    -                        // predicates, and so we end up using multiversioning anyway. We
    -                        // should fix those cases eventually, to strengthen the checks here.
    -                        //
    -                        // The array cases are a little more tame, and do not have the same
    -                        // issues as the MemorySegment cases.
    -                        (containerKind == ContainerKind.ARRAY)
    -                        ? """
    -                          // Aliasing check should never fail at runtime, so the predicate
    -                          // should never fail, and we do not have to use multiversioning.
    -                          // Failure could have a few causes:
    -                          // - issues with doing RCE / missing predicates
    -                          //   -> other loop-opts need to be fixed
    -                          // - predicate fails: recompile with multiversioning
    -                          //   -> logic in runtime check may be wrong
    -                          @IR(counts = {".*multiversion.*", "= 0"},
    -                              phase = CompilePhase.PRINT_IDEAL,
    -                              applyIf = {"UseAutoVectorizationPredicate", "true"},
    -                              applyIfPlatform = {"64-bit", "true"},
    -                              applyIfCPUFeatureOr = {"sse4.1", "true", "asimd", "true"})
    -                          """
    -                        : """
    -                          // Due to cases like JDK-8360204 and JDK-8365982, there can be issues
    -                          // with RCE leading cases where we remove predicates and then unroll again
    -                          // and then end up multiversioning. These cases seem relatively rare but
    -                          // prevent us from asserting that there is never multiversioning in these cases.
    -                          """;
    +                            """
    +                            // Aliasing check should never fail at runtime, so the predicate
    +                            // should never fail, and we do not have to use multiversioning.
    +                            // Failure could have a few causes:
    +                            // - issues with doing RCE / missing predicates
    +                            //   -> other loop-opts need to be fixed
    +                            // - predicate fails: recompile with multiversioning
    +                            //   -> logic in runtime check may be wrong
    +                            @IR(counts = {".*multiversion.*", "= 0"},
    +                                phase = CompilePhase.PRINT_IDEAL,
    +                                applyIf = {"UseAutoVectorizationPredicate", "true"},
    +                                applyIfPlatform = {"64-bit", "true"},
    +                                applyIfCPUFeatureOr = {"sse4.1", "true", "asimd", "true"})
    +                            """;
                        case Aliasing.CONTAINER_SAME_ALIASING_UNKNOWN,
                              Aliasing.CONTAINER_UNKNOWN_ALIASING_UNKNOWN ->
                                 """
    diff --git a/test/hotspot/jtreg/compiler/vectorization/TestSubwordTruncation.java b/test/hotspot/jtreg/compiler/vectorization/TestSubwordTruncation.java
    index 26a49aba7bf..53c1b89a203 100644
    --- a/test/hotspot/jtreg/compiler/vectorization/TestSubwordTruncation.java
    +++ b/test/hotspot/jtreg/compiler/vectorization/TestSubwordTruncation.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2025, 2026, 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
    @@ -479,6 +479,32 @@ public class TestSubwordTruncation {
             return new Object[] { in, res };
         }
     
    +    @Test
    +    @IR(counts = { IRNode.CAST_II, ">0" })
    +    @Warmup(0)
    +    public Object[] testCastII() {
    +        byte[] bytes = new byte[400];
    +        intField = 6;
    +        int i = 0;
    +        int j = 1;
    +
    +        do {
    +            bytes[j] = (byte) i;
    +            int k = 1;
    +
    +            do {
    +                i <<= intField;
    +                i += (k ^ i);
    +                i -= j;
    +
    +                for (int u = 1; 1 > u; u++) {
    +                }
    +            } while (++k < 8);
    +        } while (++j < 191);
    +
    +        return new Object[] { bytes };
    +    }
    +
         public static void main(String[] args) {
             TestFramework.run();
         }
    diff --git a/test/hotspot/jtreg/containers/docker/ShareTmpDir.java b/test/hotspot/jtreg/containers/docker/ShareTmpDir.java
    index bbf08c0dae7..986cd044737 100644
    --- a/test/hotspot/jtreg/containers/docker/ShareTmpDir.java
    +++ b/test/hotspot/jtreg/containers/docker/ShareTmpDir.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2022, 2026, 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
    @@ -88,6 +88,11 @@ public class ShareTmpDir {
                 };
             t1.start();
     
    +        while (!started1.exists()) {
    +            System.out.println("Waiting for first JVM to start");
    +            Thread.sleep(1000);
    +        }
    +
             Thread t2 = new Thread() {
                     public void run() {
                         synchronized(lock) {
    @@ -98,8 +103,8 @@ public class ShareTmpDir {
                 };
             t2.start();
     
    -        while (!started1.exists() || !started2.exists()) {
    -            System.out.println("Waiting for all two JVMs to start");
    +        while (!started2.exists()) {
    +            System.out.println("Waiting for second JVM to start");
                 Thread.sleep(1000);
             }
     
    diff --git a/test/hotspot/jtreg/gc/epsilon/TestInitAllocs.java b/test/hotspot/jtreg/gc/epsilon/TestInitAllocs.java
    new file mode 100644
    index 00000000000..353daaef8b6
    --- /dev/null
    +++ b/test/hotspot/jtreg/gc/epsilon/TestInitAllocs.java
    @@ -0,0 +1,76 @@
    +/*
    + * Copyright Amazon.com Inc. 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.
    + */
    +
    +package gc.epsilon;
    +
    +/**
    + * @test TestInitAllocs
    + * @requires vm.gc.Epsilon
    + * @summary Test that allocation path taken in early JVM phases works
    + *
    + * @run main/othervm -Xmx256m
    + *                   -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
    + *                   gc.epsilon.TestInitAllocs
    + *
    + * @run main/othervm -Xmx256m
    + *                   -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
    + *                   -XX:+UseTLAB
    + *                   -XX:+UseCompressedOops
    + *                   -XX:EpsilonMinHeapExpand=1024
    + *                   -XX:EpsilonUpdateCountersStep=1
    + *                   -XX:EpsilonPrintHeapSteps=1000000
    + *                   gc.epsilon.TestInitAllocs
    + *
    + * @run main/othervm -Xmx256m
    + *                   -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
    + *                   -XX:+UseTLAB
    + *                   -XX:-UseCompressedOops
    + *                   -XX:EpsilonMinHeapExpand=1024
    + *                   -XX:EpsilonUpdateCountersStep=1
    + *                   -XX:EpsilonPrintHeapSteps=1000000
    + *                   gc.epsilon.TestInitAllocs
    + *
    + * @run main/othervm -Xmx256m
    + *                   -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
    + *                   -XX:-UseTLAB
    + *                   -XX:+UseCompressedOops
    + *                   -XX:EpsilonMinHeapExpand=1024
    + *                   -XX:EpsilonUpdateCountersStep=1
    + *                   -XX:EpsilonPrintHeapSteps=1000000
    + *                   gc.epsilon.TestInitAllocs
    + *
    + * @run main/othervm -Xmx256m
    + *                   -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
    + *                   -XX:-UseTLAB
    + *                   -XX:-UseCompressedOops
    + *                   -XX:EpsilonMinHeapExpand=1024
    + *                   -XX:EpsilonUpdateCountersStep=1
    + *                   -XX:EpsilonPrintHeapSteps=1000000
    + *                   gc.epsilon.TestInitAllocs
    + */
    +
    +public class TestInitAllocs {
    +  public static void main(String[] args) throws Exception {
    +    System.out.println("Hello World");
    +  }
    +}
    diff --git a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/common/optionsvalidation/JVMOption.java b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/common/optionsvalidation/JVMOption.java
    index 78a508343d6..d87ca25fc3c 100644
    --- a/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/common/optionsvalidation/JVMOption.java
    +++ b/test/hotspot/jtreg/runtime/CommandLine/OptionsValidation/common/optionsvalidation/JVMOption.java
    @@ -116,7 +116,7 @@ public abstract class JVMOption {
                 default:
                     throw new Error("Expected only \"int\", \"intx\", \"size_t\", "
                             + "\"uint\", \"uintx\", \"uint64_t\", or \"double\" "
    -                        + "option types! Got " + type + " type!");
    +                        + "option types! Got " + type + " type for option " + name + "!");
             }
     
             return parameter;
    diff --git a/test/hotspot/jtreg/runtime/cds/TestDefaultArchiveLoading.java b/test/hotspot/jtreg/runtime/cds/TestDefaultArchiveLoading.java
    index acff3191300..8b07cd86a72 100644
    --- a/test/hotspot/jtreg/runtime/cds/TestDefaultArchiveLoading.java
    +++ b/test/hotspot/jtreg/runtime/cds/TestDefaultArchiveLoading.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
      * Copyright (c) 2024, Red Hat, Inc. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
    @@ -55,7 +55,7 @@
      * @requires vm.cds.default.archive.available
      * @requires vm.cds.write.archived.java.heap
      * @requires vm.bits == 64
    - * @requires !vm.gc.Z
    + * @requires vm.gc != "Z"
      * @library /test/lib
      * @modules java.base/jdk.internal.misc
      *          java.management
    @@ -69,7 +69,7 @@
      * @requires vm.cds.default.archive.available
      * @requires vm.cds.write.archived.java.heap
      * @requires vm.bits == 64
    - * @requires !vm.gc.Z
    + * @requires vm.gc != "Z"
      * @library /test/lib
      * @modules java.base/jdk.internal.misc
      *          java.management
    diff --git a/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/DynamicDumpWithAOTLinkedStaticArchive.java b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/DynamicDumpWithAOTLinkedStaticArchive.java
    new file mode 100644
    index 00000000000..8014ed685ee
    --- /dev/null
    +++ b/test/hotspot/jtreg/runtime/cds/appcds/aotClassLinking/DynamicDumpWithAOTLinkedStaticArchive.java
    @@ -0,0 +1,60 @@
    +/*
    + * Copyright (c) 2025, 2026, 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.
    + *
    + */
    +
    +
    +/*
    + * @test
    + * @bug 8374639
    + * @requires vm.cds.supports.aot.class.linking
    + * @library /test/lib
    + * @build DynamicDumpWithAOTLinkedStaticArchive jdk.test.whitebox.WhiteBox
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar app.jar TestApp
    + * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. DynamicDumpWithAOTLinkedStaticArchive
    + */
    +
    +import jdk.test.lib.cds.SimpleCDSAppTester;
    +import jdk.test.lib.process.OutputAnalyzer;
    +
    +public class DynamicDumpWithAOTLinkedStaticArchive {
    +    public static void main(String... args) throws Exception {
    +        SimpleCDSAppTester.of("DynamicDumpWithAOTLinkedStaticArchive")
    +            .classpath("app.jar")
    +            .appCommandLine("TestApp")
    +            .setGenerateBaseArchive(true)
    +            .setBaseArchiveOptions("-XX:+AOTClassLinking")
    +            .setProductionChecker((OutputAnalyzer out) -> {
    +                    out.shouldContain("HelloWorld");
    +                })
    +            .runDynamicWorkflow();
    +    }
    +}
    +
    +class TestApp {
    +    public static void main(String[] args) {
    +        System.out.println("HelloWorld");
    +        System[][][] x = new System[0][0][0];
    +        System.out.println(x);
    +    }
    +}
    diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/ArrayKlasses.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/ArrayKlasses.java
    deleted file mode 100644
    index e476382c8c9..00000000000
    --- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/ArrayKlasses.java
    +++ /dev/null
    @@ -1,170 +0,0 @@
    -/*
    - * Copyright (c) 2019, 2025, 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.
    - *
    - */
    -
    -/*
    - * @test
    - * @summary handling of the existence of InstanceKlass::array_klasses()
    - * @requires vm.cds
    - * @library /test/lib /test/hotspot/jtreg/runtime/cds/appcds
    - *          /test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes
    - * @build ArrayKlassesApp
    - * @build jdk.test.whitebox.WhiteBox
    - * @run driver jdk.test.lib.helpers.ClassFileInstaller -jar ArrayKlasses.jar ArrayKlassesApp
    - * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    - * @run main/othervm -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -Xbootclasspath/a:. ArrayKlasses
    - */
    -
    -import jdk.test.lib.helpers.ClassFileInstaller;
    -
    -public class ArrayKlasses extends DynamicArchiveTestBase {
    -    public static void main(String[] args) throws Exception {
    -        runTest(ArrayKlasses::test);
    -    }
    -
    -    static void test() throws Exception {
    -        String topArchiveName = getNewArchiveName();
    -        final String appJar = ClassFileInstaller.getJarPath("ArrayKlasses.jar");
    -        final String mainClass = "ArrayKlassesApp";
    -        final String runtimeLogOptions =
    -            "-Xlog:class+load,class+load+array=debug,cds+dynamic=debug,cds=debug,aot+unshareable=trace";
    -
    -        // Case 1
    -        // Create a dynamic archive with the ArrayKlassesApp app class and its
    -        // array classes.
    -        dump2(null, topArchiveName,
    -              "-Xlog:cds+dynamic=debug,cds+class=debug",
    -              "-cp", appJar, mainClass)
    -              .assertNormalExit(output -> {
    -                     output.shouldMatch("cds.class.*klasses.*array \\[LArrayKlassesApp;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[LArrayKlassesApp;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[\\[LArrayKlassesApp;");
    -                 });
    -
    -        // Case 1
    -        // At runtime , the ArrayKlasesApp and its array class should be loaded
    -        // from the dynamic archive.
    -        run2(null, topArchiveName, runtimeLogOptions,
    -             "-cp", appJar, mainClass)
    -             .assertNormalExit(output -> {
    -                     output.shouldContain("ArrayKlassesApp source: shared objects file (top)")
    -                           .shouldContain("restore: ArrayKlassesApp with class loader: jdk.internal.loader.ClassLoaders$AppClassLoader")
    -                           .shouldContain("[LArrayKlassesApp; source: shared objects file (top)")
    -                           .shouldContain("restore: [LArrayKlassesApp; with class loader: jdk.internal.loader.ClassLoaders$AppClassLoader")
    -                           .shouldContain("[[LArrayKlassesApp; source: shared objects file (top)")
    -                           .shouldContain("restore: [[LArrayKlassesApp; with class loader: jdk.internal.loader.ClassLoaders$AppClassLoader")
    -                           .shouldContain("[[[LArrayKlassesApp; source: shared objects file (top)")
    -                           .shouldContain("restore: [[[LArrayKlassesApp; with class loader: jdk.internal.loader.ClassLoaders$AppClassLoader")
    -                           .shouldHaveExitValue(0);
    -                 });
    -
    -        // Case 2
    -        // Create a dynamic archive with the array classes of java/util/Date which
    -        // is in the default CDS archive.
    -        topArchiveName = getNewArchiveName();
    -        dump2(null, topArchiveName,
    -              "-Xlog:class+load,cds+dynamic=debug,cds+class=debug",
    -              "-cp", appJar, mainClass, "system")
    -              .assertNormalExit(output -> {
    -                     output.shouldContain("java.util.Date source: shared objects file")
    -                           .shouldMatch("cds.class.*klasses.*array \\[Ljava.util.Date;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[Ljava.util.Date;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[\\[Ljava.util.Date;");
    -                 });
    -
    -        // Case 2
    -        // At runtime, the java/util/Date class should be loaded from the default
    -        // CDS archive; its array class should be loaded from the dynamic archive.
    -        run2(null, topArchiveName, runtimeLogOptions,
    -             "-cp", appJar, mainClass, "system")
    -             .assertNormalExit(output -> {
    -                     output.shouldContain("java.util.Date source: shared objects file")
    -                           .shouldContain("restore: java.util.Date with class loader: boot")
    -                           .shouldContain("[Ljava.util.Date; source: shared objects file (top)")
    -                           .shouldContain("restore: [Ljava.util.Date; with class loader: boot")
    -                           .shouldContain("[[Ljava.util.Date; source: shared objects file (top)")
    -                           .shouldContain("restore: [[Ljava.util.Date; with class loader: boot")
    -                           .shouldContain("[[[Ljava.util.Date; source: shared objects file (top)")
    -                           .shouldContain("restore: [[[Ljava.util.Date; with class loader: boot")
    -                           .shouldHaveExitValue(0);
    -                 });
    -
    -        // Case 3
    -        // Create a dynamic archive with primitive arrays [[J and [[[J with [J
    -        // already in the default CDS archive
    -        topArchiveName = getNewArchiveName();
    -        dump2(null, topArchiveName,
    -              "-Xlog:class+load,cds+dynamic=debug,cds+class=debug",
    -              "-cp", appJar, mainClass, "primitive")
    -              .assertNormalExit(output -> {
    -                     output.shouldMatch("cds.class.*klasses.*array \\[\\[J")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[\\[J");
    -                 });
    -
    -        // Case 3
    -        // At runtime, the [J should be loaded from the default CDS archive;
    -        // the higher-dimension array should be loaded from the dynamic archive.
    -        run2(null, topArchiveName, runtimeLogOptions,
    -             "-cp", appJar, mainClass, "primitive")
    -             .assertNormalExit(output -> {
    -                     output.shouldContain("[J source: shared objects file")
    -                           .shouldContain("restore: [J with class loader: boot")
    -                           .shouldContain("[[J source: shared objects file (top)")
    -                           .shouldContain("restore: [[J with class loader: boot")
    -                           .shouldContain("[[[J source: shared objects file (top)")
    -                           .shouldContain("restore: [[[J with class loader: boot")
    -                           .shouldHaveExitValue(0);
    -                 });
    -
    -        // Case 4
    -        // Create a dynamic archive with 2-, 3- and 4-dimension arrays of java/lang/Integer.
    -        // The java/lang/Integer class and the 1-dimension array is in the default archive.
    -        topArchiveName = getNewArchiveName();
    -        dump2(null, topArchiveName,
    -              "-Xlog:class+load,cds+dynamic=debug,cds+class=debug",
    -              "-cp", appJar, mainClass, "integer-array")
    -              .assertNormalExit(output -> {
    -                     output.shouldMatch("cds.class.*klasses.*array \\[\\[Ljava.lang.Integer;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[\\[Ljava.lang.Integer;")
    -                           .shouldMatch("cds.class.*klasses.*array \\[\\[\\[\\[Ljava.lang.Integer;");
    -                 });
    -
    -        // Case 4
    -        // At runtime, the 4-dimension array of java/lang/Integer should be
    -        // loaded from the dynamic archive.
    -        run2(null, topArchiveName, runtimeLogOptions,
    -             "-cp", appJar, mainClass, "integer-array")
    -             .assertNormalExit(output -> {
    -                     output.shouldContain("java.lang.Integer source: shared objects file")
    -                           .shouldContain("restore: java.lang.Integer with class loader: boot")
    -                           .shouldContain("restore: [Ljava.lang.Integer; with class loader: boot")
    -                           .shouldContain("[[Ljava.lang.Integer; source: shared objects file (top)")
    -                           .shouldContain("restore: [[Ljava.lang.Integer; with class loader: boot")
    -                           .shouldContain("[[[Ljava.lang.Integer; source: shared objects file (top)")
    -                           .shouldContain("restore: [[[Ljava.lang.Integer; with class loader: boot")
    -                           .shouldContain("[[[[Ljava.lang.Integer; source: shared objects file (top)")
    -                           .shouldContain("restore: [[[[Ljava.lang.Integer; with class loader: boot")
    -                           .shouldHaveExitValue(0);
    -                 });
    -    }
    -}
    diff --git a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes/ArrayKlassesApp.java b/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes/ArrayKlassesApp.java
    deleted file mode 100644
    index 3c78e3e8dbe..00000000000
    --- a/test/hotspot/jtreg/runtime/cds/appcds/dynamicArchive/test-classes/ArrayKlassesApp.java
    +++ /dev/null
    @@ -1,77 +0,0 @@
    -/*
    - * Copyright (c) 2019, 2023, 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 java.lang.reflect.Array;
    -import java.util.Date;
    -
    -public class ArrayKlassesApp {
    -    public static void main(String args[]) {
    -        if (args.length == 1) {
    -            if (args[0].equals("system")) {
    -                Date[][][] array = new Date[1][2][2];
    -                int count = 0;
    -                for (int i=0; i<1; i++) {
    -                    for (int j=0; j<2; j++) {
    -                        for (int k=0; k<2; k++) {
    -                            array[i][j][k] = new Date();
    -                            count++;
    -                            array[i][j][k].setTime(20000 * count);
    -                        }
    -                    }
    -                }
    -            } else if (args[0].equals("primitive")) {
    -                long[][][] larray = new long[1][2][2];
    -                long lcount = 0;
    -                for (int i=0; i<1; i++) {
    -                    for (int j=0; j<2; j++) {
    -                        for (int k=0; k<2; k++) {
    -                            lcount++;
    -                            larray[i][j][k] = lcount;
    -                        }
    -                    }
    -                }
    -            } else if (args[0].equals("integer-array")) {
    -                Integer[][][][] iarray = new Integer[4][4][4][4];
    -                int count = 0;
    -                for (int i = 0; i < 4; i++) {
    -                    for (int j = 0; j < 4; j++) {
    -                        for (int k = 0; k < 4; k++) {
    -                            for (int l = 0; l < 4; l++) {
    -                                count++;
    -                                iarray[i][j][k][l] = new Integer(count);
    -                            }
    -                        }
    -                    }
    -                }
    -                System.out.println(iarray);
    -                System.out.println(iarray.getClass());
    -            }
    -        } else {
    -            Object x = Array.newInstance(ArrayKlassesApp.class, 3,3,3);
    -            System.out.println(x);
    -            System.out.println(x.getClass());
    -            System.out.println(Array.getLength(x));
    -        }
    -    }
    -}
    diff --git a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java
    index 6c465b357d7..10888dce1b4 100644
    --- a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java
    +++ b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/NMethodRelocationTest.java
    @@ -23,23 +23,30 @@
     
     /*
      * @test
    - *
      * @bug 8316694
      * @summary Verify that nmethod relocation posts the correct JVMTI events
    - * @requires vm.jvmti
    - * @requires vm.gc == "null" | vm.gc == "Serial"
    + * @requires vm.jvmti &
    + *           vm.gc != "Epsilon" &
    + *           vm.flavor == "server" &
    + *           !vm.emulatedClient &
    + *           (vm.opt.TieredStopAtLevel == null | vm.opt.TieredStopAtLevel == 4)
      * @library /test/lib /test/hotspot/jtreg
      * @build jdk.test.whitebox.WhiteBox
      * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    - * @run main/othervm/native NMethodRelocationTest
    + * @run main/othervm/native -agentlib:NMethodRelocationTest
    + *                          --enable-native-access=ALL-UNNAMED
    + *                          -Xbootclasspath/a:.
    + *                          -Xbatch
    + *                          -XX:+UnlockDiagnosticVMOptions
    + *                          -XX:+WhiteBoxAPI
    + *                          -XX:+SegmentedCodeCache
    + *                          -XX:-TieredCompilation
    + *                          -XX:+UnlockExperimentalVMOptions
    + *                          -XX:+NMethodRelocation
    + *                          NMethodRelocationTest
      */
     
    -import static compiler.whitebox.CompilerWhiteBoxTest.COMP_LEVEL_FULL_OPTIMIZATION;
    -
     import java.lang.reflect.Executable;
    -import java.util.Objects;
    -import java.util.regex.Matcher;
    -import java.util.regex.Pattern;
     
     import jdk.test.lib.Asserts;
     import jdk.test.lib.process.OutputAnalyzer;
    @@ -48,52 +55,9 @@ import jdk.test.whitebox.WhiteBox;
     import jdk.test.whitebox.code.BlobType;
     import jdk.test.whitebox.code.NMethod;
     
    +import static compiler.whitebox.CompilerWhiteBoxTest.COMP_LEVEL_FULL_OPTIMIZATION;
     
     public class NMethodRelocationTest {
    -    public static void main(String[] args) throws Exception {
    -        ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(
    -                "-agentlib:NMethodRelocationTest",
    -                "--enable-native-access=ALL-UNNAMED",
    -                "-Xbootclasspath/a:.",
    -                "-XX:+UseSerialGC",
    -                "-XX:+UnlockDiagnosticVMOptions",
    -                "-XX:+WhiteBoxAPI",
    -                "-XX:+SegmentedCodeCache",
    -                "-XX:-TieredCompilation",
    -                "-XX:+UnlockExperimentalVMOptions",
    -                "-XX:+NMethodRelocation",
    -                "DoWork");
    -
    -        OutputAnalyzer oa = new OutputAnalyzer(pb.start());
    -        String output = oa.getOutput();
    -        if (oa.getExitValue() != 0) {
    -            System.err.println(oa.getOutput());
    -            throw new RuntimeException("Non-zero exit code returned from the test");
    -        }
    -        Asserts.assertTrue(oa.getExitValue() == 0);
    -
    -        Pattern pattern = Pattern.compile("(?m)^Relocated nmethod from (0x[0-9a-f]{16}) to (0x[0-9a-f]{16})$");
    -        Matcher matcher = pattern.matcher(output);
    -
    -        if (matcher.find()) {
    -            String fromAddr = matcher.group(1);
    -            String toAddr = matcher.group(2);
    -
    -            // Confirm events sent for both original and relocated nmethod
    -            oa.shouldContain(":   name: compiledMethod, code: " + fromAddr);
    -            oa.shouldContain(":   name: compiledMethod, code: " + toAddr);
    -            oa.shouldContain(":   name: compiledMethod, code: " + fromAddr);
    -            oa.shouldContain(":   name: compiledMethod, code: " + toAddr);
    -        } else {
    -            System.err.println(oa.getOutput());
    -            throw new RuntimeException("Unable to find relocation information");
    -        }
    -    }
    -}
    -
    -class DoWork {
    -
    -    protected static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox();
     
         /** Load native library if required. */
         static {
    @@ -107,43 +71,18 @@ class DoWork {
             }
         }
     
    -    /**
    -     * Returns value of VM option.
    -     *
    -     * @param name option's name
    -     * @return value of option or {@code null}, if option doesn't exist
    -     * @throws NullPointerException if name is null
    -     */
    -    protected static String getVMOption(String name) {
    -        Objects.requireNonNull(name);
    -        return Objects.toString(WHITE_BOX.getVMFlag(name), null);
    -    }
    +    protected static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox();
     
    -    /**
    -     * Returns value of VM option or default value.
    -     *
    -     * @param name         option's name
    -     * @param defaultValue default value
    -     * @return value of option or {@code defaultValue}, if option doesn't exist
    -     * @throws NullPointerException if name is null
    -     * @see #getVMOption(String)
    -     */
    -    protected static String getVMOption(String name, String defaultValue) {
    -        String result = getVMOption(name);
    -        return result == null ? defaultValue : result;
    -    }
    +    native static boolean shouldExit();
     
    -    public static void main(String argv[]) throws Exception {
    -        run();
    -    }
    -
    -    public static void run() throws Exception {
    -        Executable method = DoWork.class.getDeclaredMethod("compiledMethod");
    +    public static void main(String[] argv) throws Exception {
    +        Executable method = NMethodRelocationTest.class.getDeclaredMethod("compiledMethod");
             WHITE_BOX.testSetDontInlineMethod(method, true);
     
             WHITE_BOX.enqueueMethodForCompilation(method, COMP_LEVEL_FULL_OPTIMIZATION);
    -        while (WHITE_BOX.isMethodQueuedForCompilation(method)) {
    -            Thread.onSpinWait();
    +
    +        if (!WHITE_BOX.isMethodCompiled(method)) {
    +            throw new AssertionError("Method not compiled");
             }
     
             NMethod originalNMethod = NMethod.get(method, false);
    @@ -164,13 +103,9 @@ class DoWork {
     
             WHITE_BOX.deoptimizeAll();
     
    -        WHITE_BOX.fullGC();
    -        WHITE_BOX.fullGC();
    -
    -        WHITE_BOX.lockCompilation();
    -
    -        System.out.printf("Relocated nmethod from 0x%016x to 0x%016x%n", originalNMethod.code_begin, relocatedNMethod.code_begin);
    -        System.out.flush();
    +        while (!shouldExit()) {
    +            WHITE_BOX.fullGC();
    +        }
         }
     
         public static long compiledMethod() {
    diff --git a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp
    index 41ba6b10608..7f8ab7b78bc 100644
    --- a/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp
    +++ b/test/hotspot/jtreg/serviceability/jvmti/NMethodRelocation/libNMethodRelocationTest.cpp
    @@ -21,10 +21,20 @@
      * questions.
      */
     
    +#include 
     #include 
    -#include 
    -#include 
    -#include 
    +#include "jvmti_common.hpp"
    +
    +extern "C" {
    +
    +// Track nmethod addresses for LOAD and UNLOAD events
    +static const void* first_load_addr      = nullptr;
    +static const void* second_load_addr     = nullptr;
    +static const void* first_unload_addr    = nullptr;
    +static const void* second_unload_addr   = nullptr;
    +
    +// Keep track of test completion
    +static std::atomic should_exit{false};
     
     /**
      * Callback for COMPILED_METHOD_LOAD event.
    @@ -34,18 +44,27 @@ callbackCompiledMethodLoad(jvmtiEnv* jvmti, jmethodID method,
                                 jint code_size, const void* code_addr,
                                 jint map_length, const jvmtiAddrLocationMap* map,
                                 const void* compile_info) {
    -    char* name = nullptr;
    -    char* sig = nullptr;
     
    -    if (jvmti->GetMethodName(method, &name, &sig, nullptr) != JVMTI_ERROR_NONE) {
    -        printf("    [Could not retrieve method name]\n");
    -        fflush(stdout);
    +    // Only track events for "compiledMethod"
    +    char* name = get_method_name(jvmti, method);
    +    if (strcmp(name, "compiledMethod") != 0) {
             return;
         }
     
    -    printf(":   name: %s, code: 0x%016" PRIxPTR "\n",
    -        name, (uintptr_t)code_addr);
    -    fflush(stdout);
    +    LOG(":   name: %s, code: 0x%016" PRIxPTR "\n", name, (uintptr_t)code_addr);
    +
    +    if (first_load_addr == nullptr) {
    +        first_load_addr = code_addr;
    +    } else if (second_load_addr == nullptr) {
    +        second_load_addr = code_addr;
    +
    +        // Verify that the addresses are different
    +        if (first_load_addr == second_load_addr) {
    +            fatal("Load events for 'compiledMethod' are expected to use different addresses");
    +        }
    +    } else {
    +        fatal("Received too many load events for 'compiledMethod'");
    +    }
     }
     
     /**
    @@ -54,25 +73,50 @@ callbackCompiledMethodLoad(jvmtiEnv* jvmti, jmethodID method,
     JNIEXPORT void JNICALL
     callbackCompiledMethodUnload(jvmtiEnv* jvmti, jmethodID method,
                                  const void* code_addr) {
    -    char* name = nullptr;
    -    char* sig = nullptr;
     
    -    if (jvmti->GetMethodName(method, &name, &sig, nullptr) != JVMTI_ERROR_NONE) {
    -        printf("    [Could not retrieve method name]\n");
    -        fflush(stdout);
    +    // Only track events for "compiledMethod"
    +    char* name = get_method_name(jvmti, method);
    +    if (strcmp(name, "compiledMethod") != 0) {
             return;
         }
    -    printf(":   name: %s, code: 0x%016" PRIxPTR "\n",
    -        name, (uintptr_t)code_addr);
    -    fflush(stdout);
    +
    +    LOG(":   name: %s, code: 0x%016" PRIxPTR "\n", name, (uintptr_t)code_addr);
    +
    +    // Validate both loads have occurred
    +    if (first_load_addr == nullptr || second_load_addr == nullptr) {
    +        fatal("UNLOAD event for 'compiledMethod' occurred before both LOAD events");
    +    }
    +
    +    if (first_unload_addr == nullptr) {
    +        first_unload_addr = code_addr;
    +    } else if (second_unload_addr == nullptr) {
    +        second_unload_addr = code_addr;
    +
    +        // Verify that the addresses are different
    +        if (first_unload_addr == second_unload_addr) {
    +            fatal("Unload events for 'compiledMethod' are expected to use different addresses");
    +        }
    +
    +        // LOAD and UNLOAD events should report the same two addresses, but the order of
    +        // the UNLOADs is not guaranteed, since the GC may unload either nmethod first.
    +        if ((first_load_addr == first_unload_addr  && second_load_addr == second_unload_addr) ||
    +            (first_load_addr == second_unload_addr && second_load_addr == first_unload_addr)) {
    +
    +            // Update should_exit to signal test completion
    +            should_exit.store(true);
    +        } else {
    +            fatal("Address mismatch for 'compiledMethod' events");
    +        }
    +    } else {
    +        fatal("Received too many unload events for 'compiledMethod'");
    +    }
     }
     
     JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {
         jvmtiEnv* jvmti = nullptr;
    -    jvmtiError error;
     
         if (jvm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_0) != JNI_OK) {
    -        printf("Unable to access JVMTI!\n");
    +        LOG("Unable to access JVMTI!\n");
             return JNI_ERR;
         }
     
    @@ -80,11 +124,8 @@ JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
         jvmtiCapabilities caps;
         memset(&caps, 0, sizeof(caps));
         caps.can_generate_compiled_method_load_events = 1;
    -    error = jvmti->AddCapabilities(&caps);
    -    if (error != JVMTI_ERROR_NONE) {
    -        printf("ERROR: Unable to add capabilities, error=%d\n", error);
    -        return JNI_ERR;
    -    }
    +    jvmtiError error = jvmti->AddCapabilities(&caps);
    +    check_jvmti_error(error, "Unable to add capabilities");
     
         // Set event callbacks
         jvmtiEventCallbacks eventCallbacks;
    @@ -92,23 +133,21 @@ JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
         eventCallbacks.CompiledMethodLoad = callbackCompiledMethodLoad;
         eventCallbacks.CompiledMethodUnload = callbackCompiledMethodUnload;
         error = jvmti->SetEventCallbacks(&eventCallbacks, sizeof(eventCallbacks));
    -    if (error != JVMTI_ERROR_NONE) {
    -        printf("ERROR: Unable to set event callbacks, error=%d\n", error);
    -        return JNI_ERR;
    -    }
    +    check_jvmti_error(error, "Unable to set event callbacks");
     
         // Enable events
         error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_COMPILED_METHOD_LOAD, nullptr);
    -    if (error != JVMTI_ERROR_NONE) {
    -        printf("ERROR: Unable to enable COMPILED_METHOD_LOAD event, error=%d\n", error);
    -        return JNI_ERR;
    -    }
    +    check_jvmti_error(error, "Unable to enable COMPILED_METHOD_LOAD event");
     
         error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_COMPILED_METHOD_UNLOAD, nullptr);
    -    if (error != JVMTI_ERROR_NONE) {
    -        printf("ERROR: Unable to enable COMPILED_METHOD_UNLOAD event, error=%d\n", error);
    -        return JNI_ERR;
    -    }
    +    check_jvmti_error(error, "Unable to enable COMPILED_METHOD_UNLOAD event");
     
         return JNI_OK;
     }
    +
    +JNIEXPORT jboolean JNICALL
    +Java_NMethodRelocationTest_shouldExit(JNIEnv *env, jclass cls) {
    +  return should_exit.load();
    +}
    +
    +}
    diff --git a/test/hotspot/jtreg/serviceability/jvmti/vthread/ThreadListStackTracesTest/ThreadListStackTracesTest.java b/test/hotspot/jtreg/serviceability/jvmti/vthread/ThreadListStackTracesTest/ThreadListStackTracesTest.java
    index 079f65620d8..2a5c9bea111 100644
    --- a/test/hotspot/jtreg/serviceability/jvmti/vthread/ThreadListStackTracesTest/ThreadListStackTracesTest.java
    +++ b/test/hotspot/jtreg/serviceability/jvmti/vthread/ThreadListStackTracesTest/ThreadListStackTracesTest.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2023, 2026, 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
    @@ -45,8 +45,9 @@ abstract class TestTask implements Runnable {
         }
     
         public void ensureReadyAndWaiting(Thread vt, Thread.State expState, ReentrantLock rlock) {
    +        sleep(50); // reliability: wait for a potential class loading to complete
             // wait while the thread is not ready or thread state is unexpected
    -        while (!threadReady || (vt.getState() != expState) || !rlock.hasQueuedThreads()) {
    +        while (!threadReady || (vt.getState() != expState) || !rlock.hasQueuedThread(vt)) {
                 sleep(1);
             }
         }
    @@ -125,11 +126,12 @@ public class ThreadListStackTracesTest {
             int jvmtiExpState = (expState == Thread.State.WAITING) ?
                                 JVMTI_THREAD_STATE_WAITING :
                                 JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER;
    +        Thread.State state = vt.getState();
     
    -        System.out.printf("State: expected: %s single: %x multi: %x\n",
    -                          vt.getState(), singleState, multiState);
    +        System.out.printf("State: expected: %s, vt.getState(): %s, jvmtiExpState: %x single: %x multi: %x\n",
    +                          expState, state, jvmtiExpState, singleState, multiState);
     
    -        if (vt.getState() != expState) {
    +        if (state != expState) {
                 failed("Java thread state is wrong");
             }
             if ((singleState & jvmtiExpState) == 0) {
    diff --git a/test/hotspot/jtreg/vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java b/test/hotspot/jtreg/vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java
    index c97353006d4..d88e34ae5e0 100644
    --- a/test/hotspot/jtreg/vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java
    +++ b/test/hotspot/jtreg/vmTestbase/gc/gctests/MemoryEaterMT/MemoryEaterMT.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2007, 2020, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2007, 2026, 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
    @@ -28,6 +28,7 @@
      * @summary converted from VM Testbase gc/gctests/MemoryEaterMT.
      * VM Testbase keywords: [gc, stress, stressopt, nonconcurrent]
      *
    + * @requires test.thread.factory != "Virtual"
      * @library /vmTestbase
      *          /test/lib
      * @run main/othervm -XX:-UseGCOverheadLimit gc.gctests.MemoryEaterMT.MemoryEaterMT
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters001/CollectionCounters001.java b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters001/CollectionCounters001.java
    index 7c089abb6ec..e2f1b9cbc71 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters001/CollectionCounters001.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters001/CollectionCounters001.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2007, 2026, 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
    @@ -24,15 +24,19 @@
     
     /*
      * @test
    - * @key randomness
      *
      * @summary converted from VM Testbase nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters001.
      * VM Testbase keywords: [monitoring]
      *
    - * @requires vm.opt.DisableExplicitGC != "true"
      * @library /vmTestbase
      *          /test/lib
    - * @run main/othervm -XX:-UseGCOverheadLimit
    + *
    + * @build jdk.test.whitebox.WhiteBox
    + * @requires vm.opt.DisableExplicitGC != "true"
    + * @requires vm.compMode != "Xcomp"
    + *
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-UseGCOverheadLimit
      *      nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCounters001.CollectionCounters001
      *      -testMode=directly
      *      -iterations=5
    @@ -42,10 +46,12 @@ package nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCount
     
     import java.util.List;
     import java.lang.management.*;
    +
    +import jdk.test.whitebox.WhiteBox;
    +
     import nsk.share.TestFailure;
     import nsk.share.test.*;
     import nsk.monitoring.share.*;
    -import nsk.share.gc.Algorithms;
     import nsk.share.runner.RunParams;
     import nsk.share.runner.RunParamsAware;
     
    @@ -80,12 +86,15 @@ public class CollectionCounters001 extends MonitoringTestBase implements RunPara
             private void runOne(ExecutionController stresser) {
                     updateCounters();
                     validate(false /* don't check gc count increases */);
    -                Algorithms.eatMemory(stresser);
    +
    +                WhiteBox.getWhiteBox().fullGC();
                     updateCounters();
                     validate(true);
    +
                     System.gc();
                     updateCounters();
                     validate(true);
    +
                     memory.gc();
                     updateCounters();
                     validate(true);
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters002/TestDescription.java b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters002/TestDescription.java
    index 51abb4e7a0b..2c1e21fcb15 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters002/TestDescription.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters002/TestDescription.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2017, 2026, 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
    @@ -24,15 +24,19 @@
     
     /*
      * @test
    - * @key randomness
      *
      * @summary converted from VM Testbase nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters002.
      * VM Testbase keywords: [monitoring]
      *
    - * @requires vm.opt.DisableExplicitGC != "true"
      * @library /vmTestbase
      *          /test/lib
    - * @run main/othervm -XX:-UseGCOverheadLimit
    + *
    + * @build jdk.test.whitebox.WhiteBox
    + * @requires vm.opt.DisableExplicitGC != "true"
    + * @requires vm.compMode != "Xcomp"
    + *
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-UseGCOverheadLimit
      *      nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCounters001.CollectionCounters001
      *      -testMode=server
      *      -MBeanServer=default
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters003/TestDescription.java b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters003/TestDescription.java
    index 20e266f50f1..241b1912d0f 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters003/TestDescription.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters003/TestDescription.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2017, 2026, 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
    @@ -24,15 +24,19 @@
     
     /*
      * @test
    - * @key randomness
      *
      * @summary converted from VM Testbase nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters003.
      * VM Testbase keywords: [monitoring]
      *
    - * @requires vm.opt.DisableExplicitGC != "true"
      * @library /vmTestbase
      *          /test/lib
    - * @run main/othervm -XX:-UseGCOverheadLimit
    + *
    + * @build jdk.test.whitebox.WhiteBox
    + * @requires vm.opt.DisableExplicitGC != "true"
    + * @requires vm.compMode != "Xcomp"
    + *
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-UseGCOverheadLimit
      *      nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCounters001.CollectionCounters001
      *      -testMode=server
      *      -MBeanServer=custom
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters004/TestDescription.java b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters004/TestDescription.java
    index db5a04cede8..cd0ed660ed0 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters004/TestDescription.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters004/TestDescription.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2017, 2026, 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
    @@ -24,15 +24,19 @@
     
     /*
      * @test
    - * @key randomness
      *
      * @summary converted from VM Testbase nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters004.
      * VM Testbase keywords: [monitoring]
      *
    - * @requires vm.opt.DisableExplicitGC != "true"
      * @library /vmTestbase
      *          /test/lib
    - * @run main/othervm -XX:-UseGCOverheadLimit
    + *
    + * @build jdk.test.whitebox.WhiteBox
    + * @requires vm.opt.DisableExplicitGC != "true"
    + * @requires vm.compMode != "Xcomp"
    + *
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-UseGCOverheadLimit
      *      nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCounters001.CollectionCounters001
      *      -testMode=proxy
      *      -MBeanServer=default
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters005/TestDescription.java b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters005/TestDescription.java
    index 68c50f026b7..c3c6fac62e7 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters005/TestDescription.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters005/TestDescription.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2017, 2026, 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
    @@ -24,15 +24,19 @@
     
     /*
      * @test
    - * @key randomness
      *
      * @summary converted from VM Testbase nsk/monitoring/GarbageCollectorMXBean/CollectionCounters/CollectionCounters005.
      * VM Testbase keywords: [monitoring]
      *
    - * @requires vm.opt.DisableExplicitGC != "true"
      * @library /vmTestbase
      *          /test/lib
    - * @run main/othervm -XX:-UseGCOverheadLimit
    + *
    + * @build jdk.test.whitebox.WhiteBox
    + * @requires vm.opt.DisableExplicitGC != "true"
    + * @requires vm.compMode != "Xcomp"
    + *
    + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
    + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI -XX:-UseGCOverheadLimit
      *      nsk.monitoring.GarbageCollectorMXBean.CollectionCounters.CollectionCounters001.CollectionCounters001
      *      -testMode=proxy
      *      -MBeanServer=custom
    diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/gc/Algorithms.java b/test/hotspot/jtreg/vmTestbase/nsk/share/gc/Algorithms.java
    index 404dc6498f7..518a932e16b 100644
    --- a/test/hotspot/jtreg/vmTestbase/nsk/share/gc/Algorithms.java
    +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/gc/Algorithms.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2003, 2026, 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
    @@ -73,10 +73,7 @@ public class Algorithms {
              * 

    * * Note that this method can throw Failure if any exception - * is thrown while eating memory. To avoid OOM while allocating - * exception we preallocate it before the lunch starts. It means - * that exception stack trace does not correspond to the place - * where exception is thrown, but points at start of the method. + * is thrown while eating memory. * * This method uses nsk.share.test.Stresser class to control * it's execution. Consumed number of iterations depends on diff --git a/test/hotspot/jtreg/vmTestbase/nsk/share/gc/gp/GarbageUtils.java b/test/hotspot/jtreg/vmTestbase/nsk/share/gc/gp/GarbageUtils.java index 1c7a8c6172c..529b299fd15 100644 --- a/test/hotspot/jtreg/vmTestbase/nsk/share/gc/gp/GarbageUtils.java +++ b/test/hotspot/jtreg/vmTestbase/nsk/share/gc/gp/GarbageUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2026, 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 @@ -181,10 +181,7 @@ public final class GarbageUtils { * Eat memory using default(byte[]) garbage producer. * * Note that this method can throw Failure if any exception - * is thrown while eating memory. To avoid OOM while allocating - * exception we preallocate it before the lunch starts. It means - * that exception stack trace does not correspond to the place - * where exception is thrown, but points at start of the method. + * is thrown while eating memory. * * @param stresser stresser * @param initialFactor determines which portion of initial memory initial chunk will be @@ -200,10 +197,7 @@ public final class GarbageUtils { * Eat memory using given garbage producer. * * Note that this method can throw Failure if any exception - * is thrown while eating memory. To avoid OOM while allocating - * exception we preallocate it before the lunch starts. It means - * that exception stack trace does not correspond to the place - * where exception is thrown, but points at start of the method. + * is thrown while eating memory. * * @param stresser stresser to use * @param gp garbage producer @@ -270,10 +264,7 @@ public final class GarbageUtils { * Eat memory using given garbage producer. * * Note that this method can throw Failure if any exception - * is thrown while eating memory. To avoid OOM while allocating - * exception we preallocate it before the lunch starts. It means - * that exception stack trace does not correspond to the place - * where exception is thrown, but points at start of the method. + * is thrown while eating memory. * * @param stresser stresser to use * @param gp garbage producer diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index addcc8830af..9cfc23ea8da 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -1,6 +1,6 @@ ########################################################################### # -# Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2009, 2026, 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 @@ -561,8 +561,6 @@ java/net/MulticastSocket/Test.java 7145658,8308807 # jdk_nio -java/nio/channels/AsyncCloseAndInterrupt.java 8368290 macosx-26.0.1 - java/nio/channels/DatagramChannel/AdaptorMulticasting.java 8308807,8144003 aix-ppc64,macosx-all java/nio/channels/DatagramChannel/AfterDisconnect.java 8308807 aix-ppc64 java/nio/channels/DatagramChannel/ManySourcesAndTargets.java 8264385 macosx-aarch64 diff --git a/test/jdk/java/io/File/GetXSpace.java b/test/jdk/java/io/File/GetXSpace.java index 96ce4ede1b2..f1529c18fb2 100644 --- a/test/jdk/java/io/File/GetXSpace.java +++ b/test/jdk/java/io/File/GetXSpace.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, 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 @@ -44,7 +44,6 @@ import java.util.ArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import jdk.test.lib.Platform; -import jdk.test.lib.Platform; import static java.lang.System.err; import static java.lang.System.out; @@ -103,16 +102,11 @@ public class GetXSpace { private final long free; private final long available; - Space(String name) { + Space(String name) throws IOException { this.name = name; long[] sizes = new long[4]; if (Platform.isWindows() && isCDDrive(name)) { - try { - getCDDriveSpace(name, sizes); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("can't get CDDrive sizes"); - } + getCDDriveSpace(name, sizes); } else { if (getSpace(name, sizes)) System.err.println("WARNING: total space is estimated"); @@ -170,7 +164,7 @@ public class GetXSpace { return al; } - private static void compare(Space s) { + private static void compare(Space s) throws IOException { File f = new File(s.name()); long ts = f.getTotalSpace(); long fs = f.getFreeSpace(); @@ -318,7 +312,7 @@ public class GetXSpace { } } - private static int testFile(Path dir) { + private static int testFile(Path dir) throws IOException { String dirName = dir.toString(); out.format("--- Testing %s%n", dirName); compare(new Space(dir.getRoot().toString())); @@ -333,10 +327,11 @@ public class GetXSpace { return fail != 0 ? 1 : 0; } - private static int testVolumes() { + private static int testVolumes() throws IOException { out.println("--- Testing volumes"); // Find all of the partitions on the machine and verify that the sizes - // returned by File::getXSpace are equivalent to those from getSpace or getCDDriveSpace + // returned by File::getXSpace are equivalent to those from getSpace + // or getCDDriveSpace ArrayList l; try { l = paths(); @@ -350,7 +345,18 @@ public class GetXSpace { throw new RuntimeException("no partitions?"); for (var p : l) { - Space s = new Space(p); + Space s; + try { + s = new Space(p); + } catch (IOException x) { + // Avoid failing for transient file systems on Windows + if (Platform.isWindows()) { + File f = new File(p); + if (!f.exists()) + continue; + } + throw new IOException("Failure for volume " + p, x); + } compare(s); compareZeroNonExist(); compareZeroExist(); @@ -408,19 +414,19 @@ public class GetXSpace { // size[2] free space: number of free bytes in the volume // size[3] usable space: number of bytes available to the caller // - private static native boolean getSpace0(String root, long[] space); + private static native boolean getSpace0(String root, long[] space) + throws IOException; private static native boolean isCDDrive(String root); - private static boolean getSpace(String root, long[] space) { + private static boolean getSpace(String root, long[] space) + throws IOException { try { return getSpace0(root, space); - } catch (RuntimeException e) { + } catch (IOException e) { File f = new File(root); - boolean exists = f.exists(); - boolean readable = f.canRead(); System.err.printf("getSpace0 failed for %s (%s, %s)%n", - root, exists, readable); + root, f.exists(), f.canRead()); throw e; } } diff --git a/test/jdk/java/io/File/libGetXSpace.c b/test/jdk/java/io/File/libGetXSpace.c index 59805e0adcb..9297721b8f4 100644 --- a/test/jdk/java/io/File/libGetXSpace.c +++ b/test/jdk/java/io/File/libGetXSpace.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, 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 @@ -85,7 +85,7 @@ Java_GetXSpace_getSpace0 BOOL hres = pfnGetDiskSpaceInformation(path, &diskSpaceInfo); (*env)->ReleaseStringChars(env, root, strchars); if (FAILED(hres)) { - JNU_ThrowByNameWithLastError(env, "java/lang/RuntimeException", + JNU_ThrowByNameWithLastError(env, "java/io/IOException", "GetDiskSpaceInformationW"); return totalSpaceIsEstimated; } @@ -113,7 +113,7 @@ Java_GetXSpace_getSpace0 &totalNumberOfBytes, &totalNumberOfFreeBytes); (*env)->ReleaseStringChars(env, root, strchars); if (FAILED(hres)) { - JNU_ThrowByNameWithLastError(env, "java/lang/RuntimeException", + JNU_ThrowByNameWithLastError(env, "java/io/IOException", "GetDiskFreeSpaceExW"); return totalSpaceIsEstimated; } @@ -131,8 +131,7 @@ Java_GetXSpace_getSpace0 char* chars = (char*)malloc((len + 1)*sizeof(char)); if (chars == NULL) { (*env)->ReleaseStringChars(env, root, strchars); - JNU_ThrowByNameWithLastError(env, "java/lang/RuntimeException", - "malloc"); + JNU_ThrowOutOfMemoryError(env, "malloc"); return JNI_FALSE; } @@ -146,7 +145,7 @@ Java_GetXSpace_getSpace0 int result = statfs(chars, &buf); free(chars); if (result < 0) { - JNU_ThrowByNameWithLastError(env, "java/lang/RuntimeException", + JNU_ThrowByNameWithLastError(env, "java/io/IOException", strerror(errno)); return totalSpaceIsEstimated; } diff --git a/test/jdk/java/security/Security/ConfigFileTest.java b/test/jdk/java/security/Security/SecurityPropFile/ExtraFileAndIncludes.java similarity index 88% rename from test/jdk/java/security/Security/ConfigFileTest.java rename to test/jdk/java/security/Security/SecurityPropFile/ExtraFileAndIncludes.java index caf657005e1..4cf723f856a 100644 --- a/test/jdk/java/security/Security/ConfigFileTest.java +++ b/test/jdk/java/security/Security/SecurityPropFile/ExtraFileAndIncludes.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -62,13 +62,13 @@ import java.util.stream.Stream; * @test * @summary Tests security properties passed through java.security, * java.security.properties or included from other properties files. - * @bug 8155246 8292297 8292177 8281658 8319332 + * @bug 4303068 8155246 8292297 8292177 8281658 8319332 * @modules java.base/sun.net.www * @library /test/lib - * @run main ConfigFileTest + * @run main ExtraFileAndIncludes */ -public class ConfigFileTest { +public class ExtraFileAndIncludes { static final String SEPARATOR_THIN = "----------------------------"; private static void printTestHeader(String testName) { @@ -91,7 +91,8 @@ public class ConfigFileTest { } else { // Executed by the test JVM. try (FilesManager filesMgr = new FilesManager()) { - for (Method m : ConfigFileTest.class.getDeclaredMethods()) { + for (Method m : + ExtraFileAndIncludes.class.getDeclaredMethods()) { if (m.getName().startsWith("test")) { printTestHeader(m.getName()); Executor.run(m, filesMgr); @@ -120,7 +121,7 @@ public class ConfigFileTest { static void testIncludeBasic(Executor ex, FilesManager filesMgr) throws Exception { PropsFile masterFile = filesMgr.newMasterFile(); - ExtraPropsFile extraFile = filesMgr.newExtraFile(); + ExtraPropsFile extraFile = filesMgr.newExtraFile(ExtraMode.FILE_URI); PropsFile file0 = filesMgr.newFile("file0.properties"); PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); PropsFile file2 = filesMgr.newFile("dir1/dir2/file2.properties"); @@ -130,7 +131,7 @@ public class ConfigFileTest { file2.addAbsoluteInclude(file1); ex.setMasterFile(masterFile); - ex.setExtraFile(extraFile, Executor.ExtraMode.FILE_URI, false); + ex.setExtraFile(extraFile, false); ex.assertSuccess(); } @@ -152,7 +153,7 @@ public class ConfigFileTest { static void testIncludeWithOverrideAll(Executor ex, FilesManager filesMgr) throws Exception { PropsFile masterFile = filesMgr.newMasterFile(); - ExtraPropsFile extraFile = filesMgr.newExtraFile(); + ExtraPropsFile extraFile = filesMgr.newExtraFile(ExtraMode.HTTP_SERVED); PropsFile file0 = filesMgr.newFile("file0.properties"); PropsFile file1 = filesMgr.newFile("dir1/file1.properties"); @@ -160,40 +161,40 @@ public class ConfigFileTest { extraFile.addAbsoluteInclude(file1); ex.setMasterFile(masterFile); - ex.setExtraFile(extraFile, Executor.ExtraMode.HTTP_SERVED, true); + ex.setExtraFile(extraFile, true); ex.assertSuccess(); } static void extraPropertiesByHelper(Executor ex, FilesManager filesMgr, - Executor.ExtraMode mode) throws Exception { - ExtraPropsFile extraFile = filesMgr.newExtraFile(); + ExtraMode mode) throws Exception { + ExtraPropsFile extraFile = filesMgr.newExtraFile(mode); PropsFile file0 = filesMgr.newFile("file0.properties"); extraFile.addRelativeInclude(file0); ex.setMasterFile(filesMgr.newMasterFile()); - ex.setExtraFile(extraFile, mode, true); + ex.setExtraFile(extraFile, true); ex.assertSuccess(); } static void testExtraPropertiesByPathAbsolute(Executor ex, FilesManager filesMgr) throws Exception { - extraPropertiesByHelper(ex, filesMgr, Executor.ExtraMode.PATH_ABS); + extraPropertiesByHelper(ex, filesMgr, ExtraMode.PATH_ABS); } static void testExtraPropertiesByPathRelative(Executor ex, FilesManager filesMgr) throws Exception { - extraPropertiesByHelper(ex, filesMgr, Executor.ExtraMode.PATH_REL); + extraPropertiesByHelper(ex, filesMgr, ExtraMode.PATH_REL); } static void specialCharsIncludes(Executor ex, FilesManager filesMgr, - char specialChar, Executor.ExtraMode extraMode, - boolean useRelativeIncludes) throws Exception { + char specialChar, ExtraMode extraMode, boolean useRelativeIncludes) + throws Exception { String suffix = specialChar + ".properties"; ExtraPropsFile extraFile; PropsFile file0, file1; try { - extraFile = filesMgr.newExtraFile("extra" + suffix); + extraFile = filesMgr.newExtraFile("extra" + suffix, extraMode); file0 = filesMgr.newFile("file0" + suffix); file1 = filesMgr.newFile("file1" + suffix); } catch (InvalidPathException ipe) { @@ -210,20 +211,18 @@ public class ConfigFileTest { extraFile.addAbsoluteInclude(file1); ex.setMasterFile(filesMgr.newMasterFile()); - ex.setExtraFile(extraFile, extraMode, false); + ex.setExtraFile(extraFile, false); ex.assertSuccess(); } static void testUnicodeIncludes1(Executor ex, FilesManager filesMgr) throws Exception { - specialCharsIncludes(ex, filesMgr, '\u2022', - Executor.ExtraMode.PATH_ABS, true); + specialCharsIncludes(ex, filesMgr, '\u2022', ExtraMode.PATH_ABS, true); } static void testUnicodeIncludes2(Executor ex, FilesManager filesMgr) throws Exception { - specialCharsIncludes(ex, filesMgr, '\u2022', - Executor.ExtraMode.FILE_URI, true); + specialCharsIncludes(ex, filesMgr, '\u2022', ExtraMode.FILE_URI, true); } static void testUnicodeIncludes3(Executor ex, FilesManager filesMgr) @@ -232,7 +231,7 @@ public class ConfigFileTest { // file:/tmp/extra•.properties are supported for the extra file. // However, relative includes are not allowed in these cases. specialCharsIncludes(ex, filesMgr, '\u2022', - Executor.ExtraMode.RAW_FILE_URI1, false); + ExtraMode.RAW_FILE_URI1, false); } static void testUnicodeIncludes4(Executor ex, FilesManager filesMgr) @@ -241,19 +240,17 @@ public class ConfigFileTest { // file:///tmp/extra•.properties are supported for the extra file. // However, relative includes are not allowed in these cases. specialCharsIncludes(ex, filesMgr, '\u2022', - Executor.ExtraMode.RAW_FILE_URI2, false); + ExtraMode.RAW_FILE_URI2, false); } static void testSpaceIncludes1(Executor ex, FilesManager filesMgr) throws Exception { - specialCharsIncludes(ex, filesMgr, ' ', - Executor.ExtraMode.PATH_ABS, true); + specialCharsIncludes(ex, filesMgr, ' ', ExtraMode.PATH_ABS, true); } static void testSpaceIncludes2(Executor ex, FilesManager filesMgr) throws Exception { - specialCharsIncludes(ex, filesMgr, ' ', - Executor.ExtraMode.FILE_URI, true); + specialCharsIncludes(ex, filesMgr, ' ', ExtraMode.FILE_URI, true); } static void testSpaceIncludes3(Executor ex, FilesManager filesMgr) @@ -261,8 +258,7 @@ public class ConfigFileTest { // Backward compatibility check. Malformed URLs such as // file:/tmp/extra .properties are supported for the extra file. // However, relative includes are not allowed in these cases. - specialCharsIncludes(ex, filesMgr, ' ', - Executor.ExtraMode.RAW_FILE_URI1, false); + specialCharsIncludes(ex, filesMgr, ' ', ExtraMode.RAW_FILE_URI1, false); } static void testSpaceIncludes4(Executor ex, FilesManager filesMgr) @@ -270,8 +266,7 @@ public class ConfigFileTest { // Backward compatibility check. Malformed URLs such as // file:///tmp/extra .properties are supported for the extra file. // However, relative includes are not allowed in these cases. - specialCharsIncludes(ex, filesMgr, ' ', - Executor.ExtraMode.RAW_FILE_URI2, false); + specialCharsIncludes(ex, filesMgr, ' ', ExtraMode.RAW_FILE_URI2, false); } static void notOverrideOnFailureHelper(Executor ex, FilesManager filesMgr, @@ -370,13 +365,13 @@ public class ConfigFileTest { static void testCannotResolveRelativeFromHTTPServed(Executor ex, FilesManager filesMgr) throws Exception { - ExtraPropsFile extraFile = filesMgr.newExtraFile(); + ExtraPropsFile extraFile = filesMgr.newExtraFile(ExtraMode.HTTP_SERVED); PropsFile file0 = filesMgr.newFile("file0.properties"); extraFile.addRelativeInclude(file0); ex.setMasterFile(filesMgr.newMasterFile()); - ex.setExtraFile(extraFile, Executor.ExtraMode.HTTP_SERVED, true); + ex.setExtraFile(extraFile, true); ex.assertError("InternalError: Cannot resolve '" + file0.fileName + "' relative path when included from a non-regular " + "properties file (e.g. HTTP served file)"); @@ -394,14 +389,15 @@ public class ConfigFileTest { masterFile.addRelativeInclude(file0); ex.setMasterFile(masterFile); - ex.assertError( - "InternalError: Cyclic include of '" + masterFile.path + "'"); + ex.assertError("Cyclic include"); + ex.getOutputAnalyzer().stderrShouldMatch("\\QInternalError: Cyclic " + + "include of '\\E[^']+\\Q" + masterFile.fileName + "'\\E"); } static void testCannotIncludeURL(Executor ex, FilesManager filesMgr) throws Exception { PropsFile masterFile = filesMgr.newMasterFile(); - ExtraPropsFile extraFile = filesMgr.newExtraFile(); + ExtraPropsFile extraFile = filesMgr.newExtraFile(ExtraMode.HTTP_SERVED); masterFile.addRawProperty("include", extraFile.url.toString()); @@ -432,8 +428,7 @@ public class ConfigFileTest { // Launch a JDK without a master java.security file present, but with an // extra file passed. Since the "security.overridePropertiesFile=true" // security property is missing, it should fail anyway. - ex.setExtraFile( - filesMgr.newExtraFile(), Executor.ExtraMode.FILE_URI, true); + ex.setExtraFile(filesMgr.newExtraFile(ExtraMode.FILE_URI), true); ex.assertError("InternalError: Error loading java.security file"); } } @@ -455,17 +450,24 @@ sealed class PropsFile permits ExtraPropsFile { static Include of(PropsFile propsFile, String value) { return new Include(propsFile, value); } + + void assertProcessed(OutputAnalyzer oa) { + oa.shouldContain("processing include: '" + value + "'"); + oa.shouldContain("finished processing " + propsFile.displayPath); + } } protected final List includes = new ArrayList<>(); protected final PrintWriter writer; protected boolean includedFromExtra = false; + protected Path displayPath; final String fileName; final Path path; PropsFile(String fileName, Path path) throws IOException { this.fileName = fileName; this.path = path; + this.displayPath = path; this.writer = new PrintWriter(Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.APPEND), true); } @@ -513,8 +515,9 @@ sealed class PropsFile permits ExtraPropsFile { } void addRelativeInclude(PropsFile propsFile) { - addIncludeDefinition(Include.of(propsFile, - path.getParent().relativize(propsFile.path).toString())); + Path rel = path.getParent().relativize(propsFile.path); + addIncludeDefinition(Include.of(propsFile, rel.toString())); + propsFile.displayPath = displayPath.getParent().resolve(rel); } void assertApplied(OutputAnalyzer oa) { @@ -522,8 +525,7 @@ sealed class PropsFile permits ExtraPropsFile { FilesManager.APPLIED_PROP_VALUE); for (Include include : includes) { include.propsFile.assertApplied(oa); - oa.shouldContain("processing include: '" + include.value + "'"); - oa.shouldContain("finished processing " + include.propsFile.path); + include.assertProcessed(oa); } } @@ -534,8 +536,7 @@ sealed class PropsFile permits ExtraPropsFile { if (!include.propsFile.includedFromExtra) { include.propsFile.assertWasOverwritten(oa); } - oa.shouldContain("processing include: '" + include.value + "'"); - oa.shouldContain("finished processing " + include.propsFile.path); + include.assertProcessed(oa); } } @@ -556,13 +557,24 @@ sealed class PropsFile permits ExtraPropsFile { } } +enum ExtraMode { + HTTP_SERVED, FILE_URI, RAW_FILE_URI1, RAW_FILE_URI2, PATH_ABS, PATH_REL +} + final class ExtraPropsFile extends PropsFile { + private static final Path CWD = Path.of(".").toAbsolutePath(); private final Map systemProps = new LinkedHashMap<>(); + private final ExtraMode mode; final URI url; - ExtraPropsFile(String fileName, URI url, Path path) throws IOException { + ExtraPropsFile(String fileName, URI url, Path path, ExtraMode mode) + throws IOException { super(fileName, path); this.url = url; + this.mode = mode; + if (mode == ExtraMode.PATH_REL) { + this.displayPath = CWD.relativize(path); + } } @Override @@ -578,14 +590,25 @@ final class ExtraPropsFile extends PropsFile { super.addIncludeDefinition(include); } + String getSysPropValue() { + return switch (mode) { + case HTTP_SERVED -> url.toString(); + case FILE_URI -> path.toUri().toString(); + case RAW_FILE_URI1 -> "file:" + path; + case RAW_FILE_URI2 -> + "file://" + (path.startsWith("/") ? "" : "/") + path; + case PATH_ABS, PATH_REL -> displayPath.toString(); + }; + } + Map getSystemProperties() { return Collections.unmodifiableMap(systemProps); } } final class FilesManager implements Closeable { - private static final Path ROOT_DIR = - Path.of(ConfigFileTest.class.getSimpleName()).toAbsolutePath(); + private static final Path ROOT_DIR = Path.of( + ExtraFileAndIncludes.class.getSimpleName()).toAbsolutePath(); private static final Path PROPS_DIR = ROOT_DIR.resolve("properties"); private static final Path JDK_DIR = ROOT_DIR.resolve("jdk"); private static final Path MASTER_FILE = @@ -684,11 +707,11 @@ final class FilesManager implements Closeable { propsFile.addComment("Property to determine if this properties file " + "was parsed and not overwritten:"); propsFile.addRawProperty(fileName, APPLIED_PROP_VALUE); - propsFile.addComment(ConfigFileTest.SEPARATOR_THIN); + propsFile.addComment(ExtraFileAndIncludes.SEPARATOR_THIN); propsFile.addComment("Property to be overwritten by every properties " + "file (master, extra or included):"); propsFile.addRawProperty(LAST_FILE_PROP_NAME, fileName); - propsFile.addComment(ConfigFileTest.SEPARATOR_THIN); + propsFile.addComment(ExtraFileAndIncludes.SEPARATOR_THIN); createdFiles.add(propsFile); return propsFile; } @@ -702,16 +725,17 @@ final class FilesManager implements Closeable { return newFile(MASTER_FILE, PropsFile::new); } - ExtraPropsFile newExtraFile() throws IOException { - return newExtraFile("extra.properties"); + ExtraPropsFile newExtraFile(ExtraMode mode) throws IOException { + return newExtraFile("extra.properties", mode); } - ExtraPropsFile newExtraFile(String extraFileName) throws IOException { + ExtraPropsFile newExtraFile(String extraFileName, ExtraMode mode) + throws IOException { return (ExtraPropsFile) newFile(PROPS_DIR.resolve(extraFileName), (fileName, path) -> { URI uri = serverUri.resolve(ParseUtil.encodePath( ROOT_DIR.relativize(path).toString())); - return new ExtraPropsFile(fileName, uri, path); + return new ExtraPropsFile(fileName, uri, path, mode); }); } @@ -719,7 +743,7 @@ final class FilesManager implements Closeable { for (PropsFile propsFile : createdFiles) { System.err.println(); System.err.println(propsFile.path.toString()); - System.err.println(ConfigFileTest.SEPARATOR_THIN.repeat(3)); + System.err.println(ExtraFileAndIncludes.SEPARATOR_THIN.repeat(3)); try (Stream lines = Files.lines(propsFile.path)) { long lineNumber = 1L; Iterator it = lines.iterator(); @@ -757,9 +781,6 @@ final class FilesManager implements Closeable { } final class Executor { - enum ExtraMode { - HTTP_SERVED, FILE_URI, RAW_FILE_URI1, RAW_FILE_URI2, PATH_ABS, PATH_REL - } static final String RUNNER_ARG = "runner"; static final String INITIAL_PROP_LOG_MSG = "Initial security property: "; private static final String OVERRIDING_LOG_MSG = @@ -769,7 +790,6 @@ final class Executor { INITIAL_PROP_LOG_MSG + "postInitTest=shouldNotRecord", INITIAL_PROP_LOG_MSG + "include=", }; - private static final Path CWD = Path.of(".").toAbsolutePath(); private static final String JAVA_SEC_PROPS = "java.security.properties"; private static final String CLASS_PATH = Objects.requireNonNull( System.getProperty("test.classes"), "unspecified test.classes"); @@ -812,20 +832,10 @@ final class Executor { this.masterPropsFile = masterPropsFile; } - void setExtraFile(ExtraPropsFile extraPropsFile, ExtraMode mode, - boolean overrideAll) { + void setExtraFile(ExtraPropsFile extraPropsFile, boolean overrideAll) { this.extraPropsFile = extraPropsFile; expectedOverrideAll = overrideAll; - setRawExtraFile(switch (mode) { - case HTTP_SERVED -> extraPropsFile.url.toString(); - case FILE_URI -> extraPropsFile.path.toUri().toString(); - case RAW_FILE_URI1 -> "file:" + extraPropsFile.path; - case RAW_FILE_URI2 -> "file://" + - (extraPropsFile.path.startsWith("/") ? "" : "/") + - extraPropsFile.path; - case PATH_ABS -> extraPropsFile.path.toString(); - case PATH_REL -> CWD.relativize(extraPropsFile.path).toString(); - }, overrideAll); + setRawExtraFile(extraPropsFile.getSysPropValue(), overrideAll); } void setIgnoredExtraFile(String extraPropsFile, boolean overrideAll) { @@ -841,7 +851,7 @@ final class Executor { List command = new ArrayList<>(jvmArgs); Collections.addAll(command, Utils.getTestJavaOpts()); addSystemPropertiesAsJvmArgs(command); - command.add(ConfigFileTest.class.getSimpleName()); + command.add(ExtraFileAndIncludes.class.getSimpleName()); command.add(RUNNER_ARG); oa = ProcessTools.executeProcess(new ProcessBuilder(command)); oa.shouldHaveExitValue(successExpected ? 0 : 1); diff --git a/test/jdk/java/security/Security/SecurityPropFile/LinuxAnonymousFiles.java b/test/jdk/java/security/Security/SecurityPropFile/LinuxAnonymousFiles.java new file mode 100644 index 00000000000..7ca2a7c0f8b --- /dev/null +++ b/test/jdk/java/security/Security/SecurityPropFile/LinuxAnonymousFiles.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2026, Red Hat, Inc. + * + * 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 jdk.test.lib.process.ProcessTools; + +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; + +/* + * @test + * @summary Ensures the java executable is able to load extra security + * properties files from anonymous files and pipes. + * @bug 8352728 + * @requires os.family == "linux" + * @modules java.base/java.io:+open + * @library /test/lib + * @run main LinuxAnonymousFiles + */ + +public class LinuxAnonymousFiles { + private static final String TEST_PROP = "property.name=PROPERTY_VALUE"; + + private static final class AnonymousFile implements AutoCloseable { + public final Path fdPath; + private final FileInputStream fis; + + private AnonymousFile(CharSequence content) throws Exception { + Path tmp = Files.createTempFile("anonymous-file-", ""); + Files.writeString(tmp, content + System.lineSeparator()); + fis = new FileInputStream(tmp.toFile()); + Files.delete(tmp); + // Now the file is regular but anonymous, and will be unlinked + // when we close the last file descriptor referring to it. The + // fis instance ensures we keep it alive until close() is invoked. + Field field = FileDescriptor.class.getDeclaredField("fd"); + field.setAccessible(true); + int fd = field.getInt(fis.getFD()); + fdPath = Path.of("/proc/self").toRealPath().resolve("fd/" + fd); + } + + @Override + public void close() throws IOException { + fis.close(); + } + } + + public static void main(String[] args) throws Exception { + Path java = Path.of(System.getProperty("test.jdk"), "bin", "java"); + try (AnonymousFile af = new AnonymousFile("include /dev/stdin")) { + ProcessTools.executeProcess(new ProcessBuilder(java.toString(), + "-Djava.security.debug=properties", + "-Djava.security.properties=" + af.fdPath, + "-XshowSettings:security:properties", "-version"), + TEST_PROP).shouldHaveExitValue(0).shouldContain(TEST_PROP); + } + System.out.println("TEST PASS - OK"); + } +} diff --git a/test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.file b/test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.file deleted file mode 100644 index 2b4c08c6903..00000000000 --- a/test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.file +++ /dev/null @@ -1 +0,0 @@ -policy.url.2=file:${test.src}/SecurityPropFile.policy diff --git a/test/jdk/java/security/Security/SecurityPropFile/WindowsParentDirPermissions.java b/test/jdk/java/security/Security/SecurityPropFile/WindowsParentDirPermissions.java new file mode 100644 index 00000000000..a41fd2f3535 --- /dev/null +++ b/test/jdk/java/security/Security/SecurityPropFile/WindowsParentDirPermissions.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2026, Red Hat, Inc. + * + * 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 jdk.test.lib.process.ProcessTools; +import jdk.test.lib.util.FileUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.util.List; + +/* + * @test + * @summary Ensures java.security is loadable in Windows, even when the user + * does not have permissions on one of the parent directories. + * @bug 8352728 + * @requires os.family == "windows" + * @library /test/lib + * @run main WindowsParentDirPermissions + */ + +public class WindowsParentDirPermissions { + private static AutoCloseable restrictedAcl(Path path) throws IOException { + AclFileAttributeView view = + Files.getFileAttributeView(path, AclFileAttributeView.class); + List originalAcl = List.copyOf(view.getAcl()); + view.setAcl(List.of(AclEntry.newBuilder().setType(AclEntryType.DENY) + .setPrincipal(Files.getOwner(path)).build())); + return () -> view.setAcl(originalAcl); + } + + public static void main(String[] args) throws Exception { + Path temp = Files.createTempDirectory("JDK-8352728-tmp-"); + try (AutoCloseable a1 = () -> FileUtils.deleteFileTreeUnchecked(temp)) { + // Copy the jdk to a different directory + Path originalJdk = Path.of(System.getProperty("test.jdk")); + Path jdk = temp.resolve("jdk-parent-dir", "jdk"); + Files.createDirectories(jdk); + FileUtils.copyDirectory(originalJdk, jdk); + + // Remove current user permissions from jdk-parent-dir + try (AutoCloseable a2 = restrictedAcl(jdk.getParent())) { + // Make sure the permissions are affecting the current user + try { + jdk.toRealPath(); + throw new jtreg.SkippedException("Must run non-elevated!"); + } catch (IOException expected) { } + + // Execute the copied jdk, ensuring java.security.Security is + // loaded (i.e. use -XshowSettings:security:properties) + ProcessTools.executeProcess(new ProcessBuilder( + List.of(jdk.resolve("bin", "java.exe").toString(), + "-Djava.security.debug=properties", + "-XshowSettings:security:properties", + "-version"))).shouldHaveExitValue(0); + } + } + System.out.println("TEST PASS - OK"); + } +} diff --git a/test/jdk/java/time/tck/java/time/AbstractDateTimeTest.java b/test/jdk/java/time/tck/java/time/AbstractDateTimeTest.java index f96ab522517..cf138419c83 100644 --- a/test/jdk/java/time/tck/java/time/AbstractDateTimeTest.java +++ b/test/jdk/java/time/tck/java/time/AbstractDateTimeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -59,8 +59,11 @@ */ package tck.java.time; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.time.DateTimeException; import java.time.temporal.TemporalAccessor; @@ -68,8 +71,8 @@ import java.time.temporal.TemporalField; import java.time.temporal.TemporalQuery; import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import test.java.time.temporal.MockFieldNoValue; @@ -99,183 +102,137 @@ public abstract class AbstractDateTimeTest extends AbstractTCKTest { //----------------------------------------------------------------------- // isSupported(TemporalField) //----------------------------------------------------------------------- - @Test() - public void basicTest_isSupported_TemporalField_supported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : validFields()) { - assertEquals(true, sample.isSupported(field), "Failed on " + sample + " " + field); - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_isSupported_TemporalField_supported(TemporalAccessor sample) { + for (TemporalField field : validFields()) { + assertTrue(sample.isSupported(field), "Failed on " + sample + " " + field); } } - @Test() - public void basicTest_isSupported_TemporalField_unsupported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : invalidFields()) { - assertEquals(false, sample.isSupported(field), "Failed on " + sample + " " + field); - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_isSupported_TemporalField_unsupported(TemporalAccessor sample) { + for (TemporalField field : invalidFields()) { + assertFalse(sample.isSupported(field), "Failed on " + sample + " " + field); } } - @Test() - public void basicTest_isSupported_TemporalField_null() { - for (TemporalAccessor sample : samples()) { - assertEquals(false, sample.isSupported(null), "Failed on " + sample); - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_isSupported_TemporalField_null(TemporalAccessor sample) { + assertFalse(sample.isSupported(null), "Failed on " + sample); } //----------------------------------------------------------------------- // range(TemporalField) //----------------------------------------------------------------------- - @Test() - public void basicTest_range_TemporalField_supported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : validFields()) { - sample.range(field); // no exception - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_range_TemporalField_supported(TemporalAccessor sample) { + for (TemporalField field : validFields()) { + assertDoesNotThrow(() -> sample.range(field)); } } - @Test() - public void basicTest_range_TemporalField_unsupported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : invalidFields()) { - try { - sample.range(field); - fail("Failed on " + sample + " " + field); - } catch (DateTimeException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_range_TemporalField_unsupported(TemporalAccessor sample) { + for (TemporalField field : invalidFields()) { + assertThrows(DateTimeException.class, + () -> sample.range(field), "Failed on " + sample + " " + field); } } - @Test() - public void basicTest_range_TemporalField_null() { - for (TemporalAccessor sample : samples()) { - try { - sample.range(null); - fail("Failed on " + sample); - } catch (NullPointerException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_range_TemporalField_null(TemporalAccessor sample) { + assertThrows(NullPointerException.class, + () -> sample.range(null), "Failed on " + sample); } //----------------------------------------------------------------------- // get(TemporalField) //----------------------------------------------------------------------- - @Test() - public void basicTest_get_TemporalField_supported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : validFields()) { - if (sample.range(field).isIntValue()) { - sample.get(field); // no exception - } else { - try { - sample.get(field); - fail("Failed on " + sample + " " + field); - } catch (DateTimeException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_get_TemporalField_supported(TemporalAccessor sample) { + for (TemporalField field : validFields()) { + if (sample.range(field).isIntValue()) { + assertDoesNotThrow(() -> sample.get(field)); + } else { + assertThrows(DateTimeException.class, + () -> sample.get(field), "Failed on " + sample + " " + field); } } } - @Test() - public void basicTest_get_TemporalField_unsupported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : invalidFields()) { - try { - sample.get(field); - fail("Failed on " + sample + " " + field); - } catch (DateTimeException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_get_TemporalField_unsupported(TemporalAccessor sample) { + for (TemporalField field : invalidFields()) { + assertThrows(DateTimeException.class, + () -> sample.get(field), "Failed on " + sample + " " + field); } } - @Test - public void test_get_TemporalField_invalidField() { - Assertions.assertThrows(DateTimeException.class, () -> { - for (TemporalAccessor sample : samples()) { - sample.get(MockFieldNoValue.INSTANCE); - } - }); + @ParameterizedTest + @MethodSource("samples") + public void test_get_TemporalField_invalidField(TemporalAccessor sample) { + assertThrows(DateTimeException.class, + () -> sample.get(MockFieldNoValue.INSTANCE)); } - @Test() - public void basicTest_get_TemporalField_null() { - for (TemporalAccessor sample : samples()) { - try { - sample.get(null); - fail("Failed on " + sample); - } catch (NullPointerException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_get_TemporalField_null(TemporalAccessor sample) { + assertThrows(NullPointerException.class, + () -> sample.get(null), "Failed on " + sample); } //----------------------------------------------------------------------- // getLong(TemporalField) //----------------------------------------------------------------------- - @Test() - public void basicTest_getLong_TemporalField_supported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : validFields()) { - sample.getLong(field); // no exception - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_getLong_TemporalField_supported(TemporalAccessor sample) { + for (TemporalField field : validFields()) { + sample.getLong(field); } } - @Test() - public void basicTest_getLong_TemporalField_unsupported() { - for (TemporalAccessor sample : samples()) { - for (TemporalField field : invalidFields()) { - try { - sample.getLong(field); - fail("Failed on " + sample + " " + field); - } catch (DateTimeException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_getLong_TemporalField_unsupported(TemporalAccessor sample) { + for (TemporalField field : invalidFields()) { + assertThrows(DateTimeException.class, + () -> sample.getLong(field), "Failed on " + sample + " " + field); } } - @Test - public void test_getLong_TemporalField_invalidField() { - Assertions.assertThrows(DateTimeException.class, () -> { - for (TemporalAccessor sample : samples()) { - sample.getLong(MockFieldNoValue.INSTANCE); - } - }); + @ParameterizedTest + @MethodSource("samples") + public void test_getLong_TemporalField_invalidField(TemporalAccessor sample) { + assertThrows(DateTimeException.class, + () -> sample.getLong(MockFieldNoValue.INSTANCE)); } - @Test() - public void basicTest_getLong_TemporalField_null() { - for (TemporalAccessor sample : samples()) { - try { - sample.getLong(null); - fail("Failed on " + sample); - } catch (NullPointerException ex) { - // expected - } - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_getLong_TemporalField_null(TemporalAccessor sample) { + assertThrows(NullPointerException.class, + () -> sample.getLong(null), "Failed on " + sample); } //----------------------------------------------------------------------- - @Test - public void basicTest_query() { - for (TemporalAccessor sample : samples()) { - assertEquals("foo", sample.query(new TemporalQuery() { - @Override - public String queryFrom(TemporalAccessor temporal) { - return "foo"; - } - })); - } + @ParameterizedTest + @MethodSource("samples") + public void basicTest_query(TemporalAccessor sample) { + assertEquals("foo", sample.query(new TemporalQuery() { + @Override + public String queryFrom(TemporalAccessor temporal) { + return "foo"; + } + })); } - } diff --git a/test/jdk/java/time/tck/java/time/TCKInstant.java b/test/jdk/java/time/tck/java/time/TCKInstant.java index 4a512d37665..8c245d69810 100644 --- a/test/jdk/java/time/tck/java/time/TCKInstant.java +++ b/test/jdk/java/time/tck/java/time/TCKInstant.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -139,7 +139,8 @@ public class TCKInstant extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_12345_123456789, Instant.MIN, Instant.MAX, Instant.EPOCH}; + TemporalAccessor[] array = {Instant.ofEpochSecond(12345, 123456789), + Instant.MIN, Instant.MAX, Instant.EPOCH}; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKLocalDate.java b/test/jdk/java/time/tck/java/time/TCKLocalDate.java index 281007e7665..132aa31ed97 100644 --- a/test/jdk/java/time/tck/java/time/TCKLocalDate.java +++ b/test/jdk/java/time/tck/java/time/TCKLocalDate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -170,7 +170,7 @@ public class TCKLocalDate extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_2007_07_15, LocalDate.MAX, LocalDate.MIN, }; + TemporalAccessor[] array = {LocalDate.of(2007, 7, 15), LocalDate.MAX, LocalDate.MIN, }; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKLocalTime.java b/test/jdk/java/time/tck/java/time/TCKLocalTime.java index bdec611ad76..e2fc4ea2ca7 100644 --- a/test/jdk/java/time/tck/java/time/TCKLocalTime.java +++ b/test/jdk/java/time/tck/java/time/TCKLocalTime.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -161,7 +161,8 @@ public class TCKLocalTime extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_12_30_40_987654321, LocalTime.MIN, LocalTime.MAX, LocalTime.MIDNIGHT, LocalTime.NOON}; + TemporalAccessor[] array = {LocalTime.of(12, 30, 40, 987654321), + LocalTime.MIN, LocalTime.MAX, LocalTime.MIDNIGHT, LocalTime.NOON}; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKMonthDay.java b/test/jdk/java/time/tck/java/time/TCKMonthDay.java index 84326a4c47a..36ad4af069e 100644 --- a/test/jdk/java/time/tck/java/time/TCKMonthDay.java +++ b/test/jdk/java/time/tck/java/time/TCKMonthDay.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -118,7 +118,7 @@ public class TCKMonthDay extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_07_15, }; + TemporalAccessor[] array = {MonthDay.of(7, 15), }; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKOffsetDateTime.java b/test/jdk/java/time/tck/java/time/TCKOffsetDateTime.java index f2b98ee9972..6ba0318890f 100644 --- a/test/jdk/java/time/tck/java/time/TCKOffsetDateTime.java +++ b/test/jdk/java/time/tck/java/time/TCKOffsetDateTime.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -167,7 +167,8 @@ public class TCKOffsetDateTime extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_2008_6_30_11_30_59_000000500, OffsetDateTime.MIN, OffsetDateTime.MAX}; + TemporalAccessor[] array = {OffsetDateTime.of(2008, 6, 30, 11, 30, 59, 500, OFFSET_PONE), + OffsetDateTime.MIN, OffsetDateTime.MAX}; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKOffsetTime.java b/test/jdk/java/time/tck/java/time/TCKOffsetTime.java index 350b21574fa..7ea9504edbc 100644 --- a/test/jdk/java/time/tck/java/time/TCKOffsetTime.java +++ b/test/jdk/java/time/tck/java/time/TCKOffsetTime.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -149,7 +149,8 @@ public class TCKOffsetTime extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_11_30_59_500_PONE, OffsetTime.MIN, OffsetTime.MAX}; + TemporalAccessor[] array = {OffsetTime.of(11, 30, 59, 500, OFFSET_PONE), + OffsetTime.MIN, OffsetTime.MAX}; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKYearMonth.java b/test/jdk/java/time/tck/java/time/TCKYearMonth.java index 3f4fda2a924..e39bbe90cf9 100644 --- a/test/jdk/java/time/tck/java/time/TCKYearMonth.java +++ b/test/jdk/java/time/tck/java/time/TCKYearMonth.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -134,7 +134,7 @@ public class TCKYearMonth extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_2008_06, }; + TemporalAccessor[] array = {YearMonth.of(2008, 6), }; return Arrays.asList(array); } diff --git a/test/jdk/java/time/tck/java/time/TCKZonedDateTime.java b/test/jdk/java/time/tck/java/time/TCKZonedDateTime.java index 847035f121a..73adbbba88c 100644 --- a/test/jdk/java/time/tck/java/time/TCKZonedDateTime.java +++ b/test/jdk/java/time/tck/java/time/TCKZonedDateTime.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, 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 @@ -176,7 +176,7 @@ public class TCKZonedDateTime extends AbstractDateTimeTest { //----------------------------------------------------------------------- @Override protected List samples() { - TemporalAccessor[] array = {TEST_DATE_TIME, }; + TemporalAccessor[] array = {ZonedDateTime.of(LocalDateTime.of(2008, 6, 30, 11, 30, 59, 500), ZONE_0100), }; return Arrays.asList(array); } diff --git a/test/jdk/javax/swing/Security/6657138/bug6657138.java b/test/jdk/javax/swing/Security/6657138/bug6657138.java deleted file mode 100644 index 4670d57595c..00000000000 --- a/test/jdk/javax/swing/Security/6657138/bug6657138.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2009, 2018, 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. - */ - -/* - * @test - * @bug 6657138 - * @summary Verifies that buttons and labels don't share their ui's across appContexts - * @author Alexander Potochkin - * @modules java.desktop/sun.awt - */ - -import sun.awt.SunToolkit; - -import javax.swing.*; -import javax.swing.plaf.ButtonUI; -import javax.swing.plaf.ComponentUI; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -public class bug6657138 implements Runnable { - - private static Map> componentMap = - Collections.synchronizedMap( - new HashMap>()); - - public void run() { - SunToolkit.createNewAppContext(); - try { - testUIMap(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private static void testUIMap() throws Exception { - UIManager.LookAndFeelInfo[] lafs = UIManager.getInstalledLookAndFeels(); - Set components = componentMap.keySet(); - for (JComponent c : components) { - Map uiMap = componentMap.get(c); - - for (UIManager.LookAndFeelInfo laf : lafs) { - if ("Nimbus".equals(laf.getName())) { - // for some unclear reasons - // Nimbus ui delegate for a button is null - // when this method is called from the new AppContext - continue; - } - String className = laf.getClassName(); - try { - UIManager.setLookAndFeel(className); - } catch (final UnsupportedLookAndFeelException ignored) { - continue; - } - ComponentUI ui = UIManager.getUI(c); - if (ui == null) { - throw new RuntimeException("UI is null for " + c); - } - if (ui == uiMap.get(laf.getName())) { - throw new RuntimeException( - "Two AppContexts share the same UI delegate! \n" + - c + "\n" + ui); - } - uiMap.put(laf.getName(), ui); - } - } - } - - public static void main(String[] args) throws Exception { - componentMap.put(new JButton("JButton"), - new HashMap()); - componentMap.put(new JToggleButton("JToggleButton"), - new HashMap()); - componentMap.put(new JRadioButton("JRadioButton"), - new HashMap()); - componentMap.put(new JCheckBox("JCheckBox"), - new HashMap()); - componentMap.put(new JCheckBox("JLabel"), - new HashMap()); - testUIMap(); - ThreadGroup group = new ThreadGroup("6657138"); - Thread thread = new Thread(group, new bug6657138()); - thread.start(); - thread.join(); - } -} - diff --git a/test/jdk/jdk/jfr/event/tracing/TestConstructors.java b/test/jdk/jdk/jfr/event/tracing/TestConstructors.java new file mode 100644 index 00000000000..26548646b49 --- /dev/null +++ b/test/jdk/jdk/jfr/event/tracing/TestConstructors.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025, 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. + */ +package jdk.jfr.event.tracing; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedMethod; +import jdk.jfr.consumer.RecordingFile; +import jdk.test.lib.jfr.Events; + +/** + * @test + * @summary Tests that constructors are instrumented correctly. + * @requires vm.flagless + * @requires vm.hasJFR + * @library /test/lib + * @run main/othervm -Xlog:jfr+methodtrace=debug + * jdk.jfr.event.tracing.TestConstructors + **/ +public class TestConstructors { + static private void methodThatThrows() { + throw new RuntimeException(); + } + + public static class Cat { + Cat() { + new String(); + methodThatThrows(); + super(); + methodThatThrows(); + } + } + + public static class Dog { + Dog() { + super(); + methodThatThrows(); + } + } + + public static class Tiger { + Tiger() { + methodThatThrows(); + super(); + } + } + + public static class Zebra { + Zebra(boolean shouldThrow) { + this(shouldThrow ? 1 : 0); + } + + Zebra(int shouldThrow) { + if (shouldThrow == 1) { + throw new RuntimeException(); + } + } + } + + public static class Snake { + Snake() { + try { + throw new RuntimeException(); + } catch (Exception e) { + // Ignore + } + super(); + } + } + + public static void main(String... args) throws Exception { + try (Recording r = new Recording()) { + r.enable("jdk.MethodTrace").with("filter", Dog.class.getName() + ";" + Cat.class.getName() + ";" + Tiger.class.getName() + ";" + Zebra.class.getName() + ";" + Snake.class.getName()); + r.start(); + try { + new Cat(); + } catch (Exception e) { + // ignore + } + try { + new Dog(); + } catch (Exception e) { + // ignore + } + try { + new Tiger(); + } catch (Exception e) { + // ignore + } + try { + new Zebra(true); + } catch (Exception e) { + // ignore + } + try { + new Zebra(false); + } catch (Exception e) { + // ignore + } + try { + new Snake(); + } catch (Exception e) { + // ignore + } + r.stop(); + List events = Events.fromRecording(r); + var methods = buildMethodMap(events); + if (methods.size() != 5) { + throw new Exception("Expected 5 different methods"); + } + assertMethodCount(methods, "Cat", 1); + assertMethodCount(methods, "Dog", 1); + assertMethodCount(methods, "Snake", 1); + assertMethodCount(methods, "Tiger", 1); + assertMethodCount(methods, "Zebra", 3); + } + } + + private static void assertMethodCount(Map methods, String className, int expectedCount) throws Exception { + String name = TestConstructors.class.getName() + "$" + className + "::"; + Long count = methods.get(name); + if (count == null) { + throw new Exception("Could not find traced method " + name); + } + if (count != expectedCount) { + throw new Exception("Expected " + expectedCount + " trace event for " + name); + } + } + + private static Map buildMethodMap(List events) { + Map map = new TreeMap<>(); + for (RecordedEvent e : events) { + RecordedMethod m = e.getValue("method"); + String name = m.getType().getName() + "::" + m.getName(); + map.compute(name, (_, value) -> (value == null) ? 1 : value + 1); + } + for (var e : map.entrySet()) { + System.out.println(e.getKey() + " " + e.getValue()); + } + return map; + } +} \ No newline at end of file diff --git a/test/jdk/jdk/jfr/event/tracing/TestInstrumentation.java b/test/jdk/jdk/jfr/event/tracing/TestInstrumentation.java index 834d4ab4989..5709c95812a 100644 --- a/test/jdk/jdk/jfr/event/tracing/TestInstrumentation.java +++ b/test/jdk/jdk/jfr/event/tracing/TestInstrumentation.java @@ -93,6 +93,8 @@ public class TestInstrumentation { assertMethod(map, "exception", 2); assertMethod(map, "switchExpression", 3); assertMethod(map, "recursive", 4); + assertMethod(map, "deepException", 1); + assertMethod(map, "whileTrue", 1); assertMethod(map, "multipleReturns", 5); if (!map.isEmpty()) { throw new Exception("Found unexpected methods " + map.keySet()); @@ -105,6 +107,8 @@ public class TestInstrumentation { assertMethod(map, "exception", 2); assertMethod(map, "switchExpression", 3); assertMethod(map, "recursive", 4); + assertMethod(map, "deepException", 1); + assertMethod(map, "whileTrue", 1); assertMethod(map, "multipleReturns", 5); for (var entry : map.entrySet()) { long invocations = entry.getValue(); diff --git a/test/jdk/sun/awt/image/png/PngImageDecoder8BitTest.java b/test/jdk/sun/awt/image/png/PngImageDecoder8BitTest.java new file mode 100644 index 00000000000..b2c134e09b2 --- /dev/null +++ b/test/jdk/sun/awt/image/png/PngImageDecoder8BitTest.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2026, 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. + */ + +/* + * @test + * @bug 8374377 + * @summary This test confirms the PNGImageProducer decodes 8-bit interlaced + * and non-interlaced PNGs correctly. + */ + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ImageConsumer; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +/** + * The proposed change for 8374377 affects how 8-bit PNGs are decoded. + * So this test confirms that 8-bit PNGs (both interlaced and non-interlaced) + * are still decoded by the PNGImageDecoder so they match what ImageIO decodes. + * + * This test has never failed. + */ +public class PngImageDecoder8BitTest { + + static BufferedImage createBufferedImage(Image img) + throws ExecutionException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + img.getSource().startProduction(new ImageConsumer() { + private int imageWidth, imageHeight; + private BufferedImage bi; + + @Override + public void setDimensions(int width, int height) { + imageWidth = width; + imageHeight = height; + } + + @Override + public void setProperties(Hashtable props) { + // intentionally empty + } + + @Override + public void setColorModel(ColorModel model) { + // intentionally empty + } + + @Override + public void setHints(int hintflags) { + // intentionally empty + } + + @Override + public void setPixels(int x, int y, int w, int h, ColorModel model, + byte[] pixels, int off, int scansize) { + if (bi == null) { + bi = new BufferedImage(imageWidth, imageHeight, + BufferedImage.TYPE_BYTE_INDEXED, + (IndexColorModel) model); + } + + if (w == imageWidth && h == imageHeight) { + // this is how interlaced PNGs are decoded: + bi.getRaster().setDataElements(0, 0, + imageWidth, imageHeight, pixels); + return; + } + + if (h != 1) { + throw new UnsupportedOperationException( + "this test requires h = 1"); + } + if (off != 0) { + throw new UnsupportedOperationException( + "this test requires off = 0"); + } + + bi.getRaster().setDataElements(x, y, w, 1, pixels); + } + + @Override + public void setPixels(int x, int y, int w, int h, ColorModel model, + int[] pixels, int off, int scansize) { + throw new UnsupportedOperationException(); + } + + @Override + public void imageComplete(int status) { + future.complete(bi); + } + }); + return future.get(); + } + + public static void main(String[] args) throws Exception { + BufferedImage expected = createImageData(); + for (boolean interlace : new boolean[] { false, true} ) { + System.out.println("Testing interlacing = "+ interlace); + byte[] imageData = encodePNG(expected, interlace); + + Image i = Toolkit.getDefaultToolkit().createImage(imageData); + BufferedImage actual = createBufferedImage(i); + + testCorrectness(expected, actual); + } + System.out.println("Confirmed that 8-bit PNGs decode correctly " + + "whether we use interlacing or not."); + } + + /** + * Create a large sample image stored as an 8-bit PNG. + */ + private static BufferedImage createImageData() { + BufferedImage bi = new BufferedImage(6000, 6000, + BufferedImage.TYPE_BYTE_INDEXED); + Random r = new Random(0); + Graphics2D g = bi.createGraphics(); + for (int a = 0; a < 20000; a++) { + g.setColor(new Color(r.nextInt(0xffffff))); + int radius = 10 + r.nextInt(90); + g.fillOval(r.nextInt(bi.getWidth()), r.nextInt(bi.getHeight()), + radius, radius); + } + g.dispose(); + return bi; + } + + /** + * Encode an image as 8-bit PNG. + */ + private static byte[] encodePNG(BufferedImage bi, boolean interlace) + throws IOException { + Iterator writers = + ImageIO.getImageWritersByFormatName("png"); + if (!writers.hasNext()) { + throw new IllegalStateException("No PNG writers found"); + } + ImageWriter writer = writers.next(); + + ImageWriteParam param = writer.getDefaultWriteParam(); + if (interlace) { + param.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); + } + + try (ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + ImageOutputStream imageOut = + ImageIO.createImageOutputStream(byteOut)) { + writer.setOutput(imageOut); + writer.write(null, new IIOImage(bi, null, null), param); + return byteOut.toByteArray(); + } finally { + writer.dispose(); + } + } + + /** + * This throws an Error if the two images are not identical. + *

    + * This unit test is intended to accompany a performance enhancement for + * PNGImageDecoder. This method makes sure the enhancement didn't cost us + * any accuracy. + */ + private static void testCorrectness(BufferedImage expected, + BufferedImage actual) { + if (expected.getWidth() != actual.getWidth()) { + throw new RuntimeException("expected.getWidth() = " + + expected.getWidth() + ", actual.getWidth() = " + + actual.getWidth()); + } + if (expected.getHeight() != actual.getHeight()) { + throw new RuntimeException("expected.getHeight() = " + + expected.getHeight() + ", actual.getHeight() = " + + actual.getHeight()); + } + for (int y = 0; y < expected.getHeight(); y++) { + for (int x = 0; x < expected.getWidth(); x++) { + int argb1 = expected.getRGB(x, y); + int argb2 = actual.getRGB(x, y); + if (argb1 != argb2) { + throw new RuntimeException("x = " + x + ", y = " + y + + " argb1 = " + Integer.toUnsignedString(argb1, 16) + + " argb2 = " + Integer.toUnsignedString(argb2, 16)); + } + } + } + } +} diff --git a/test/jdk/sun/security/ec/ECDSAPrimitive.java b/test/jdk/sun/security/ec/ECDSAPrimitive.java index 71e2e30044b..b8901a784ff 100644 --- a/test/jdk/sun/security/ec/ECDSAPrimitive.java +++ b/test/jdk/sun/security/ec/ECDSAPrimitive.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -249,7 +249,7 @@ public class ECDSAPrimitive { byte[] u1Bytes = u1.asByteArray(length); byte[] u2Bytes = u2.asByteArray(length); - AffinePoint publicKeyPoint = ECDSAOperations.toAffinePoint(publicKey, + AffinePoint publicKeyPoint = AffinePoint.fromECPoint(publicKey, ecOps.getField()); MutablePoint R = ecOps.multiply(publicKeyPoint, u2Bytes); AffinePoint a1 = ops.basePointMultiply(u1Bytes); diff --git a/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/Chunghwa.java b/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/Chunghwa.java new file mode 100644 index 00000000000..8512640bb01 --- /dev/null +++ b/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/Chunghwa.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2026, 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 javax.net.ssl.X509TrustManager; +import java.io.File; +import java.security.Security; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Date; + +/* + * @test + * @bug 8369282 + * @summary Check that TLS Server certificates chaining back to distrusted + * Chunghwa root are invalid + * @library /test/lib + * @modules java.base/sun.security.validator + * @run main/othervm Chunghwa after policyOn invalid + * @run main/othervm Chunghwa after policyOff valid + * @run main/othervm Chunghwa before policyOn valid + * @run main/othervm Chunghwa before policyOff valid + */ + +public class Chunghwa { + + private static final String CERT_PATH = "chains" + File.separator + "chunghwa"; + + // The ePKI root has a test certificate chain stored in a file + // named "-chain.pem". + private static final String ROOT_TO_TEST = "chunghwaepkirootca"; + + // Date after the restrictions take effect + private static final ZonedDateTime DISTRUST_DATE = + LocalDate.of(2026, 03, 18).atStartOfDay(ZoneOffset.UTC); + + public static void main(String[] args) throws Exception { + + Distrust distrust = new Distrust(args); + + X509TrustManager[] tms = new X509TrustManager[]{ + distrust.getTMF("PKIX", null), + distrust.getTMF("SunX509", null) + }; + + Date notBefore = distrust.getNotBefore(DISTRUST_DATE); + distrust.testCertificateChain(CERT_PATH, notBefore, tms, ROOT_TO_TEST); + } +} diff --git a/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/chains/chunghwa/chunghwaepkirootca-chain.pem b/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/chains/chunghwa/chunghwaepkirootca-chain.pem new file mode 100644 index 00000000000..fdaa6ee04df --- /dev/null +++ b/test/jdk/sun/security/ssl/X509TrustManagerImpl/distrust/chains/chunghwa/chunghwaepkirootca-chain.pem @@ -0,0 +1,50 @@ +Owner: CN=HiPKI Root CA - G1, + O="Chunghwa Telecom Co., Ltd.", C=TW +Issuer: OU=ePKI Root Certification Authority, + O="Chunghwa Telecom Co., Ltd.", C=TW +Serial number: 23fba648360e15e92ba78aedb67a0ae5 +Valid from: Wed Dec 20 19:11:23 MST 2023 until: Tue Dec 19 08:59:59 MST 2034 +Certificate fingerprints: + SHA1: 87:F1:DD:3B:8E:F1:E0:8C:A8:CA:CB:9B:CE:4E:26:5A:E4:4E:05:F2 + SHA256: 68:07:C9:72:35:C5:EC:60:90:26:9A:4B:5F:ED:FA:B4:69:86:E4:2F:4D:67:D2:ED:DD:CF:6E:45:CF:0D:FA:80 +Signature algorithm name: SHA256withRSA +Subject Public Key Algorithm: 4096-bit RSA key +Version: 3 + +-----BEGIN CERTIFICATE----- +MIIGjDCCBHSgAwIBAgIQI/umSDYOFekrp4rttnoK5TANBgkqhkiG9w0BAQsFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0yMzEyMjEwMjExMjNaFw0zNDEyMTkxNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQ +S0kgUm9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +9B5/UnMyDHPkvRN0o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh +8Ge6zCFovkRTv4354twvVcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux5 +5199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEu +iAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRt +U6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd +hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXT +T3OUM3ECoWqj1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK +9p/7qxj3ccC2HTHsOyDry+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8 +b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8Pg +cSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NV +vxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaOCAVMwggFPMB8G +A1UdIwQYMBaAFB4M97Zn8uGSJglFwFU5Lnc/QkqiMB0GA1UdDgQWBBTydxf6Xqj+ +9j1x1Wi6yUYMONivsDAOBgNVHQ8BAf8EBAMCAYYwQAYDVR0fBDkwNzA1oDOgMYYv +aHR0cDovL2VjYS5oaW5ldC5uZXQvcmVwb3NpdG9yeS9DUkxfU0hBMi9DQS5jcmww +gYIGCCsGAQUFBwEBBHYwdDA7BggrBgEFBQcwAoYvaHR0cDovL2VjYS5oaW5ldC5u +ZXQvcmVwb3NpdG9yeS9DZXJ0cy9lQ0FHMS5jcnQwNQYIKwYBBQUHMAGGKWh0dHA6 +Ly9vY3NwLmVjYS5oaW5ldC5uZXQvT0NTUC9vY3NwRzFzaGEyMBIGA1UdEwEB/wQI +MAYBAf8CAQEwIgYDVR0gBBswGTAIBgZngQwBAgIwDQYLKwYBBAGBtyNkAAMwDQYJ +KoZIhvcNAQELBQADggIBACY9pps8fqk3p8Xqv/qr26I1aFA4jOEG3VWd2bqn68Y9 +InOMZozTMVh7iOnOfat7mEqn/RNhikvR5MOV3qAeg4gwgNb1OMuGltwfXWGiuGeT +vhimsV6E2hhJFAmZyXtfuoV9vSrnr1a5pCWqhVYWSCvoAQ/8Kv0tATKbIe21CYXz +NIo7O9QBSXt0BiaP9+CVQtJAYYuy2MNAcXgzgL4rownrYYAixhPmkxQE0Dt1gVbW +s2htBLJGse0z1fJDblY0Zar4t2ly+kIScx5DhRrrd8XKMK0YvID9Ythb+ao8m7Wd +Kymqr36benGL3GsvmSypLPlqZtfEqVITFhXwQiL8ruxoL+3WfNQJ09x0iV4xaP+E +bZSLLVzIiyhU49YdFHaqKyAJQvzgF2Za3DOwQWlP7OngtUx0ScEGHsoo78AM+Y0T +eLFxmr82kuyH18wZkUT9bLZlot11P2aC8VTprBGr+jEAMJjpmEjSA83ja/ttmqgh +qjj29Jnw3Lgy91XIhzBFMxMYo+hhYeBRmBFWl5+Y5oxBgPVLZpDJvg2rKa8xdqim +KgvF0DMKHntE0hhVy7JfUCnKovNQ0pf0NodLfjpqcCS2GBZ1mNcsW2MG2uBPANcn +LRXmt7N4XX11mctQTADwt8yZZ+2HDrST4kghOz+FXgftrPBdtDtM0T6WJcHWR1uS +-----END CERTIFICATE----- diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java deleted file mode 100644 index 2b075e0f13c..00000000000 --- a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java +++ /dev/null @@ -1,401 +0,0 @@ -/* - * Copyright (c) 2025, 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. - */ -package jdk.jpackage.test; - -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toSet; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.UncheckedIOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; -import java.util.spi.ToolProvider; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; - -public class ExecutorTest extends JUnitAdapter { - - private record Command(List stdout, List stderr) { - Command { - stdout.forEach(Objects::requireNonNull); - stderr.forEach(Objects::requireNonNull); - } - - List asExecutable() { - final List commandline = new ArrayList<>(); - if (TKit.isWindows()) { - commandline.addAll(List.of("cmd", "/C")); - } else { - commandline.addAll(List.of("sh", "-c")); - } - commandline.add(Stream.concat(createEchoCommands(stdout), - createEchoCommands(stderr).map(v -> v + ">&2")).collect(joining(" && "))); - return commandline; - } - - private static Stream createEchoCommands(List lines) { - return lines.stream().map(line -> { - if (TKit.isWindows()) { - return "(echo " + line + ")"; - } else { - return "echo " + line; - } - }); - } - - ToolProvider asToolProvider() { - return new ToolProvider() { - - @Override - public int run(PrintWriter out, PrintWriter err, String... args) { - stdout.forEach(out::println); - stderr.forEach(err::println); - return 0; - } - - @Override - public String name() { - return "test"; - } - }; - } - } - - private enum OutputData { - EMPTY(List.of()), - ONE_LINE(List.of("Jupiter")), - MANY(List.of("Uranus", "Saturn", "Earth")); - - OutputData(List data) { - data.forEach(Objects::requireNonNull); - this.data = data; - } - - final List data; - } - - private record CommandSpec(OutputData stdout, OutputData stderr) { - CommandSpec { - Objects.requireNonNull(stdout); - Objects.requireNonNull(stderr); - } - - Command command() { - return new Command(stdout.data.stream().map(line -> { - return "stdout." + line; - }).toList(), stderr.data.stream().map(line -> { - return "stderr." + line; - }).toList()); - } - } - - public enum OutputControl { - DUMP(Executor::dumpOutput), - SAVE_ALL(Executor::saveOutput), - SAVE_FIRST_LINE(Executor::saveFirstLineOfOutput), - DISCARD_STDOUT(Executor::discardStdout), - DISCARD_STDERR(Executor::discardStderr), - ; - - OutputControl(Consumer configureExector) { - this.configureExector = Objects.requireNonNull(configureExector); - } - - Executor applyTo(Executor exec) { - configureExector.accept(exec); - return exec; - } - - static List> variants() { - final List> variants = new ArrayList<>(); - for (final var withDump : BOOLEAN_VALUES) { - variants.addAll(Stream.of( - Set.of(), - Set.of(SAVE_ALL), - Set.of(SAVE_FIRST_LINE), - Set.of(DISCARD_STDOUT), - Set.of(DISCARD_STDERR), - Set.of(SAVE_ALL, DISCARD_STDOUT), - Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT), - Set.of(SAVE_ALL, DISCARD_STDERR), - Set.of(SAVE_FIRST_LINE, DISCARD_STDERR), - Set.of(SAVE_ALL, DISCARD_STDOUT, DISCARD_STDERR), - Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT, DISCARD_STDERR) - ).map(v -> { - if (withDump) { - return Stream.concat(Stream.of(DUMP), v.stream()).collect(toSet()); - } else { - return v; - } - }).toList()); - } - return variants.stream().map(options -> { - return options.stream().filter(o -> { - return o.configureExector != NOP; - }).collect(toSet()); - }).distinct().toList(); - } - - private final Consumer configureExector; - - static final Set SAVE = Set.of(SAVE_ALL, SAVE_FIRST_LINE); - } - - public record OutputTestSpec(boolean toolProvider, Set outputControl, CommandSpec commandSpec) { - public OutputTestSpec { - outputControl.forEach(Objects::requireNonNull); - if (outputControl.containsAll(OutputControl.SAVE)) { - throw new IllegalArgumentException(); - } - Objects.requireNonNull(commandSpec); - } - - @Override - public String toString() { - final List tokens = new ArrayList<>(); - - if (toolProvider) { - tokens.add("tool-provider"); - } - - tokens.add("output=" + format(outputControl)); - tokens.add("command=" + commandSpec); - - return String.join(",", tokens.toArray(String[]::new)); - } - - void test() { - final var command = commandSpec.command(); - final var commandWithDiscardedStreams = discardStreams(command); - - final Executor.Result[] result = new Executor.Result[1]; - final var outputCapture = OutputCapture.captureOutput(() -> { - result[0] = createExecutor(command).executeWithoutExitCodeCheck(); - }); - - assertEquals(0, result[0].getExitCode()); - - // If we dump the subprocesses's output, and the command produced both STDOUT and STDERR, - // then the captured STDOUT may contain interleaved command's STDOUT and STDERR, - // not in sequential order (STDOUT followed by STDERR). - // In this case don't check the contents of the captured command's STDOUT. - if (toolProvider || outputCapture.outLines().isEmpty() || (command.stdout().isEmpty() || command.stderr().isEmpty())) { - assertEquals(expectedCapturedSystemOut(commandWithDiscardedStreams), outputCapture.outLines()); - } - assertEquals(expectedCapturedSystemErr(commandWithDiscardedStreams), outputCapture.errLines()); - - assertEquals(expectedResultStdout(commandWithDiscardedStreams), result[0].stdout().getOutput()); - assertEquals(expectedResultStderr(commandWithDiscardedStreams), result[0].stderr().getOutput()); - - if (!saveOutput()) { - assertNull(result[0].getOutput()); - } else { - assertNotNull(result[0].getOutput()); - final var allExpectedOutput = expectedCommandOutput(command); - assertEquals(allExpectedOutput.isEmpty(), result[0].getOutput().isEmpty()); - if (!allExpectedOutput.isEmpty()) { - if (outputControl.contains(OutputControl.SAVE_ALL)) { - assertEquals(allExpectedOutput, result[0].getOutput()); - } else if (outputControl.contains(OutputControl.SAVE_FIRST_LINE)) { - assertEquals(1, result[0].getOutput().size()); - assertEquals(allExpectedOutput.getFirst(), result[0].getFirstLineOfOutput()); - } else { - throw new UnsupportedOperationException(); - } - } - } - } - - private boolean dumpOutput() { - return outputControl.contains(OutputControl.DUMP); - } - - private boolean saveOutput() { - return !Collections.disjoint(outputControl, OutputControl.SAVE); - } - - private boolean discardStdout() { - return outputControl.contains(OutputControl.DISCARD_STDOUT); - } - - private boolean discardStderr() { - return outputControl.contains(OutputControl.DISCARD_STDERR); - } - - private static String format(Set outputControl) { - return outputControl.stream().map(OutputControl::name).sorted().collect(joining("+")); - } - - private List expectedCapturedSystemOut(Command command) { - if (!dumpOutput() || (!toolProvider && !saveOutput())) { - return List.of(); - } else if(saveOutput()) { - return Stream.concat(command.stdout().stream(), command.stderr().stream()).toList(); - } else { - return command.stdout(); - } - } - - private List expectedCapturedSystemErr(Command command) { - if (!dumpOutput() || (!toolProvider && !saveOutput())) { - return List.of(); - } else if(saveOutput()) { - return List.of(); - } else { - return command.stderr(); - } - } - - private List expectedResultStdout(Command command) { - return expectedResultStream(command.stdout()); - } - - private List expectedResultStderr(Command command) { - if (outputControl.contains(OutputControl.SAVE_FIRST_LINE) && !command.stdout().isEmpty()) { - return List.of(); - } - return expectedResultStream(command.stderr()); - } - - private List expectedResultStream(List commandOutput) { - Objects.requireNonNull(commandOutput); - if (outputControl.contains(OutputControl.SAVE_ALL)) { - return commandOutput; - } else if (outputControl.contains(OutputControl.SAVE_FIRST_LINE)) { - return commandOutput.stream().findFirst().map(List::of).orElseGet(List::of); - } else { - return null; - } - } - - private Command discardStreams(Command command) { - return new Command(discardStdout() ? List.of() : command.stdout(), discardStderr() ? List.of() : command.stderr()); - } - - private record OutputCapture(byte[] out, byte[] err, Charset outCharset, Charset errCharset) { - OutputCapture { - Objects.requireNonNull(out); - Objects.requireNonNull(err); - Objects.requireNonNull(outCharset); - Objects.requireNonNull(errCharset); - } - - List outLines() { - return toLines(out, outCharset); - } - - List errLines() { - return toLines(err, errCharset); - } - - private static List toLines(byte[] buf, Charset charset) { - try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf), charset))) { - return reader.lines().filter(line -> { - return !line.contains("TRACE"); - }).toList(); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - static OutputCapture captureOutput(Runnable runnable) { - final var captureOut = new ByteArrayOutputStream(); - final var captureErr = new ByteArrayOutputStream(); - - final var out = System.out; - final var err = System.err; - try { - final var outCharset = System.out.charset(); - final var errCharset = System.err.charset(); - System.setOut(new PrintStream(captureOut, true, outCharset)); - System.setErr(new PrintStream(captureErr, true, errCharset)); - runnable.run(); - return new OutputCapture(captureOut.toByteArray(), captureErr.toByteArray(), outCharset, errCharset); - } finally { - try { - System.setOut(out); - } finally { - System.setErr(err); - } - } - } - } - - private List expectedCommandOutput(Command command) { - command = discardStreams(command); - return Stream.of(command.stdout(), command.stderr()).flatMap(List::stream).toList(); - } - - private Executor createExecutor(Command command) { - final Executor exec; - if (toolProvider) { - exec = Executor.of(command.asToolProvider()); - } else { - exec = Executor.of(command.asExecutable()); - } - - outputControl.forEach(control -> control.applyTo(exec)); - - return exec; - } - } - - @ParameterizedTest - @MethodSource - public void testSavedOutput(OutputTestSpec spec) { - spec.test(); - } - - public static List testSavedOutput() { - List testCases = new ArrayList<>(); - for (final var toolProvider : BOOLEAN_VALUES) { - for (final var outputControl : OutputControl.variants()) { - for (final var stdoutContent : List.of(OutputData.values())) { - for (final var stderrContent : List.of(OutputData.values())) { - final var commandSpec = new CommandSpec(stdoutContent, stderrContent); - testCases.add(new OutputTestSpec(toolProvider, outputControl, commandSpec)); - } - } - } - } - return testCases; - } - - private static final List BOOLEAN_VALUES = List.of(Boolean.TRUE, Boolean.FALSE); - private static final Consumer NOP = exec -> {}; -} diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java index 16909d0eb40..3e169b9e184 100644 --- a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -213,7 +213,7 @@ public class PackageTestTest extends JUnitAdapter { @Override public void accept(JPackageCommand cmd, Executor.Result result) { tick(); - jpackageExitCode = result.exitCode(); + jpackageExitCode = result.getExitCode(); } @Override @@ -371,8 +371,7 @@ public class PackageTestTest extends JUnitAdapter { } catch (IOException ex) { throw new UncheckedIOException(ex); } - return new Executor.Result(actualJPackageExitCode, - this::getPrintableCommandLine).assertExitCodeIs(expectedExitCode); + return new Executor.Result(actualJPackageExitCode).assertExitCodeIs(expectedExitCode); } }; }).setExpectedExitCode(expectedJPackageExitCode) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java index 94f5b0a52b4..1c6c0ce4447 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AppImageFile.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2024, 2026, 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 @@ -28,7 +28,6 @@ import static jdk.jpackage.internal.util.function.ThrowingFunction.toFunction; import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; @@ -132,7 +131,7 @@ public record AppImageFile(String mainLauncherName, Optional mainLaunche public static AppImageFile load(Path appImageDir) { return toSupplier(() -> { Document doc = XmlUtils.initDocumentBuilder().parse( - Files.newInputStream(getPathInAppImage(appImageDir))); + getPathInAppImage(appImageDir).toFile()); XPath xPath = XPathFactory.newInstance().newXPath(); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java index ef118e525c5..5d3033e2e8c 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -22,18 +22,9 @@ */ package jdk.jpackage.test; -import static java.util.stream.Collectors.joining; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintStream; -import java.io.StringReader; import java.io.UncheckedIOException; -import java.io.Writer; +import java.nio.charset.Charset; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; @@ -43,15 +34,17 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import java.util.regex.Pattern; import java.util.spi.ToolProvider; -import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; -import jdk.jpackage.internal.util.function.ThrowingSupplier; +import jdk.jpackage.internal.util.CommandLineFormat; +import jdk.jpackage.internal.util.CommandOutputControl; +import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedExitCodeException; +import jdk.jpackage.internal.util.RetryExecutor; import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.internal.util.function.ThrowingSupplier; public final class Executor extends CommandArguments { @@ -69,8 +62,6 @@ public final class Executor extends CommandArguments { } public Executor() { - outputStreamsControl = new OutputStreamsControl(); - winEnglishOutput = false; } public Executor setExecutable(String v) { @@ -136,62 +127,31 @@ public final class Executor extends CommandArguments { return this; } - /** - * Configures this instance to save all stdout and stderr streams from the to be - * executed command. - *

    - * This function is mutually exclusive with {@link #saveFirstLineOfOutput()}. - * - * @return this - */ public Executor saveOutput() { return saveOutput(true); } - /** - * Configures if all stdout and stderr streams from the to be executed command - * should be saved. - *

    - * If v is true, the function call is equivalent to - * {@link #saveOutput()} call. If v is false, command - * output will not be saved. - * - * @parameter v if both stdout and stderr streams should be saved - * - * @return this - */ public Executor saveOutput(boolean v) { - return setOutputControl(v, OutputControlOption.SAVE_ALL); + commandOutputControl.saveOutput(v); + return this; } - /** - * Configures this instance to save the first line of a stream merged from - * stdout and stderr streams from the to be executed command. - *

    - * This function is mutually exclusive with {@link #saveOutput()}. - * - * @return this - */ public Executor saveFirstLineOfOutput() { - return setOutputControl(true, OutputControlOption.SAVE_FIRST_LINE); + commandOutputControl.saveFirstLineOfOutput(); + return this; } - /** - * Configures this instance to dump both stdout and stderr streams from the to - * be executed command into {@link System.out}. - * - * @return this - */ public Executor dumpOutput() { return dumpOutput(true); } public Executor dumpOutput(boolean v) { - return setOutputControl(v, OutputControlOption.DUMP); + commandOutputControl.dumpOutput(v); + return this; } public Executor discardStdout(boolean v) { - outputStreamsControl.stdout().discard(v); + commandOutputControl.discardStdout(v); return this; } @@ -200,7 +160,7 @@ public final class Executor extends CommandArguments { } public Executor discardStderr(boolean v) { - outputStreamsControl.stderr().discard(v); + commandOutputControl.discardStderr(v); return this; } @@ -208,45 +168,126 @@ public final class Executor extends CommandArguments { return discardStderr(true); } - public interface Output { - public List getOutput(); - - public default String getFirstLineOfOutput() { - return findFirstLineOfOutput().orElseThrow(); - } - - public default Optional findFirstLineOfOutput() { - return getOutput().stream().findFirst(); - } + public Executor binaryOutput(boolean v) { + commandOutputControl.binaryOutput(v); + return this; } - public record Result(int exitCode, CommandOutput output, Supplier cmdline) implements Output { + public Executor binaryOutput() { + return binaryOutput(true); + } + + public Executor charset(Charset v) { + commandOutputControl.charset(v); + return this; + } + + public Charset charset() { + return commandOutputControl.charset(); + } + + Executor storeOutputInFiles(boolean v) { + commandOutputControl.storeOutputInFiles(v); + return this; + } + + Executor storeOutputInFiles() { + return storeOutputInFiles(true); + } + + public record Result(CommandOutputControl.Result base) { public Result { - Objects.requireNonNull(output); - Objects.requireNonNull(cmdline); + Objects.requireNonNull(base); } - public Result(int exitCode, Supplier cmdline) { - this(exitCode, CommandOutput.EMPTY, cmdline); + public Result(int exitCode) { + this(new CommandOutputControl.Result(exitCode)); } - @Override public List getOutput() { - return output.lines().orElse(null); + return base.content(); } - public Output stdout() { - return createView(output.stdoutLines()); + public String getFirstLineOfOutput() { + return getOutput().getFirst(); } - public Output stderr() { - return createView(output.stderrLines()); + public List stdout() { + return base.stdout(); } - public Result assertExitCodeIs(int expectedExitCode) { - TKit.assertEquals(expectedExitCode, exitCode, String.format( - "Check command %s exited with %d code", - cmdline.get(), expectedExitCode)); + public List stderr() { + return base.stderr(); + } + + public Optional> findContent() { + return base.findContent(); + } + + public Optional> findStdout() { + return base.findStdout(); + } + + public Optional> findStderr() { + return base.findStderr(); + } + + public byte[] byteContent() { + return base.byteContent(); + } + + public byte[] byteStdout() { + return base.byteStdout(); + } + + public byte[] byteStderr() { + return base.byteStderr(); + } + + public Optional findByteContent() { + return base.findByteContent(); + } + + public Optional findByteStdout() { + return base.findByteStdout(); + } + + public Optional findByteStderr() { + return base.findByteStderr(); + } + + public Result toCharacterResult(Charset charset, boolean keepByteContent) { + try { + return new Result(base.toCharacterResult(charset, keepByteContent)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + public Result assertExitCodeIs(int main, int... other) { + if (other.length != 0) { + return assertExitCodeIs(IntStream.concat(IntStream.of(main), IntStream.of(other)).boxed().toList()); + } else { + return assertExitCodeIs(List.of(main)); + } + } + + private Result assertExitCodeIs(List expectedExitCodes) { + Objects.requireNonNull(expectedExitCodes); + switch (expectedExitCodes.size()) { + case 0 -> { + throw new IllegalArgumentException(); + } case 1 -> { + long expectedExitCode = expectedExitCodes.getFirst(); + TKit.assertEquals(expectedExitCode, getExitCode(), String.format( + "Check command %s exited with %d code", + base.execAttrs(), expectedExitCode)); + } default -> { + TKit.assertTrue(expectedExitCodes.contains(getExitCode()), String.format( + "Check command %s exited with one of %s codes", + base.execAttrs(), expectedExitCodes.stream().sorted().toList())); + } + } return this; } @@ -255,16 +296,11 @@ public final class Executor extends CommandArguments { } public int getExitCode() { - return exitCode; + return base.getExitCode(); } - private static Output createView(Optional> lines) { - return new Output() { - @Override - public List getOutput() { - return lines.orElse(null); - } - }; + public String getPrintableCommandLine() { + return base.execAttrs().toString(); } } @@ -292,8 +328,8 @@ public final class Executor extends CommandArguments { }).get(); } - public Result execute(int expectedCode) { - return executeWithoutExitCodeCheck().assertExitCodeIs(expectedCode); + Result execute(int mainExitCode, int... otherExitCodes) { + return executeWithoutExitCodeCheck().assertExitCodeIs(mainExitCode, otherExitCodes); } public Result execute() { @@ -301,28 +337,36 @@ public final class Executor extends CommandArguments { } public String executeAndGetFirstLineOfOutput() { - return saveFirstLineOfOutput().execute().getFirstLineOfOutput(); + return saveFirstLineOfOutput().execute().getOutput().getFirst(); } public List executeAndGetOutput() { return saveOutput().execute().getOutput(); } - private static class BadResultException extends RuntimeException { - BadResultException(Result v) { - value = v; + private static class FailedAttemptException extends Exception { + FailedAttemptException(Exception cause) { + super(Objects.requireNonNull(cause)); } - Result getValue() { - return value; - } - - private final transient Result value; private static final long serialVersionUID = 1L; } + public RetryExecutor retryUntilExitCodeIs( + int mainExpectedExitCode, int... otherExpectedExitCodes) { + return new RetryExecutor(UnexpectedExitCodeException.class).setExecutable(() -> { + var result = executeWithoutExitCodeCheck(); + result.base().expectExitCode(mainExpectedExitCode, otherExpectedExitCodes); + return result; + }).setExceptionMapper((UnexpectedExitCodeException ex) -> { + createResult(ex.getResult()).assertExitCodeIs(mainExpectedExitCode, otherExpectedExitCodes); + // Unreachable, because the above `Result.assertExitCodeIs(...)` must throw. + throw ExceptionBox.reachedUnreachable(); + }); + } + /** - * Executes the configured command {@code max} at most times and waits for + * Executes the configured command at most {@code max} times and waits for * {@code wait} seconds between each execution until the command exits with * {@code expectedCode} exit code. * @@ -332,17 +376,10 @@ public final class Executor extends CommandArguments { * command */ public Result executeAndRepeatUntilExitCode(int expectedExitCode, int max, int wait) { - try { - return tryRunMultipleTimes(() -> { - Result result = executeWithoutExitCodeCheck(); - if (result.getExitCode() != expectedExitCode) { - throw new BadResultException(result); - } - return result; - }, max, wait).assertExitCodeIs(expectedExitCode); - } catch (BadResultException ex) { - return ex.getValue().assertExitCodeIs(expectedExitCode); - } + return retryUntilExitCodeIs(expectedExitCode) + .setAttemptTimeout(wait, TimeUnit.SECONDS) + .setMaxAttemptsCount(max) + .executeUnchecked(); } /** @@ -359,26 +396,16 @@ public final class Executor extends CommandArguments { * @param wait number of seconds to wait between executions of the */ public static T tryRunMultipleTimes(Supplier task, int max, int wait) { - RuntimeException lastException = null; - int count = 0; - - do { + return new RetryExecutor(FailedAttemptException.class).setExecutable(() -> { try { return task.get(); } catch (RuntimeException ex) { - lastException = ex; + throw new FailedAttemptException(ex); } + }).setExceptionMapper((FailedAttemptException ex) -> { + return (RuntimeException)ex.getCause(); + }).setAttemptTimeout(wait, TimeUnit.SECONDS).setMaxAttemptsCount(max).executeUnchecked(); - try { - Thread.sleep(wait * 1000); - } catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - - count++; - } while (count < max); - - throw lastException; } public static void tryRunMultipleTimes(Runnable task, int max, int wait) { @@ -392,12 +419,6 @@ public final class Executor extends CommandArguments { return saveOutput().executeWithoutExitCodeCheck().getOutput(); } - private Executor setOutputControl(boolean set, OutputControlOption v) { - outputStreamsControl.stdout().set(set, v); - outputStreamsControl.stderr().set(set, v); - return this; - } - private Path executablePath() { if (directory == null || executable.isAbsolute() @@ -431,12 +452,8 @@ public final class Executor extends CommandArguments { builder.environment().put("TMP", winTmpDir); } - outputStreamsControl.applyTo(builder); - StringBuilder sb = new StringBuilder(getPrintableCommandLine()); - outputStreamsControl.describe().ifPresent(desc -> { - sb.append("; ").append(desc); - }); + sb.append("; ").append(commandOutputControl.description()); if (directory != null) { builder.directory(directory.toFile()); @@ -466,141 +483,31 @@ public final class Executor extends CommandArguments { }); } - trace("Execute " + sb.toString() + "..."); - Process process = builder.start(); - - var stdoutGobbler = CompletableFuture.>>supplyAsync(() -> { - try { - return processProcessStream(outputStreamsControl.stdout(), process.getInputStream()); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }); - - var stderrGobbler = CompletableFuture.>>supplyAsync(() -> { - try { - return processProcessStream(outputStreamsControl.stderr(), process.getErrorStream()); - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }); - - final CommandOutput output; - - try { - output = combine(stdoutGobbler.join(), stderrGobbler.join()); - } catch (CompletionException ex) { - var cause = ex.getCause(); - switch (cause) { - case UncheckedIOException uioex -> { - throw uioex.getCause(); - } - default -> { - throw ExceptionBox.toUnchecked(ExceptionBox.unbox(cause)); - } - } - } - - final int exitCode = process.waitFor(); - trace("Done. Exit code: " + exitCode); - - return createResult(exitCode, output); + return execute(sb, commandOutputControl.createExecutable(builder)); } - private int runToolProvider(PrintStream out, PrintStream err) { + private Result runToolProvider() throws IOException, InterruptedException { final var sb = new StringBuilder(getPrintableCommandLine()); - outputStreamsControl.describe().ifPresent(desc -> { - sb.append("; ").append(desc); - }); - trace("Execute " + sb + "..."); - final int exitCode = toolProvider.run(out, err, args.toArray( - String[]::new)); - trace("Done. Exit code: " + exitCode); - return exitCode; + sb.append("; ").append(commandOutputControl.description()); + + return execute(sb, commandOutputControl.createExecutable(toolProvider, args.toArray(String[]::new))); } - private Result runToolProvider() throws IOException { - final var toolProviderStreamConfig = ToolProviderStreamConfig.create(outputStreamsControl); + private Result execute(StringBuilder traceMsg, CommandOutputControl.Executable exec) throws IOException, InterruptedException { + Objects.requireNonNull(traceMsg); - final var exitCode = runToolProvider(toolProviderStreamConfig); + trace("Execute " + traceMsg + "..."); - final var output = combine( - read(outputStreamsControl.stdout(), toolProviderStreamConfig.out()), - read(outputStreamsControl.stderr(), toolProviderStreamConfig.err())); - return createResult(exitCode, output); + var result = exec.execute(); + + trace("Done. Exit code: " + result.getExitCode()); + + return createResult(result); } - private int runToolProvider(ToolProviderStreamConfig cfg) throws IOException { - try { - return runToolProvider(cfg.out().ps(), cfg.err().ps()); - } finally { - cfg.out().ps().flush(); - cfg.err().ps().flush(); - } - } - - private static Optional> processProcessStream(OutputControl outputControl, InputStream in) throws IOException { - List outputLines = null; - try (final var bufReader = new BufferedReader(new InputStreamReader(in))) { - if (outputControl.dump() || outputControl.saveAll()) { - outputLines = bufReader.lines().toList(); - } else if (outputControl.saveFirstLine()) { - outputLines = Optional.ofNullable(bufReader.readLine()).map(List::of).orElseGet(List::of); - // Read all input, or the started process may exit with an error (cmd.exe does so). - bufReader.transferTo(Writer.nullWriter()); - } else { - // This should be empty input stream, fetch it anyway. - bufReader.transferTo(Writer.nullWriter()); - } - } finally { - if (outputControl.dump() && outputLines != null) { - outputLines.forEach(System.out::println); - if (outputControl.saveFirstLine()) { - outputLines = outputLines.stream().findFirst().map(List::of).orElseGet(List::of); - } - } - if (!outputControl.save()) { - outputLines = null; - } - } - return Optional.ofNullable(outputLines); - } - - private static Optional> read(OutputControl outputControl, CachingPrintStream cps) throws IOException { - final var bufferAsString = cps.bufferContents(); - try (final var bufReader = new BufferedReader(new StringReader(bufferAsString.orElse("")))) { - if (outputControl.saveFirstLine()) { - return Optional.of(bufReader.lines().findFirst().map(List::of).orElseGet(List::of)); - } else if (outputControl.saveAll()) { - return Optional.of(bufReader.lines().toList()); - } else if (bufferAsString.isPresent()) { - return Optional.of(List.of()); - } else { - return Optional.empty(); - } - } - } - - private CommandOutput combine(Optional> out, Optional> err) { - if (out.isEmpty() && err.isEmpty()) { - return new CommandOutput(); - } else if (out.isEmpty()) { - return new CommandOutput(err, -1); - } else if (err.isEmpty()) { - return new CommandOutput(out, Integer.MAX_VALUE); - } else { - final var combined = Stream.of(out, err).map(Optional::orElseThrow).flatMap(List::stream); - if (outputStreamsControl.stdout().saveFirstLine() && outputStreamsControl.stderr().saveFirstLine()) { - return new CommandOutput(Optional.of(combined.findFirst().map(List::of).orElseGet(List::of)), - Integer.min(1, out.orElseThrow().size())); - } else { - return new CommandOutput(Optional.of(combined.toList()), out.orElseThrow().size()); - } - } - } - - private Result createResult(int exitCode, CommandOutput output) { - return new Result(exitCode, output, this::getPrintableCommandLine); + private Result createResult(CommandOutputControl.Result baseResult) { + return new Result(baseResult.copyWithExecutableAttributes( + new ExecutableAttributes(baseResult.execAttrs(), getPrintableCommandLine()))); } public String getPrintableCommandLine() { @@ -618,359 +525,40 @@ public final class Executor extends CommandArguments { var cmdline = Stream.of(prefixCommandLineArgs(), List.of(exec), args).flatMap( List::stream).toList(); - return String.format(format, printCommandLine(cmdline), cmdline.size()); + return String.format(format, CommandLineFormat.DEFAULT.apply(cmdline), cmdline.size()); } - private static String printCommandLine(List cmdline) { - // Want command line printed in a way it can be easily copy/pasted - // to be executed manually - Pattern regex = Pattern.compile("\\s"); - return cmdline.stream().map( - v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect( - Collectors.joining(" ")); + private record ExecutableAttributes(CommandOutputControl.ExecutableAttributes base, String toStringValue) + implements CommandOutputControl.ExecutableAttributes { + + ExecutableAttributes { + Objects.requireNonNull(base); + if (toStringValue.isBlank()) { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() { + return toStringValue; + } + + @Override + public List commandLine() { + return base.commandLine(); + } } private static void trace(String msg) { TKit.trace(String.format("exec: %s", msg)); } - private static PrintStream nullPrintStream() { - return new PrintStream(OutputStream.nullOutputStream()); - } - - private record OutputStreamsControl(OutputControl stdout, OutputControl stderr) { - OutputStreamsControl { - Objects.requireNonNull(stdout); - Objects.requireNonNull(stderr); - } - - OutputStreamsControl() { - this(new OutputControl(), new OutputControl()); - } - - void applyTo(ProcessBuilder pb) { - pb.redirectOutput(stdout.asProcessBuilderRedirect()); - pb.redirectError(stderr.asProcessBuilderRedirect()); - } - - Optional describe() { - final List tokens = new ArrayList<>(); - if (stdout.save() || stderr.save()) { - streamsLabel("save ", true).ifPresent(tokens::add); - } - if (stdout.dump() || stderr.dump()) { - streamsLabel("inherit ", true).ifPresent(tokens::add); - } - streamsLabel("discard ", false).ifPresent(tokens::add); - if (tokens.isEmpty()) { - return Optional.empty(); - } else { - return Optional.of(String.join("; ", tokens)); - } - } - - Optional streamsLabel(String prefix, boolean negate) { - Objects.requireNonNull(prefix); - final var str = Stream.of(stdoutLabel(negate), stderrLabel(negate)) - .filter(Optional::isPresent) - .map(Optional::orElseThrow) - .collect(joining("+")); - if (str.isEmpty()) { - return Optional.empty(); - } else { - return Optional.of(prefix + str); - } - } - - private Optional stdoutLabel(boolean negate) { - if ((stdout.discard() && !negate) || (!stdout.discard() && negate)) { - return Optional.of("out"); - } else { - return Optional.empty(); - } - } - - private Optional stderrLabel(boolean negate) { - if ((stderr.discard() && !negate) || (!stderr.discard() && negate)) { - return Optional.of("err"); - } else { - return Optional.empty(); - } - } - } - - private record CachingPrintStream(PrintStream ps, Optional buf) { - CachingPrintStream { - Objects.requireNonNull(ps); - Objects.requireNonNull(buf); - } - - Optional bufferContents() { - return buf.map(ByteArrayOutputStream::toString); - } - - static Builder build() { - return new Builder(); - } - - static final class Builder { - - Builder save(boolean v) { - save = v; - return this; - } - - Builder discard(boolean v) { - discard = v; - return this; - } - - Builder dumpStream(PrintStream v) { - dumpStream = v; - return this; - } - - CachingPrintStream create() { - final Optional buf; - if (save && !discard) { - buf = Optional.of(new ByteArrayOutputStream()); - } else { - buf = Optional.empty(); - } - - final PrintStream ps; - if (buf.isPresent() && dumpStream != null) { - ps = new PrintStream(new TeeOutputStream(List.of(buf.orElseThrow(), dumpStream)), true, dumpStream.charset()); - } else if (!discard) { - ps = buf.map(PrintStream::new).or(() -> Optional.ofNullable(dumpStream)).orElseGet(Executor::nullPrintStream); - } else { - ps = nullPrintStream(); - } - - return new CachingPrintStream(ps, buf); - } - - private boolean save; - private boolean discard; - private PrintStream dumpStream; - } - } - - private record ToolProviderStreamConfig(CachingPrintStream out, CachingPrintStream err) { - ToolProviderStreamConfig { - Objects.requireNonNull(out); - Objects.requireNonNull(err); - } - - static ToolProviderStreamConfig create(OutputStreamsControl cfg) { - final var errCfgBuilder = cfg.stderr().buildCachingPrintStream(System.err); - if (cfg.stderr().dump() && cfg.stderr().save()) { - errCfgBuilder.dumpStream(System.out); - } - return new ToolProviderStreamConfig( - cfg.stdout().buildCachingPrintStream(System.out).create(), errCfgBuilder.create()); - } - } - - private static final class OutputControl { - - boolean save() { - return save.isPresent(); - } - - boolean saveAll() { - return save.orElse(null) == OutputControlOption.SAVE_ALL; - } - - boolean saveFirstLine() { - return save.orElse(null) == OutputControlOption.SAVE_FIRST_LINE; - } - - boolean discard() { - return discard || (!dump && save.isEmpty()); - } - - boolean dump() { - return !discard && dump; - } - - OutputControl dump(boolean v) { - this.dump = v; - return this; - } - - OutputControl discard(boolean v) { - this.discard = v; - return this; - } - - OutputControl saveAll(boolean v) { - if (v) { - save = Optional.of(OutputControlOption.SAVE_ALL); - } else { - save = Optional.empty(); - } - return this; - } - - OutputControl saveFirstLine(boolean v) { - if (v) { - save = Optional.of(OutputControlOption.SAVE_FIRST_LINE); - } else { - save = Optional.empty(); - } - return this; - } - - OutputControl set(boolean set, OutputControlOption v) { - switch (v) { - case DUMP -> dump(set); - case SAVE_ALL -> saveAll(set); - case SAVE_FIRST_LINE -> saveFirstLine(set); - } - return this; - } - - ProcessBuilder.Redirect asProcessBuilderRedirect() { - if (discard()) { - return ProcessBuilder.Redirect.DISCARD; - } else if (dump && !save()) { - return ProcessBuilder.Redirect.INHERIT; - } else { - return ProcessBuilder.Redirect.PIPE; - } - } - - CachingPrintStream.Builder buildCachingPrintStream(PrintStream dumpStream) { - Objects.requireNonNull(dumpStream); - final var builder = CachingPrintStream.build().save(save()).discard(discard()); - if (dump()) { - builder.dumpStream(dumpStream); - } - return builder; - } - - private boolean dump; - private boolean discard; - private Optional save = Optional.empty(); - } - - private static final class TeeOutputStream extends OutputStream { - - public TeeOutputStream(Iterable streams) { - streams.forEach(Objects::requireNonNull); - this.streams = streams; - } - - @Override - public void write(int b) throws IOException { - for (final var out : streams) { - out.write(b); - } - } - - @Override - public void write(byte[] b) throws IOException { - for (final var out : streams) { - out.write(b); - } - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - for (final var out : streams) { - out.write(b, off, len); - } - } - - @Override - public void flush() throws IOException { - forEach(OutputStream::flush); - } - - @Override - public void close() throws IOException { - forEach(OutputStream::close); - } - - private void forEach(OutputStreamConsumer c) throws IOException { - IOException firstEx = null; - for (final var out : streams) { - try { - c.accept(out); - } catch (IOException e) { - if (firstEx == null) { - firstEx = e; - } - } - } - if (firstEx != null) { - throw firstEx; - } - } - - @FunctionalInterface - private static interface OutputStreamConsumer { - void accept(OutputStream out) throws IOException; - } - - private final Iterable streams; - } - - private static final class CommandOutput { - CommandOutput(Optional> lines, int stdoutLineCount) { - this.lines = Objects.requireNonNull(lines); - this.stdoutLineCount = stdoutLineCount; - } - - CommandOutput() { - this(Optional.empty(), 0); - } - - Optional> lines() { - return lines; - } - - Optional> stdoutLines() { - if (lines.isEmpty() || stdoutLineCount < 0) { - return Optional.empty(); - } - - final var theLines = lines.orElseThrow(); - if (stdoutLineCount == theLines.size()) { - return lines; - } else { - return Optional.of(theLines.subList(0, Integer.min(stdoutLineCount, theLines.size()))); - } - } - - Optional> stderrLines() { - if (lines.isEmpty() || stdoutLineCount > lines.orElseThrow().size()) { - return Optional.empty(); - } else if (stdoutLineCount == 0) { - return lines; - } else { - final var theLines = lines.orElseThrow(); - return Optional.of(theLines.subList(stdoutLineCount, theLines.size())); - } - } - - private final Optional> lines; - private final int stdoutLineCount; - - static final CommandOutput EMPTY = new CommandOutput(); - } - private ToolProvider toolProvider; private Path executable; - private OutputStreamsControl outputStreamsControl; + private final CommandOutputControl commandOutputControl = new CommandOutputControl(); private Path directory; private Set removeEnvVars = new HashSet<>(); private Map setEnvVars = new HashMap<>(); private boolean winEnglishOutput; private String winTmpDir = null; - - private static enum OutputControlOption { - SAVE_ALL, SAVE_FIRST_LINE, DUMP - } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 38091cb3452..c5c4f87b097 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -52,6 +52,8 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -780,10 +782,9 @@ public class JPackageCommand extends CommandArguments { } /** - * Starts a new thread. In this thread calls - * {@link #useToolProviderByDefault(ToolProvider)} with the specified - * {@code jpackageToolProvider} and then calls {@code workload.run()}. Joins the - * thread. + * In a separate thread calls {@link #useToolProviderByDefault(ToolProvider)} + * with the specified {@code jpackageToolProvider} and then calls + * {@code workload.run()}. Joins the thread. *

    * The idea is to run the {@code workload} in the context of the specified * jpackage {@code ToolProvider} without altering the global variable holding @@ -794,13 +795,23 @@ public class JPackageCommand extends CommandArguments { * @param jpackageToolProvider jpackage {@code ToolProvider} * @param workload the workload to run */ - public static void withToolProvider(ToolProvider jpackageToolProvider, Runnable workload) { - Objects.requireNonNull(jpackageToolProvider); + public static void withToolProvider(Runnable workload, ToolProvider jpackageToolProvider) { Objects.requireNonNull(workload); - ThrowingRunnable.toRunnable(Thread.ofVirtual().start(() -> { + Objects.requireNonNull(jpackageToolProvider); + + CompletableFuture.runAsync(() -> { + var oldValue = defaultToolProvider.get(); useToolProviderByDefault(jpackageToolProvider); - workload.run(); - })::join).run(); + try { + workload.run(); + } finally { + defaultToolProvider.set(oldValue); + } + // Run the future in a new native thread. Don't run it in a virtual/pooled thread. + // Pooled and/or virtual threads are problematic when used with inheritable thread-local variables. + // TKit class depends on such a variable, which results in intermittent test failures + // if the default executor runs this future. + }, Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())).join(); } public JPackageCommand useToolProvider(boolean v) { @@ -1022,7 +1033,7 @@ public class JPackageCommand extends CommandArguments { outputValidator.accept(result.getOutput().iterator()); } - if (result.exitCode() == 0 && expectedExitCode.isPresent()) { + if (result.getExitCode() == 0 && expectedExitCode.isPresent()) { verifyActions.run(); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java index 15d96311d98..f9fcfb905af 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherVerifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -39,7 +39,6 @@ import java.util.function.BiFunction; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; -import javax.xml.parsers.ParserConfigurationException; import jdk.jpackage.internal.resources.ResourceLocator; import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.function.ThrowingBiConsumer; @@ -367,7 +366,7 @@ public final class LauncherVerifier { } } - private void verifyMacEntitlements(JPackageCommand cmd) throws ParserConfigurationException, SAXException, IOException { + private void verifyMacEntitlements(JPackageCommand cmd) throws SAXException, IOException { Path launcherPath = cmd.appLauncherPath(name); var entitlements = MacSignVerify.findEntitlements(launcherPath); @@ -457,8 +456,10 @@ public final class LauncherVerifier { private static final class DefaultEntitlements { private static Map loadFromResources(String resourceName) { return ThrowingSupplier.toSupplier(() -> { - var bytes = ResourceLocator.class.getResourceAsStream(resourceName).readAllBytes(); - return new PListReader(bytes).toMap(true); + try (var in = ResourceLocator.class.getResourceAsStream(resourceName)) { + var bytes = in.readAllBytes(); + return new PListReader(bytes).toMap(true); + } }).get(); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 3b02a3f6a69..78562b2ed26 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -723,7 +723,9 @@ public final class LinuxHelper { private static Optional queryMimeTypeDefaultHandler(String mimeType) { return Executor.of("xdg-mime", "query", "default", mimeType) - .discardStderr().saveFirstLineOfOutput().execute().findFirstLineOfOutput(); + .discardStderr() + .saveFirstLineOfOutput() + .execute().getOutput().stream().findFirst(); } private static void verifyIconInScriptlet(Scriptlet scriptletType, diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java index 92d9fa0bd44..6a5be77457a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -49,11 +49,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -66,10 +66,10 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; -import jdk.jpackage.internal.RetryExecutor; import jdk.jpackage.internal.util.FileUtils; import jdk.jpackage.internal.util.PListReader; import jdk.jpackage.internal.util.PathUtils; +import jdk.jpackage.internal.util.RetryExecutor; import jdk.jpackage.internal.util.XmlUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.internal.util.function.ThrowingSupplier; @@ -90,38 +90,34 @@ public final class MacHelper { final var mountRoot = TKit.createTempDirectory("mountRoot"); // Explode DMG assuming this can require interaction, thus use `yes`. - String attachCMD[] = { - "sh", "-c", - String.join(" ", "yes", "|", "/usr/bin/hdiutil", "attach", - JPackageCommand.escapeAndJoin(cmd.outputBundle().toString()), - "-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot), - "-nobrowse", "-plist")}; - RetryExecutor attachExecutor = new RetryExecutor(); - try { - // 10 times with 6 second delays. - attachExecutor.setMaxAttemptsCount(10) - .setAttemptTimeoutMillis(6000) - .setWriteOutputToFile(true) - .saveOutput(true) - .execute(attachCMD); - } catch (IOException ex) { - throw new RuntimeException(ex); - } + final var attachStdout = Executor.of("sh", "-c", String.join(" ", + "yes", + "|", + "/usr/bin/hdiutil", + "attach", + JPackageCommand.escapeAndJoin(cmd.outputBundle().toString()), + "-mountroot", PathUtils.normalizedAbsolutePathString(mountRoot), + "-nobrowse", + "-plist" + )).saveOutput().storeOutputInFiles().executeAndRepeatUntilExitCode(0, 10, 6).stdout(); - Path mountPoint = null; + final Path mountPoint; + + boolean mountPointInitialized = false; try { // One of "dict" items of "system-entities" array property should contain "mount-point" string property. - mountPoint = readPList(attachExecutor.getOutput()).queryArrayValue("system-entities", false).map(PListReader.class::cast).map(dict -> { - try { - return dict.queryValue("mount-point"); - } catch (NoSuchElementException ex) { - return (String)null; - } - }).filter(Objects::nonNull).map(Path::of).findFirst().orElseThrow(); + mountPoint = readPList(attachStdout).queryArrayValue("system-entities", false) + .map(PListReader.class::cast) + .map(dict -> { + return dict.findValue("mount-point"); + }) + .filter(Optional::isPresent).map(Optional::get) + .map(Path::of).findFirst().orElseThrow(); + mountPointInitialized = true; } finally { - if (mountPoint == null) { + if (!mountPointInitialized) { TKit.trace("Unexpected plist file missing `system-entities` array:"); - attachExecutor.getOutput().forEach(TKit::trace); + attachStdout.forEach(TKit::trace); TKit.trace("Done"); } } @@ -138,39 +134,27 @@ public final class MacHelper { ThrowingConsumer.toConsumer(consumer).accept(childPath); } } finally { - String detachCMD[] = { - "/usr/bin/hdiutil", - "detach", - "-verbose", - mountPoint.toAbsolutePath().toString()}; // "hdiutil detach" might not work right away due to resource busy error, so // repeat detach several times. - RetryExecutor detachExecutor = new RetryExecutor(); - // Image can get detach even if we got resource busy error, so stop - // trying to detach it if it is no longer attached. - final Path mp = mountPoint; - detachExecutor.setExecutorInitializer(exec -> { - if (!Files.exists(mp)) { - detachExecutor.abort(); + new RetryExecutor(RuntimeException.class).setExecutable(context -> { + var exec = Executor.of("/usr/bin/hdiutil", "detach").storeOutputInFiles(); + if (context.isLastAttempt()) { + // The last attempt, force detach. + exec.addArgument("-force"); } - }); - try { - // 10 times with 6 second delays. - detachExecutor.setMaxAttemptsCount(10) - .setAttemptTimeoutMillis(6000) - .setWriteOutputToFile(true) - .saveOutput(true) - .execute(detachCMD); - } catch (IOException ex) { - if (!detachExecutor.isAborted()) { - // Now force to detach if it still attached - if (Files.exists(mountPoint)) { - Executor.of("/usr/bin/hdiutil", "detach", - "-force", "-verbose") - .addArgument(mountPoint).execute(); - } + exec.addArgument(mountPoint); + + // The image can get detached even if we get a resource busy error, + // so execute the detach command without checking the exit code. + var result = exec.executeWithoutExitCodeCheck(); + + if (result.getExitCode() == 0 || !Files.exists(mountPoint)) { + // Detached successfully! + return null; + } else { + throw new RuntimeException(String.format("[%s] mount point still attached", mountPoint)); } - } + }).setMaxAttemptsCount(10).setAttemptTimeout(6, TimeUnit.SECONDS).execute(); } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSign.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSign.java index 15249c51887..70de6ba92af 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSign.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSign.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -1172,7 +1172,7 @@ public final class MacSign { "-c", certFile.normalize().toString(), "-k", keychain.name(), "-p", resolvedCertificateRequest.installed().type().verifyPolicy()).saveOutput(!quite).executeWithoutExitCodeCheck(); - if (result.exitCode() == 0) { + if (result.getExitCode() == 0) { return VerifyStatus.VERIFY_OK; } } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java index 1f37829791e..0ecfd4c3432 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/MacSignVerify.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -94,7 +94,7 @@ public final class MacSignVerify { public static Optional findEntitlements(Path path) { final var exec = Executor.of("/usr/bin/codesign", "-d", "--entitlements", "-", "--xml", path.toString()).saveOutput().dumpOutput(); final var result = exec.execute(); - var xml = result.stdout().getOutput(); + var xml = result.stdout(); if (xml.isEmpty()) { return Optional.empty(); } else { @@ -137,7 +137,7 @@ public final class MacSignVerify { public static Optional findSpctlSignOrigin(SpctlType type, Path path) { final var exec = Executor.of("/usr/sbin/spctl", "-vv", "--raw", "--assess", "--type", type.value(), path.toString()).saveOutput().discardStderr(); final var result = exec.executeWithoutExitCodeCheck(); - TKit.assertTrue(Set.of(0, 3).contains(result.exitCode()), + TKit.assertTrue(Set.of(0, 3).contains(result.getExitCode()), String.format("Check exit code of command %s is either 0 or 3", exec.getPrintableCommandLine())); return toSupplier(() -> { try { @@ -173,7 +173,7 @@ public final class MacSignVerify { } else if (result.getExitCode() == 1 && result.getFirstLineOfOutput().endsWith("code object is not signed at all")) { return Optional.empty(); } else { - reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result); + reportUnexpectedCommandOutcome(result); return null; // Unreachable } } @@ -205,7 +205,7 @@ public final class MacSignVerify { TKit.trace("Try /usr/bin/codesign again with `sudo`"); assertSigned(path, true); } else { - reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result); + reportUnexpectedCommandOutcome(result); } } @@ -264,13 +264,13 @@ public final class MacSignVerify { return signIdentities; } catch (Exception ex) { ex.printStackTrace(); - reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result); + reportUnexpectedCommandOutcome(result); return null; // Unreachable } } else if (result.getExitCode() == 1 && result.getOutput().getLast().endsWith("Status: no signature")) { return List.of(); } else { - reportUnexpectedCommandOutcome(exec.getPrintableCommandLine(), result); + reportUnexpectedCommandOutcome(result); return null; // Unreachable } } @@ -282,14 +282,13 @@ public final class MacSignVerify { } } - private static void reportUnexpectedCommandOutcome(String printableCommandLine, Executor.Result result) { - Objects.requireNonNull(printableCommandLine); + private static void reportUnexpectedCommandOutcome(Executor.Result result) { Objects.requireNonNull(result); TKit.trace(String.format("Command %s exited with exit code %d and the following output:", - printableCommandLine, result.getExitCode())); + result.getPrintableCommandLine(), result.getExitCode())); result.getOutput().forEach(TKit::trace); TKit.trace("Done"); - TKit.assertUnexpected(String.format("Outcome of command %s", printableCommandLine)); + TKit.assertUnexpected(String.format("Outcome of command %s", result.getPrintableCommandLine())); } private static final Pattern SIGN_IDENTITY_NAME_REGEXP = Pattern.compile("^\\s+\\d+\\.\\s+(.*)$"); diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java index 72b5dbc578b..f9fbf285b49 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/WindowsHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2026, 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 @@ -81,16 +81,16 @@ public class WindowsHelper { msiLog.ifPresent(v -> misexec.clearArguments().addArguments(origArgs).addArgument("/L*v").addArgument(v)); result = misexec.executeWithoutExitCodeCheck(); - if (result.exitCode() == 1605) { + if (result.getExitCode() == 1605) { // ERROR_UNKNOWN_PRODUCT, attempt to uninstall not installed // package - return result.exitCode(); + return result.getExitCode(); } // The given Executor may either be of an msiexec command or an // unpack.bat script containing the msiexec command. In the later // case, when misexec returns 1618, the unpack.bat may return 1603 - if ((result.exitCode() == 1618) || (result.exitCode() == 1603 && isUnpack)) { + if ((result.getExitCode() == 1618) || (result.getExitCode() == 1603 && isUnpack)) { // Another installation is already in progress. // Wait a little and try again. Long timeout = 1000L * (attempt + 3); // from 3 to 10 seconds @@ -100,7 +100,7 @@ public class WindowsHelper { break; } - return result.exitCode(); + return result.getExitCode(); } static PackageHandlers createMsiPackageHandlers(boolean createMsiLog) { @@ -462,7 +462,7 @@ public class WindowsHelper { var status = Executor.of("reg", "query", keyPath, "/v", valueName) .saveOutput() .executeWithoutExitCodeCheck(); - if (status.exitCode() == 1) { + if (status.getExitCode() == 1) { // Should be the case of no such registry value or key String lookupString = "ERROR: The system was unable to find the specified registry key or value."; TKit.assertTextStream(lookupString) diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandAction.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandAction.java new file mode 100644 index 00000000000..d9ab38e006a --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandAction.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.io.PrintStream; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * An action. + */ +@FunctionalInterface +public interface CommandAction { + + public record Context(PrintStream out, PrintStream err, List args) { + + public Context { + Objects.requireNonNull(out); + Objects.requireNonNull(err); + args.forEach(Objects::requireNonNull); + } + + public Optional findOptionValue(String option) { + Objects.requireNonNull(option); + var idx = args.indexOf(option); + if (idx >= 0 && idx + 1 < args.size()) { + return Optional.of(args.get(idx + 1)); + } else { + return Optional.empty(); + } + } + + public String optionValue(String option) { + return findOptionValue(option).orElseThrow(() -> { + throw new MockIllegalStateException(String.format("No option %s", option)); + }); + } + + public MockIllegalStateException unexpectedArguments() { + return new MockIllegalStateException(String.format("Unexpected arguments: %s", args)); + } + } + + /** + * Runs the action in the given context. + * + * @param context the context + * @return an {@code Optional} wrapping the exit code, indicating it is the last + * action in the sequence or an empty {@code Optional} otherwise + * @throws Exception simulates a failure + * @throws MockIllegalStateException if error in internal mock logic occurred. + * E.g.: if the action was called unexpectedly + */ + Optional run(Context context) throws Exception, MockIllegalStateException; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpec.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpec.java new file mode 100644 index 00000000000..25814d84205 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpec.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.util.function.ThrowingSupplier; +import jdk.jpackage.internal.util.function.ThrowingConsumer; +import jdk.jpackage.internal.util.function.ThrowingRunnable; + +/** + * Specification of a {@link CommandAction}. + *

    + * Comprised of a human-readable description and an associated action. + */ +public interface CommandActionSpec { + + String description(); + CommandAction action(); + + public static CommandActionSpec create(String description, CommandAction action) { + return new Internal.DefaultCommandActionSpec(description, action); + } + + public static CommandActionSpec create(String description, ThrowingSupplier action) { + Objects.requireNonNull(action); + return create(description, _ -> { + return Optional.of(action.get()); + }); + } + + public static CommandActionSpec create(String description, ThrowingRunnable action) { + Objects.requireNonNull(action); + return create(description, _ -> { + action.run(); + return Optional.empty(); + }); + } + + @SuppressWarnings("overloads") + public static CommandActionSpec create(String description, ThrowingConsumer action) { + Objects.requireNonNull(action); + return create(description, context -> { + action.accept(context); + return Optional.empty(); + }); + } + + final class Internal { + + private Internal() { + } + + private record DefaultCommandActionSpec(String description, CommandAction action) implements CommandActionSpec { + DefaultCommandActionSpec { + Objects.requireNonNull(description); + Objects.requireNonNull(action); + } + + @Override + public String toString() { + return description(); + } + } + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpecs.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpecs.java new file mode 100644 index 00000000000..e89e458c02d --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandActionSpecs.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.function.ExceptionBox; + +/** + * A sequence of actions. + */ +public record CommandActionSpecs(List specs) { + + public CommandActionSpecs { + Objects.requireNonNull(specs); + } + + public CommandActionSpecs andThen(CommandActionSpecs other) { + return build().append(this).append(other).create(); + } + + public Stream actions() { + return specs.stream().map(CommandActionSpec::action); + } + + public CommandMock.Builder toCommandMockBuilder() { + return new CommandMock.Builder().mutate(builder -> { + builder.actions().append(this); + }); + } + + @Override + public String toString() { + return specs.toString(); + } + + public static Builder build() { + return new Builder(); + } + + public static final class Builder { + + public CommandActionSpecs create() { + return new CommandActionSpecs(List.copyOf(specs)); + } + + public Builder stdout(List content) { + Objects.requireNonNull(content); + return action(CommandActionSpec.create(String.format("%s>>1", content), context -> { + var out = context.out(); + content.forEach(out::println); + })); + } + + public Builder stdout(String... str) { + return stdout(List.of(str)); + } + + public Builder stderr(List content) { + Objects.requireNonNull(content); + return action(CommandActionSpec.create(String.format("%s>>2", content), context -> { + var err = context.err(); + content.forEach(err::println); + })); + } + + public Builder stderr(String... str) { + return stderr(List.of(str)); + } + + public Builder printToStdout(List content) { + Objects.requireNonNull(content); + return action(CommandActionSpec.create(String.format("%s(no-eol)>>1", content), context -> { + var out = context.out(); + content.forEach(out::print); + })); + } + + public Builder printToStdout(String... str) { + return printToStdout(List.of(str)); + } + + public Builder printToStderr(List content) { + Objects.requireNonNull(content); + return action(CommandActionSpec.create(String.format("%s(no-eol)>>2", content), context -> { + var err = context.err(); + content.forEach(err::print); + })); + } + + public Builder printToStderr(String... str) { + return printToStderr(List.of(str)); + } + + public Builder exit(int exitCode) { + return action(CommandActionSpec.create(String.format("exit(%d)", exitCode), () -> { + return exitCode; + })); + } + + public Builder exit() { + return exit(0); + } + + public Builder exit(CommandMockExit exit) { + switch (exit) { + case SUCCEED -> { + return exit(); + } + case EXIT_1 -> { + return exit(1); + } + case THROW_MOCK_IO_EXCEPTION -> { + return action(CommandActionSpec.create("", () -> { + throw new MockingToolProvider.RethrowableException(new MockIOException("Kaput!")); + })); + } + default -> { + throw ExceptionBox.reachedUnreachable(); + } + } + } + + public Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + + public Builder append(Builder other) { + return append(other.specs); + } + + public Builder append(CommandActionSpecs other) { + return append(other.specs()); + } + + public Builder append(List other) { + specs.addAll(other); + return this; + } + + public Builder action(CommandActionSpec v) { + specs.add(Objects.requireNonNull(v)); + return this; + } + + public Builder copy() { + return new Builder().append(this); + } + + public CommandMock.Builder toCommandMockBuilder() { + return new CommandMock.Builder().mutate(builder -> { + builder.actions(this); + }); + } + + private final List specs = new ArrayList<>(); + } + + public static final CommandActionSpecs UNREACHABLE = new CommandActionSpecs(List.of()); +} + diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMock.java new file mode 100644 index 00000000000..bb3980fd4e0 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMock.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.stream.IntStream; + +/** + * Command mock. + */ +public sealed interface CommandMock permits ToolProviderCommandMock, VerbatimCommandMock, CompletableCommandMock { + + public static CommandMock ioerror(String name) { + return CommandActionSpecs.build() + .exit(CommandMockExit.THROW_MOCK_IO_EXCEPTION) + .toCommandMockBuilder().name(Objects.requireNonNull(name)).create(); + } + + public static CommandMock fail(String name) { + return CommandActionSpecs.build() + .exit(CommandMockExit.EXIT_1) + .toCommandMockBuilder().name(Objects.requireNonNull(name)).create(); + } + + public static CommandMock succeed(String name) { + return CommandActionSpecs.build() + .exit(CommandMockExit.SUCCEED) + .toCommandMockBuilder().name(Objects.requireNonNull(name)).create(); + } + + public static CommandMock unreachable() { + return MockingToolProvider.UNREACHABLE; + } + + public final class Builder { + + public ToolProviderCommandMock create() { + + var actionSpecs = Optional.ofNullable(scriptBuilder) + .map(CommandActionSpecs.Builder::create) + .orElse(CommandActionSpecs.UNREACHABLE); + if (actionSpecs.equals(CommandActionSpecs.UNREACHABLE)) { + return (ToolProviderCommandMock)unreachable(); + } + + var theName = Optional.ofNullable(name).orElse("mock"); + var script = actionSpecs.actions().toList(); + switch (repeat) { + case 0 -> { + return MockingToolProvider.create(theName, script); + } + case -1 -> { + return MockingToolProvider.createLoop(theName, script); + } + default -> { + var repeatedScript = IntStream.rangeClosed(0, repeat) + .mapToObj(i -> script) + .flatMap(List::stream) + .toList(); + return MockingToolProvider.create(theName, repeatedScript); + } + } + } + + public Builder name(String v) { + name = v; + return this; + } + + public Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + + public Builder repeat(int v) { + repeat = Integer.max(-1, v); + return this; + } + + public Builder noRepeats() { + return repeat(0); + } + + public Builder repeatInfinitely() { + return repeat(-1); + } + + public Builder actions(CommandActionSpecs.Builder v) { + scriptBuilder = Optional.ofNullable(v).orElseGet(CommandActionSpecs::build); + return this; + } + + public CommandActionSpecs.Builder actions() { + if (scriptBuilder == null) { + scriptBuilder = CommandActionSpecs.build(); + } + return scriptBuilder; + } + + private String name; + private int repeat = -1; + private CommandActionSpecs.Builder scriptBuilder; + } + +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockExit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockExit.java new file mode 100644 index 00000000000..26bc8ba757b --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockExit.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import jdk.jpackage.internal.util.CommandOutputControl; + +public enum CommandMockExit { + /** + * Exit normally with "0" exit code. + */ + SUCCEED(true, true), + /** + * Exit normally with "1" exit code. + */ + EXIT_1(false, true), + /** + * Throw {@link MockIOException}. This simulates a situation when an I/O error + * occurs starting a subprocess with {@link ProcessBuilder#start()}. + * {@link CommandOutputControl.Executable#execute()} will handle I/O errors and + * let them out. + */ + THROW_MOCK_IO_EXCEPTION(false, false), + ; + + CommandMockExit(boolean succeed, boolean exitNormally) { + this.succeed = succeed; + this.exitNormally = exitNormally; + } + + public boolean succeed() { + return succeed; + } + + public boolean exitNormally() { + return exitNormally; + } + + private final boolean succeed; + private final boolean exitNormally; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockSpec.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockSpec.java new file mode 100644 index 00000000000..2572309e751 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CommandMockSpec.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * Specification of a {@link CommandMock}. + */ +public record CommandMockSpec(Path name, Path mockName, CommandActionSpecs actions) { + + public CommandMockSpec { + Objects.requireNonNull(name); + Objects.requireNonNull(mockName); + Objects.requireNonNull(actions); + } + + public CommandMockSpec(Path name, CommandActionSpecs actions) { + this(name, Path.of(name.toString() + "-mock"), actions); + } + + public CommandMockSpec(String name, CommandActionSpecs actions) { + this(Path.of(name), actions); + } + + public CommandMockSpec(String name, String mockName, CommandActionSpecs actions) { + this(Path.of(name), Path.of(mockName), actions); + } + + public CommandMock.Builder toCommandMockBuilder() { + return actions.toCommandMockBuilder().name(mockName.toString()); + } + + public boolean isDefaultMockName() { + return (name.getFileName().toString() + "-mock").equals(mockName.getFileName().toString()); + } + + @Override + public String toString() { + return String.format("mock-of(%s)%s", name, actions); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CompletableCommandMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CompletableCommandMock.java new file mode 100644 index 00000000000..c9441e038b3 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/CompletableCommandMock.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +/** + * Command mock that runs a finite sequence of actions. + */ +public sealed interface CompletableCommandMock extends CommandMock permits ToolProviderCompletableCommandMock { + + boolean completed(); +} diff --git a/test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIOException.java similarity index 56% rename from test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.java rename to test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIOException.java index b0ba6c60854..3b299f05d3f 100644 --- a/test/jdk/java/security/Security/SecurityPropFile/SecurityPropFile.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIOException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 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 @@ -20,23 +20,20 @@ * or visit www.oracle.com if you need additional information or have any * questions. */ +package jdk.jpackage.test.mock; -/* - * @test - * @bug 4303068 - * @summary be allowed to specify the security properties file - * as a -D system property +import java.io.IOException; + +/** + * Simulates I/O error. * - * @run main/othervm -Djava.security.properties=${test.src}/SecurityPropFile.file -Djava.security.debug=properties SecurityPropFile + * @see CommandMockExit#THROW_MOCK_IO_EXCEPTION */ +public final class MockIOException extends IOException { -public class SecurityPropFile { - public static void main(String[] args) { - System.out.println(java.security.Security.getProperty - ("policy.provider")); - System.out.println(java.security.Security.getProperty - ("policy.url.1")); - System.out.println(java.security.Security.getProperty - ("policy.url.2")); + MockIOException(String msg) { + super(msg); } + + private static final long serialVersionUID = 1L; } diff --git a/src/hotspot/share/runtime/prefetch.hpp b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIllegalStateException.java similarity index 53% rename from src/hotspot/share/runtime/prefetch.hpp rename to test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIllegalStateException.java index 601337c14af..1817587364a 100644 --- a/src/hotspot/share/runtime/prefetch.hpp +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockIllegalStateException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2026, 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 @@ -19,27 +19,17 @@ * 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. - * */ +package jdk.jpackage.test.mock; -#ifndef SHARE_RUNTIME_PREFETCH_HPP -#define SHARE_RUNTIME_PREFETCH_HPP +/** + * Indicates command mock internal error. + */ +public final class MockIllegalStateException extends IllegalStateException { -#include "memory/allStatic.hpp" + public MockIllegalStateException(String msg) { + super(msg); + } -// If calls to prefetch methods are in a loop, the loop should be cloned -// such that if Prefetch{Scan,Copy}Interval and/or PrefetchFieldInterval -// say not to do prefetching, these methods aren't called. At the very -// least, they take up a memory issue slot. They should be implemented -// as inline assembly code: doing an actual call isn't worth the cost. - -class Prefetch : AllStatic { - public: - // Prefetch anticipating read; must not fault, semantically a no-op - static void read(const void* loc, intx interval); - - // Prefetch anticipating write; must not fault, semantically a no-op - static void write(void* loc, intx interval); -}; - -#endif // SHARE_RUNTIME_PREFETCH_HPP + private static final long serialVersionUID = 1L; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java new file mode 100644 index 00000000000..f8c04cc3927 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/MockingToolProvider.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import jdk.jpackage.internal.util.function.ExceptionBox; + +/** + * A command simulator implementing {@code ToolProvider}. + *

    + * Iterates over actions and runs them. Each action is write to stdout/stderr, create a file, etc. + */ +abstract sealed class MockingToolProvider implements ToolProviderCommandMock { + + MockingToolProvider(String name, Iterator actionIter) { + this.name = Objects.requireNonNull(name); + this.actionIter = Objects.requireNonNull(actionIter); + } + + static ToolProviderCommandMock createLoop(String name, Iterable actions) { + return new MockingToolProvider.NonCompletable(name, actions); + } + + static MockingToolProvider create(String name, Iterable actions) { + return new MockingToolProvider.Completable(name, actions); + } + + public boolean completed() { + return !actionIter.hasNext(); + } + + @Override + public String name() { + return name; + } + + @Override + public int run(PrintStream out, PrintStream err, String... args) { + var context = new CommandAction.Context(out, err, List.of(args)); + try { + while (actionIter.hasNext()) { + var action = actionIter.next(); + var reply = action.run(context); + if (reply.isPresent()) { + return reply.get(); + } + } + } catch (RethrowableException ex) { + // Let the checked exception out. + throwAny(ex.getCause()); + // Unreachable + return 0; + } catch (Exception ex) { + throw ExceptionBox.toUnchecked(ex); + } + + // No more actions to execute, but still expect it to keep going. + throw new MockIllegalStateException("No more actions to execute"); + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + throw new UnsupportedOperationException(); + } + + static final class RethrowableException extends Exception { + + RethrowableException(Exception ex) { + super(Objects.requireNonNull(ex)); + } + + private static final long serialVersionUID = 1L; + } + + @SuppressWarnings("unchecked") + private static void throwAny(Throwable e) throws E { + throw (E)e; + } + + private static final class LoopIterator implements Iterator { + + LoopIterator(Iterable iterable) { + this.iterable = Objects.requireNonNull(iterable); + rewind(); + } + + @Override + public boolean hasNext() { + return iter != null; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } else if (iter.hasNext()) { + return iter.next(); + } else { + rewind(); + if (!hasNext()) { + throw new NoSuchElementException(); + } else { + return iter.next(); + } + } + } + + private void rewind() { + iter = Objects.requireNonNull(iterable.iterator()); + if (!iter.hasNext()) { + iter = null; + } + } + + private final Iterable iterable; + private Iterator iter; + } + + static final class NonCompletable extends MockingToolProvider { + + NonCompletable(String name, Iterable actions) { + super(name, new LoopIterator<>(actions)); + } + + } + + static final class Completable extends MockingToolProvider implements ToolProviderCompletableCommandMock { + + Completable(String name, Iterable actions) { + super(name, actions.iterator()); + } + + } + + private final String name; + private final Iterator actionIter; + + static ToolProviderCommandMock UNREACHABLE = new MockingToolProvider.NonCompletable("", List.of()); +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/Script.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/Script.java new file mode 100644 index 00000000000..bc51d2b69f8 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/Script.java @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.IdentityWrapper; + +/** + * Script of command mocks. + */ +public interface Script { + + /** + * Returns a command mock for the given command line. + * + * @param cmdline the command line for which to look up a command mock + * + * @return a command mock matching the given command line + * @throws ScriptException if an internal script error occures + */ + CommandMock map(List cmdline) throws ScriptException; + + /** + * Returns command mocks registered with this object that have not completed yet. + * + * @See {@link CompletableCommandMock#completed()} + * + * @return the command mocks registered with this object that have not completed yet + */ + Collection incompleteMocks(); + + public static Builder build() { + return new Builder(); + } + + public static Predicate> cmdlinePredicate( + Predicate pred, + Function conv, + Function, Stream> toStream) { + + Objects.requireNonNull(pred); + Objects.requireNonNull(conv); + Objects.requireNonNull(toStream); + + return cmdline -> { + return toStream.apply(cmdline).map(conv).filter(pred).findFirst().isPresent(); + }; + } + + public static Predicate> cmdlineContains(String arg) { + return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), x -> x, List::stream); + } + + public static Predicate> cmdlineContains(Path arg) { + return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), Path::of, List::stream); + } + + public static Predicate> cmdlineStartsWith(String arg) { + return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), x -> x, cmdline -> { + return cmdline.stream().limit(1); + }); + } + + public static Predicate> cmdlineStartsWith(Path arg) { + return cmdlinePredicate(Predicate.isEqual(Objects.requireNonNull(arg)), Path::of, cmdline -> { + return cmdline.stream().limit(1); + }); + } + + public final class ScriptException extends RuntimeException { + + ScriptException(RuntimeException cause) { + super(Objects.requireNonNull(cause)); + } + + ScriptException(String msg) { + super(Objects.requireNonNull(msg)); + } + + private static final long serialVersionUID = 1L; + } + + public final class Builder { + + public Script createSequence() { + return new SequenceScript(List.copyOf(instructions), completableMocks()); + } + + public Script createLoop() { + return new LoopScript(List.copyOf(instructions), completableMocks()); + } + + public Builder map(Predicate> pred, CommandMock mock) { + Objects.requireNonNull(pred); + Objects.requireNonNull(mock); + if (mock instanceof CompletableCommandMock completable) { + completableMocks.add(new IdentityWrapper<>(completable)); + } + instruction(cmdline -> { + if (pred.test(cmdline)) { + return new CommandMockResult(Optional.of(mock)); + } else { + return new CommandMockResult(Optional.empty()); + } + }); + return this; + } + + public Builder map(Predicate> pred, CommandMock.Builder mock) { + Optional.ofNullable(commandMockBuilderMutator).ifPresent(mock::mutate); + return map(pred, mock.create()); + } + + public Builder map(Predicate> pred, CommandMockSpec mock) { + return map(pred, mock.toCommandMockBuilder()); + } + + public Builder map(CommandMockSpec mock) { + return map(cmdlineStartsWith(mock.name()), mock.toCommandMockBuilder()); + } + + public Builder use(CommandMock mock) { + return map(_ -> true, mock); + } + + public Builder use(Predicate> pred, CommandMock.Builder mock) { + return map(_ -> true, mock); + } + + public Builder use(Predicate> pred, CommandMockSpec mock) { + return map(_ -> true, mock); + } + + public Builder branch(Predicate> pred, Script script) { + Objects.requireNonNull(pred); + Objects.requireNonNull(script); + instruction(cmdline -> { + if (pred.test(cmdline)) { + return new ScriptResult(script); + } else { + return new CommandMockResult(Optional.empty()); + } + }); + return this; + } + + public Builder commandMockBuilderMutator(Consumer v) { + commandMockBuilderMutator = v; + return this; + } + + public Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + + private Builder instruction(Function, Result> instruction) { + instructions.add(Objects.requireNonNull(instruction)); + return this; + } + + private Collection completableMocks() { + return completableMocks.stream().map(IdentityWrapper::value).toList(); + } + + private static RuntimeException noMapping(List cmdline) { + return new ScriptException(String.format("Mapping for %s command line not found", cmdline)); + } + + private sealed interface Result { + } + + private record CommandMockResult(Optional value) implements Result { + CommandMockResult { + Objects.requireNonNull(value); + } + } + + private record ScriptResult(Script value) implements Result { + ScriptResult { + Objects.requireNonNull(value); + } + } + + private abstract static class AbstractScript implements Script { + + AbstractScript(Collection completableMocks) { + this.completableMocks = Objects.requireNonNull(completableMocks); + } + + @Override + public Collection incompleteMocks() { + return completableMocks.stream().filter(Predicate.not(CompletableCommandMock::completed)).toList(); + } + + private final Collection completableMocks; + } + + private static final class LoopScript extends AbstractScript { + + LoopScript(List, Result>> instructions, + Collection completableMocks) { + super(completableMocks); + this.instructions = Objects.requireNonNull(instructions); + } + + @Override + public CommandMock map(List cmdline) { + for (var instruction : instructions) { + switch (instruction.apply(cmdline)) { + case CommandMockResult result -> { + var mock = result.value(); + if (mock.isPresent()) { + return mock.get(); + } + } + case ScriptResult result -> { + return result.value().map(cmdline); + } + } + } + + throw noMapping(cmdline); + } + + private final List, Result>> instructions; + } + + private static final class SequenceScript extends AbstractScript { + + SequenceScript(List, Result>> instructions, + Collection completableMocks) { + super(completableMocks); + this.iter = instructions.iterator(); + } + + @Override + public CommandMock map(List cmdline) { + if (!iter.hasNext()) { + throw new ScriptException("No more mappings"); + } else { + switch (iter.next().apply(cmdline)) { + case CommandMockResult result -> { + var mock = result.value(); + if (mock.isPresent()) { + return mock.get(); + } + } + case ScriptResult result -> { + return result.value().map(cmdline); + } + } + } + + throw noMapping(cmdline); + } + + private final Iterator, Result>> iter; + } + + private Consumer commandMockBuilderMutator = CommandMock.Builder::noRepeats; + private final List, Result>> instructions = new ArrayList<>(); + private final Set> completableMocks = new HashSet<>(); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpec.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpec.java new file mode 100644 index 00000000000..60e5723e9a7 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpec.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +/** + * Specification of a {@link Script}. + */ +public record ScriptSpec(List items, boolean loop) { + + public ScriptSpec { + Objects.requireNonNull(items); + } + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append(items.toString()); + if (loop) { + // Append "Clockwise Gapped Circle Arrow" Unicode symbol. + sb.append('(').appendCodePoint(0x27F3).append(')'); + } + return sb.toString(); + } + + public Script create() { + var script = Script.build(); + items.forEach(item -> { + item.applyTo(script, loop); + }); + if (loop) { + return script.createLoop(); + } else { + return script.createSequence(); + } + } + + public Collection commandNames() { + return items.stream().map(Item::mockSpec).map(CommandMockSpec::name).distinct().toList(); + } + + private record Item(CommandMockSpec mockSpec, int repeatCount, boolean detailedDescription) { + + private Item { + Objects.requireNonNull(mockSpec); + if (repeatCount < 0) { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() { + var sb = new StringBuilder(); + if (detailedDescription) { + sb.append(mockSpec); + } else if (mockSpec.isDefaultMockName()) { + sb.append(mockSpec.name()); + } else { + sb.append(mockSpec.mockName()); + } + if (repeatCount > 0) { + sb.append('(').append(repeatCount + 1).append(')'); + } + return sb.toString(); + } + + void applyTo(Script.Builder script, boolean loopScript) { + var pred = Script.cmdlineStartsWith(mockSpec.name()); + + var mockBuilder = mockSpec.toCommandMockBuilder(); + if (loopScript) { + script.map(pred, mockBuilder.repeat(repeatCount).create()); + } else { + mockBuilder.repeat(0); + IntStream.rangeClosed(0, repeatCount).forEach(_ -> { + script.map(pred, mockBuilder.create()); + }); + } + } + + } + + public static Builder build() { + return new Builder(); + } + + public static final class Builder { + + private Builder() { + } + + public ScriptSpec create() { + return new ScriptSpec(List.copyOf(items), loop); + } + + public Builder loop(boolean v) { + loop = v; + return this; + } + + public Builder loop() { + return loop(true); + } + + public final class ItemBuilder { + + private ItemBuilder(CommandMockSpec mockSpec) { + this.mockSpec = Objects.requireNonNull(mockSpec); + } + + public Builder add() { + items.add(new Item(mockSpec, repeat, detailedDescription)); + return Builder.this; + } + + public ItemBuilder repeat(int v) { + if (repeat < 0) { + throw new IllegalArgumentException(); + } + repeat = v; + return this; + } + + public ItemBuilder detailedDescription(boolean v) { + detailedDescription = v; + return this; + } + + public ItemBuilder detailedDescription() { + return detailedDescription(true); + } + + private final CommandMockSpec mockSpec; + private int repeat; + private boolean detailedDescription; + } + + public Builder add(CommandMockSpec mockSpec) { + return build(mockSpec).add(); + } + + public Builder addLoop(CommandMockSpec mockSpec) { + return build(mockSpec).add(); + } + + public ItemBuilder build(CommandMockSpec mockSpec) { + return new ItemBuilder(mockSpec); + } + + private final List items = new ArrayList<>(); + private boolean loop; + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpecInDir.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpecInDir.java new file mode 100644 index 00000000000..8c59c777d96 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ScriptSpecInDir.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.nio.file.Path; +import java.util.Objects; + +/** + * Specification of a {@link Script} bound to a specific directory. + */ +public class ScriptSpecInDir { + + public ScriptSpecInDir() { + } + + @Override + public String toString() { + return scriptSpec.toString(); + } + + public boolean isPathInDir(Path path) { + return path.startsWith(dir); + } + + public ScriptSpecInDir dir(Path v) { + dir = v; + return this; + } + + public ScriptSpecInDir scriptSpec(ScriptSpec v) { + scriptSpec = v; + return this; + } + + public ScriptSpec scriptSpec() { + Objects.requireNonNull(dir); + return Objects.requireNonNull(scriptSpec); + } + + public Script create() { + return scriptSpec().create(); + } + + private ScriptSpec scriptSpec; + private Path dir; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCommandMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCommandMock.java new file mode 100644 index 00000000000..ee9556277d4 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCommandMock.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +import java.util.spi.ToolProvider; + +public sealed interface ToolProviderCommandMock extends CommandMock, ToolProvider + permits ToolProviderCompletableCommandMock, MockingToolProvider { +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCompletableCommandMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCompletableCommandMock.java new file mode 100644 index 00000000000..907cedd38d7 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/ToolProviderCompletableCommandMock.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +public sealed interface ToolProviderCompletableCommandMock extends ToolProviderCommandMock, CompletableCommandMock + permits MockingToolProvider.Completable { +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/VerbatimCommandMock.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/VerbatimCommandMock.java new file mode 100644 index 00000000000..799caaa0ea9 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/mock/VerbatimCommandMock.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.test.mock; + +public enum VerbatimCommandMock implements CommandMock { + + INSTANCE +} diff --git a/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LibProvidersLookupTest.java b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LibProvidersLookupTest.java new file mode 100644 index 00000000000..41a9bdc647e --- /dev/null +++ b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LibProvidersLookupTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockExit; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class LibProvidersLookupTest { + + @ParameterizedTest + @EnumSource(value = CommandMockExit.class) + public void test_supported(CommandMockExit exit) { + + var ldd = CommandActionSpecs.build().exit(exit).toCommandMockBuilder().name("ldd-mock").create(); + + Globals.main(() -> { + Globals.instance().executorFactory(() -> { + return new Executor().mapper(executor -> { + return executor.copy().mapper(null).toolProvider(ldd); + }); + }); + + boolean actual = LibProvidersLookup.supported(); + assertEquals(exit.exitNormally(), actual); + + return 0; + }); + } +} diff --git a/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxPackageArchTest.java b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxPackageArchTest.java new file mode 100644 index 00000000000..baf03a32142 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxPackageArchTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.model.StandardPackageType.LINUX_DEB; +import static jdk.jpackage.internal.model.StandardPackageType.LINUX_RPM; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.internal.util.Result; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockExit; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.Script; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class LinuxPackageArchTest { + + @ParameterizedTest + @MethodSource + public void test(Runnable test) { + test.run(); + } + + private static List test() { + var data = new ArrayList(); + + // "foo" stdout interleaved with "bar" stderr + var fooArch = CommandActionSpecs.build() + .printToStdout("f").printToStderr("b") + .printToStdout("o").printToStderr("a") + .printToStdout("o").printToStderr("r"); + + for (var exit : CommandMockExit.values()) { + var dpkg = fooArch.copy().printToStdout("-deb").exit(exit).create(); + + data.add(new DebTestSpec(dpkg, Optional.of("foo-deb").filter(_ -> { + return exit.succeed(); + }))); + } + + for (var rpmbuildExit : CommandMockExit.values()) { + var rpmbuild = fooArch.copy().printToStdout("-rpmbuild").exit(rpmbuildExit).create(); + for (var rpmExit : CommandMockExit.values()) { + var rpm = fooArch.copy().printToStdout("-rpm").exit(rpmExit).create(); + Optional expect; + if (rpmbuildExit.succeed()) { + expect = Optional.of("foo-rpmbuild"); + rpm = CommandActionSpecs.UNREACHABLE; + } else { + if (rpmExit.succeed()) { + expect = Optional.of("foo-rpm"); + } else { + expect = Optional.empty(); + } + } + + data.add(new RpmTestSpec(rpmbuild, rpm, expect)); + } + } + + return data; + } + + record RpmTestSpec(CommandActionSpecs rpmbuild, CommandActionSpecs rpm, Optional expect) implements Runnable { + + RpmTestSpec { + Objects.requireNonNull(rpm); + Objects.requireNonNull(rpmbuild); + Objects.requireNonNull(expect); + } + + @Override + public void run() { + + // Create an executor factory that will: + // - Substitute the "rpm" command with `rpm` mock. + // - Substitute the "rpmbuild" command with `rpmbuild` mock. + // - Throw if a command with the name other than "rpm" and "rpmbuild" is requested for execution. + + var script = Script.build() + // LinuxPackageArch must run the "rpmbuild" command first. Put its mapping at the first position. + .map(new CommandMockSpec("rpmbuild", rpmbuild)) + // LinuxPackageArch may optionally run the "rpm" command. Put its mapping after the "rpmbuild" command mapping. + .map(new CommandMockSpec("rpm", rpm)) + // Create a sequential script: after every Script#map() call, the script will advance the current mapping. + // This means each mapping in the script will be considered only once. + // If "rpm" and "rpmbuild" commands are executed in reverse order, the second Script#map() will throw. + .createSequence(); + + test(expect, LINUX_RPM, script); + } + } + + record DebTestSpec(CommandActionSpecs dpkg, Optional expect) implements Runnable { + + DebTestSpec { + Objects.requireNonNull(dpkg); + Objects.requireNonNull(expect); + } + + @Override + public void run() { + var script = Script.build().map(new CommandMockSpec("dpkg", dpkg)).createSequence(); + + test(expect, LINUX_DEB, script); + } + } + + private static void test(Optional expectedArch, StandardPackageType pkgType, Script script) { + + Globals.main(() -> { + + MockUtils.buildJPackage().script(script).applyToGlobals(); + + Result arch = LinuxPackageArch.create(pkgType); + + assertEquals(arch.hasValue(), expectedArch.isPresent()); + expectedArch.ifPresent(v -> { + assertEquals(v, arch.orElseThrow().value()); + }); + + assertEquals(List.of(), script.incompleteMocks()); + + return 0; + }); + } +} diff --git a/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxSystemEnvironmentTest.java b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxSystemEnvironmentTest.java new file mode 100644 index 00000000000..8ff958491b1 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/linux/jdk.jpackage/jdk/jpackage/internal/LinuxSystemEnvironmentTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockExit; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.Script; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class LinuxSystemEnvironmentTest { + + @ParameterizedTest + @MethodSource + public void test_detectNativePackageType(DetectNativePackageTypeTestSpec test) { + test.run(); + } + + private static List test_detectNativePackageType() { + var data = new ArrayList(); + for (var rpmExit : CommandMockExit.values()) { + for (var debExit : CommandMockExit.values()) { + CommandActionSpecs deb = CommandActionSpecs.build().exit(debExit).create(); + CommandActionSpecs rpm; + Optional expected; + if (debExit.succeed()) { + expected = Optional.of(StandardPackageType.LINUX_DEB); + rpm = CommandActionSpecs.UNREACHABLE; + } else { + rpm = CommandActionSpecs.build().exit(rpmExit).create(); + if (rpmExit.succeed()) { + expected = Optional.of(StandardPackageType.LINUX_RPM); + } else { + expected = Optional.empty(); + } + } + data.add(new DetectNativePackageTypeTestSpec(expected, rpm, deb)); + } + } + return data; + } + + record DetectNativePackageTypeTestSpec(Optional expect, CommandActionSpecs rpm, CommandActionSpecs deb) { + + DetectNativePackageTypeTestSpec { + Objects.requireNonNull(expect); + Objects.requireNonNull(rpm); + Objects.requireNonNull(deb); + } + + void run() { + + var script = Script.build() + .map(new CommandMockSpec("rpm", rpm)) + .map(new CommandMockSpec("dpkg", deb)) + .createLoop(); + + Globals.main(() -> { + + MockUtils.buildJPackage().script(script).applyToGlobals(); + + var actual = LinuxSystemEnvironment.detectNativePackageType(); + + assertEquals(expect, actual); + + assertEquals(List.of(), script.incompleteMocks()); + + return 0; + }); + } + } +} diff --git a/test/jdk/tools/jpackage/junit/linux/junit.java b/test/jdk/tools/jpackage/junit/linux/junit.java index 214812e951e..0fd337c812c 100644 --- a/test/jdk/tools/jpackage/junit/linux/junit.java +++ b/test/jdk/tools/jpackage/junit/linux/junit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -30,3 +30,35 @@ * ../../share/jdk.jpackage/jdk/jpackage/internal/model/ApplicationLayoutTest.java * @run junit jdk.jpackage/jdk.jpackage.internal.LinuxApplicationLayoutTest */ + +/* @test + * @summary Test LinuxSystemEnvironment + * @requires (os.family == "linux") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.mock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/LinuxSystemEnvironmentTest.java + * ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java + * @run junit jdk.jpackage/jdk.jpackage.internal.LinuxSystemEnvironmentTest + */ + +/* @test + * @summary Test LibProvidersLookup + * @requires (os.family == "linux") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.mock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/LibProvidersLookupTest.java + * @run junit jdk.jpackage/jdk.jpackage.internal.LibProvidersLookupTest + */ + +/* @test + * @summary Test LinuxPackageArch + * @requires (os.family == "linux") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.mock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/LinuxPackageArchTest.java + * ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java + * @run junit jdk.jpackage/jdk.jpackage.internal.LinuxPackageArchTest + */ diff --git a/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgPackagerTest.java b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgPackagerTest.java new file mode 100644 index 00000000000..e5da383142a --- /dev/null +++ b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgPackagerTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.model.StandardPackageType.MAC_DMG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.MacPackagingPipeline.MacBuildApplicationTaskID; +import jdk.jpackage.internal.PackagingPipeline.BuildApplicationTaskID; +import jdk.jpackage.internal.model.AppImageLayout; +import jdk.jpackage.internal.model.RuntimeBuilder; +import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException; +import jdk.jpackage.internal.util.FileUtils; +import jdk.jpackage.internal.util.RetryExecutor; +import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.test.mock.CommandActionSpec; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.MockIllegalStateException; +import jdk.jpackage.test.mock.ScriptSpec; +import jdk.jpackage.test.mock.ScriptSpecInDir; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class MacDmgPackagerTest { + + /** + * Exercise branches in {@link MacDmgPackager#buildDMG()}. + */ + @ParameterizedTest + @MethodSource + public void test(DmgScript scriptSpec, @TempDir Path workDir) { + scriptSpec.run(workDir); + } + + private static List test() { + var data = new ArrayList(); + + var succeed = CommandActionSpecs.build().exit().create(); + var fail = CommandActionSpecs.build().exit(1).create(); + + // Test create + for (var createFullSucceed : List.of(true, false)) { + var dmgScript = new DmgScript(); + + var scriptBuilder = ScriptSpec.build(); + + if (createFullSucceed) { + // `hdiutil create -srcfolder` succeeds + scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create", dmgScript.hdiutilCreate().exit().create())); + } else { + // `hdiutil create -srcfolder` fails + scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create", fail)); + scriptBuilder.add(new CommandMockSpec("hdiutil", "hdiutil-create-empty", dmgScript.hdiutilCreateEmpty().exit().create())); + } + + scriptBuilder + // `hdiutil attach` succeeds + .add(new CommandMockSpec("hdiutil", "hdiutil-attach", succeed)) + // `osascript` succeeds + .add(new CommandMockSpec("osascript", succeed)) + // `hdiutil detach` succeeds + .add(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit().create())) + // `hdiutil convert` succeeds + .add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create())); + + data.add(dmgScript.scriptSpec(scriptBuilder.create())); + } + + // Test detach + for (var detachResult : DetachResult.values()) { + var dmgScript = new DmgScript(); + + var scriptBuilder = ScriptSpec.build() + .add(new CommandMockSpec("hdiutil", "hdiutil-create", dmgScript.hdiutilCreate().exit().create())) + .add(new CommandMockSpec("hdiutil", "hdiutil-attach", succeed)) + .add(new CommandMockSpec("osascript", succeed)); + + switch (detachResult) { + case ALL_FAIL -> { + dmgScript.expect(UnexpectedResultException.class); + scriptBuilder.build(new CommandMockSpec("hdiutil", "hdiutil-detach", fail)).repeat(9).add(); + } + case LAST_SUCCEED -> { + scriptBuilder + .build(new CommandMockSpec("hdiutil", "hdiutil-detach", fail)).repeat(8).add() + .add(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit().create())) + .add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create())); + } + case FIRST_SUCCEED_WITH_EXIT_1 -> { + scriptBuilder + .build(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach().exit(1).create())) + .detailedDescription().add() + .add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create())); + } + case FIRST_SUCCEED_MOUNT_POINT_REMAINS -> { + scriptBuilder + .build(new CommandMockSpec("hdiutil", "hdiutil-detach", dmgScript.hdiutilDetach(false).exit().create())) + .detailedDescription().add() + .add(new CommandMockSpec("hdiutil", "hdiutil-convert", dmgScript.hdiutilConvert().exit().create())); + } + } + + data.add(dmgScript.scriptSpec(scriptBuilder.create())); + } + + return data; + } + + private enum DetachResult { + ALL_FAIL, + LAST_SUCCEED, + // The first `hdiutil detach` attempt exits with exit code "1" but deletes the mounted directory. + FIRST_SUCCEED_WITH_EXIT_1, + // The first `hdiutil detach` attempt exits with exit code "0" and the mounted directory stays undeleted. + FIRST_SUCCEED_MOUNT_POINT_REMAINS, + ; + } + + private static MacDmgSystemEnvironment createSysEnv(ScriptSpec scriptSpec) { + return new MacDmgSystemEnvironment( + Path.of("hdiutil"), + Path.of("osascript"), + Stream.of("SetFile").map(Path::of).filter(scriptSpec.commandNames()::contains).findFirst() + ); + } + + private static RuntimeBuilder createRuntimeBuilder() { + return new RuntimeBuilder() { + @Override + public void create(AppImageLayout appImageLayout) { + throw new UnsupportedOperationException(); + } + }; + } + + private static void runPackagingMock(Path workDir, MacDmgSystemEnvironment sysEnv) { + + var app = new ApplicationBuilder() + .appImageLayout(MacPackagingPipeline.APPLICATION_LAYOUT) + .runtimeBuilder(createRuntimeBuilder()) + .name("foo") + .create(); + + var macApp = new MacApplicationBuilder(app).create(); + + var macDmgPkg = new MacDmgPackageBuilder(new MacPackageBuilder(new PackageBuilder(macApp, MAC_DMG))).create(); + + var buildEnv = new BuildEnvBuilder(workDir.resolve("build-root")).appImageDirFor(macDmgPkg).create(); + + var packager = new MacDmgPackager(buildEnv, macDmgPkg, workDir, sysEnv); + + var pipelineBuilder = MacPackagingPipeline.build(Optional.of(packager.pkg())); + packager.accept(pipelineBuilder); + + // Disable actions of tasks filling an application image. + Stream.concat( + Stream.of(BuildApplicationTaskID.values()), + Stream.of(MacBuildApplicationTaskID.values()) + ).forEach(taskId -> { + pipelineBuilder.task(taskId).noaction().add(); + }); + + var contentMock = new ContentMock(); + + // Fill application image with content mock. + pipelineBuilder.task(BuildApplicationTaskID.CONTENT).applicationAction(env -> { + contentMock.create(env.resolvedLayout().contentDirectory()); + }).add(); + + pipelineBuilder.create().execute(buildEnv, packager.pkg(), packager.outputDir()); + + var outputDmg = packager.outputDir().resolve(packager.pkg().packageFileNameWithSuffix()); + + contentMock.verifyStoredInFile(outputDmg); + } + + private static final class DmgScript extends ScriptSpecInDir { + + @Override + public String toString() { + var sb = new StringBuilder(); + sb.append(super.toString()); + Optional.ofNullable(expectedErrorType).ifPresent(type -> { + sb.append("; ").append(type.getCanonicalName()); + }); + return sb.toString(); + } + + @Override + public DmgScript scriptSpec(ScriptSpec v) { + super.scriptSpec(v); + return this; + } + + DmgScript expect(Class v) { + expectedErrorType = v; + return this; + } + + void run(Path workDir) { + + var script = dir(Objects.requireNonNull(workDir)).create(); + + ExecutorFactory executorFactory = MockUtils.buildJPackage() + .script(script).listener(System.out::println).createExecutorFactory(); + + var objectFactory = ObjectFactory.build() + .executorFactory(executorFactory) + .retryExecutorFactory(new RetryExecutorFactory() { + @Override + public RetryExecutor retryExecutor(Class exceptionType) { + return RetryExecutorFactory.DEFAULT.retryExecutor(exceptionType).setSleepFunction(_ -> { + // Don't "sleep" to make the test run faster. + }); + } + }) + .create(); + + Globals.main(() -> { + Globals.instance().objectFactory(objectFactory); + if (expectedErrorType == null) { + runPackagingMock(workDir, createSysEnv(scriptSpec())); + } else { + var ex = assertThrows(Exception.class, () -> { + runPackagingMock(workDir, createSysEnv(scriptSpec())); + }); + var cause = ExceptionBox.unbox(ex); + assertEquals(expectedErrorType, cause.getClass()); + } + return 0; + }); + + assertEquals(List.of(), script.incompleteMocks()); + } + + CommandActionSpecs.Builder hdiutilCreate(boolean empty) { + CommandActionSpec action = CommandActionSpec.create("create", context -> { + var dstDmg = Path.of(context.optionValue("-ov")); + assertTrue(isPathInDir(dstDmg)); + + var volumeName = context.optionValue("-volname"); + + if (empty) { + createDmg(new CreateDmgResult(dstDmg, volumeName, Optional.empty())); + Files.createFile(dstDmg); + } else { + var srcDir = Path.of(context.optionValue("-srcfolder")); + assertTrue(isPathInDir(srcDir)); + + createDmg(new CreateDmgResult(dstDmg, volumeName, Optional.of(srcDir))); + + try (var walk = Files.walk(srcDir)) { + var paths = walk.map(srcDir::relativize).map(Path::toString).toList(); + Files.write(dstDmg, paths); + } + } + }); + return CommandActionSpecs.build().action(action); + } + + CommandActionSpecs.Builder hdiutilCreate() { + return hdiutilCreate(false); + } + + CommandActionSpecs.Builder hdiutilCreateEmpty() { + return hdiutilCreate(true); + } + + CommandActionSpecs.Builder hdiutilDetach() { + return hdiutilDetach(true); + } + + CommandActionSpecs.Builder hdiutilDetach(boolean deleteMountPoint) { + var sb = new StringBuilder(); + sb.append("detach"); + if (!deleteMountPoint) { + sb.append("(rm-mount-point)"); + } + CommandActionSpec action = CommandActionSpec.create(sb.toString(), context -> { + var mountPoint = Path.of(context.args().getLast()); + assertTrue(isPathInDir(mountPoint)); + + try (var walk = Files.walk(mountPoint)) { + var dstDmg = dmg().dmg(); + var paths = Stream.concat( + walk.map(mountPoint::relativize), + Files.readAllLines(dstDmg).stream().filter(Predicate.not(String::isEmpty)).map(Path::of) + ).sorted().map(Path::toString).toList(); + Files.write(dstDmg, paths); + } + + if (deleteMountPoint) { + FileUtils.deleteRecursive(mountPoint); + } + }); + return CommandActionSpecs.build().action(action); + } + + CommandActionSpecs.Builder hdiutilConvert() { + CommandActionSpec action = CommandActionSpec.create("convert", context -> { + var srcDmg = Path.of(context.args().get(1)); + assertTrue(isPathInDir(srcDmg)); + + var dstDmg = Path.of(context.args().getLast()); + assertTrue(isPathInDir(dstDmg)); + + Files.copy(srcDmg, dstDmg); + }); + return CommandActionSpecs.build().action(action); + } + + private void createDmg(CreateDmgResult v) { + if (dmg != null) { + throw new MockIllegalStateException("The DMG already set"); + } else { + dmg = Objects.requireNonNull(v); + } + } + + private CreateDmgResult dmg() { + if (dmg == null) { + throw new MockIllegalStateException("The DMG not set"); + } else { + return dmg; + } + } + + private record CreateDmgResult(Path dmg, String volumeName, Optional srcFolder) { + CreateDmgResult { + Objects.requireNonNull(dmg); + Objects.requireNonNull(volumeName); + Objects.requireNonNull(srcFolder); + } + } + + private CreateDmgResult dmg; + private Class expectedErrorType; + } + + private static final class ContentMock { + + void create(Path dir) throws IOException { + Files.createDirectories(dir.resolve("foo/bar")); + Files.writeString(dir.resolve("foo/bar/buz"), "Hello!"); + if (!OperatingSystem.isWindows()) { + Files.createSymbolicLink(dir.resolve("symlink"), Path.of("foo")); + } + } + + void verifyStoredInFile(Path file) { + try { + var expectedPaths = Stream.of( + Stream.of(Path.of("")), + DMG_ICON_FILES.stream(), + Stream.of( + Stream.of("foo/bar/buz"), + Stream.of("symlink").filter(_ -> { + return !OperatingSystem.isWindows(); + }) + ).flatMap(x -> x).map(Path::of).map(Path.of("foo.app/Contents")::resolve) + ).flatMap(x -> x).mapMulti(EXPAND_PATH).sorted().distinct().toList(); + var actualPaths = Files.readAllLines(file).stream().map(Path::of).toList(); + assertEquals(expectedPaths, actualPaths); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + } + + private final static BiConsumer> EXPAND_PATH = (path, sink) -> { + do { + sink.accept(path); + path = path.getParent(); + } while (path != null); + }; + + private final static List DMG_ICON_FILES = Stream.of( + ".VolumeIcon.icns", + ".background/background.tiff" + ).map(Path::of).collect(Collectors.toUnmodifiableList()); +} diff --git a/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgSystemEnvironmentTest.java b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgSystemEnvironmentTest.java new file mode 100644 index 00000000000..de2b07e86a6 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/macosx/jdk.jpackage/jdk/jpackage/internal/MacDmgSystemEnvironmentTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import jdk.jpackage.internal.util.RetryExecutor; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockExit; +import jdk.jpackage.test.mock.CommandMockSpec; +import jdk.jpackage.test.mock.Script; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class MacDmgSystemEnvironmentTest { + + @ParameterizedTest + @MethodSource + void test_findSetFileUtility(FindSetFileUtilityTestSpec test) { + test.run(); + } + + private static List test_findSetFileUtility() { + var data = new ArrayList(); + + var succeed = CommandActionSpecs.build().exit().create(); + + for (var failureCause : List.of(CommandMockExit.EXIT_1, CommandMockExit.THROW_MOCK_IO_EXCEPTION)) { + + var fail = CommandActionSpecs.build().exit(failureCause).create(); + + for (var i = 0; i != MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.size(); i++) { + + var expected = MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.get(i); + + var mocks = new ArrayList(); + + MacDmgSystemEnvironment.SETFILE_KNOWN_PATHS.subList(0, i).stream().map(failureSetFilePath -> { + return new CommandMockSpec(failureSetFilePath, fail); + }).forEach(mocks::add); + + mocks.add(new CommandMockSpec(expected, succeed)); + + data.add(new FindSetFileUtilityTestSpec(Optional.of(expected), mocks)); + } + + var lastMocks = data.getLast().mockSpecs(); + var lastSucceedMock = lastMocks.getLast(); + var lastFailMock = new CommandMockSpec(lastSucceedMock.name(), lastSucceedMock.mockName(), fail); + + var mocks = new ArrayList<>(lastMocks); + mocks.set(mocks.size() - 1, lastFailMock); + + for (var xcrunOutout : List., Boolean>>of( + // Use the path to the command of the current process + // as an output mock for the /usr/bin/xcrun command. + // MacDmgSystemEnvironment.findSetFileUtility() reads the command output + // and checks whether it is an executable file, + // so the hardcoded value is not an option for the output mock. + Map.entry(Optional.of(ProcessHandle.current().info().command().orElseThrow()), true), + // "/usr/bin/xcrun" outputs a path to non-executable file. + Map.entry(Optional.of("/dev/null"), false), + // "/usr/bin/xcrun" outputs '\0' making subsequent Path.of("\0") fail. + Map.entry(Optional.of("\0"), false), + // "/usr/bin/xcrun" doesn't output anything. + Map.entry(Optional.empty(), false) + )) { + + + mocks.add(new CommandMockSpec("/usr/bin/xcrun", CommandActionSpecs.build().mutate(builder -> { + xcrunOutout.getKey().ifPresent(builder::stdout); + }).exit(CommandMockExit.SUCCEED).create())); + + Optional expected; + if (xcrunOutout.getValue()) { + expected = xcrunOutout.getKey(); + } else { + expected = Optional.empty(); + } + + data.add(new FindSetFileUtilityTestSpec(expected.map(Path::of), List.copyOf(mocks))); + + mocks.removeLast(); + } + + // The last test case: "/usr/bin/xcrun" fails + mocks.add(new CommandMockSpec("/usr/bin/xcrun", fail)); + data.add(new FindSetFileUtilityTestSpec(Optional.empty(), mocks)); + } + + return data; + } + + record FindSetFileUtilityTestSpec(Optional expected, List mockSpecs) { + + FindSetFileUtilityTestSpec { + Objects.requireNonNull(expected); + Objects.requireNonNull(mockSpecs); + } + + @Override + public String toString() { + var tokens = new ArrayList(); + expected.ifPresent(v -> { + tokens.add(String.format("expect=%s", v)); + }); + tokens.add(mockSpecs.toString()); + return tokens.stream().collect(Collectors.joining(", ")); + } + + void run() { + + var script = Script.build().mutate(builder -> { + mockSpecs.forEach(builder::map); + }).createSequence(); + + Globals.main(() -> { + MockUtils.buildJPackage().script(script).applyToGlobals(); + + var actual = MacDmgSystemEnvironment.findSetFileUtility(); + + assertEquals(expected, actual); + assertEquals(List.of(), script.incompleteMocks()); + + return 0; + }); + } + } +} diff --git a/test/jdk/tools/jpackage/junit/macosx/junit.java b/test/jdk/tools/jpackage/junit/macosx/junit.java index 1549aaa6cd7..c7fd2bc5f8d 100644 --- a/test/jdk/tools/jpackage/junit/macosx/junit.java +++ b/test/jdk/tools/jpackage/junit/macosx/junit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -30,3 +30,25 @@ * ../../share/jdk.jpackage/jdk/jpackage/internal/model/ApplicationLayoutTest.java * @run junit jdk.jpackage/jdk.jpackage.internal.MacApplicationLayoutTest */ + +/* @test + * @summary Test MacDmgSystemEnvironmentTest + * @requires (os.family == "mac") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.mock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/MacDmgSystemEnvironmentTest.java + * ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java + * @run junit jdk.jpackage/jdk.jpackage.internal.MacDmgSystemEnvironmentTest + */ + +/* @test + * @summary Test MacDmgPackagerTest + * @requires (os.family == "mac") + * @library /test/jdk/tools/jpackage/helpers + * @build jdk.jpackage.test.mock.* + * @compile/module=jdk.jpackage -Xlint:all -Werror + * jdk/jpackage/internal/MacDmgPackagerTest.java + * ../../share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java + * @run junit jdk.jpackage/jdk.jpackage.internal.MacDmgPackagerTest + */ diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java index 35ab1fbec5e..1a14330fe6e 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/DefaultBundlingEnvironmentTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -22,15 +22,42 @@ */ package jdk.jpackage.internal; + +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.test.mock.CommandMock.ioerror; +import static jdk.jpackage.test.mock.CommandMock.succeed; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.spi.ToolProvider; +import java.util.stream.Stream; import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.cli.StandardBundlingOperation; +import jdk.jpackage.internal.model.AppImagePackageType; import jdk.jpackage.internal.model.BundlingOperationDescriptor; +import jdk.jpackage.internal.model.PackageType; +import jdk.jpackage.internal.model.StandardPackageType; +import jdk.jpackage.test.Annotations; +import jdk.jpackage.test.HelloApp; +import jdk.jpackage.test.JUnitAdapter; +import jdk.jpackage.test.JavaAppDesc; +import jdk.jpackage.test.TKit; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMock; +import jdk.jpackage.test.mock.CommandMockExit; +import jdk.jpackage.test.mock.Script; import org.junit.jupiter.api.Test; -public class DefaultBundlingEnvironmentTest { +public class DefaultBundlingEnvironmentTest extends JUnitAdapter { @Test void testDefaultBundlingOperation() { @@ -55,4 +82,200 @@ public class DefaultBundlingEnvironmentTest { assertEquals(descriptor, env.defaultOperation().orElseThrow()); assertEquals(1, executed[0]); } + + /** + * Tests that commands executed to initialize the system environment are + * executed only once. + * @throws IOException + */ + @Annotations.Test + @Annotations.ParameterSupplier + public void testInitializedOnce(StandardBundlingOperation op) throws IOException { + + List> executedCommands = Collections.synchronizedList(new ArrayList<>()); + + var script = createMockScript(op); + + ToolProvider jpackage = MockUtils.buildJPackage() + .os(op.os()) + .script(script) + .listener(executedCommands::add).create(); + + var inputDir = TKit.createTempDirectory("input"); + var appDesc = JavaAppDesc.parse(null); + HelloApp.createBundle(appDesc, inputDir); + + // + // The command line should fail as the main class name is not specified and it is not set in the main jar. + // + // Run native packaging twice. + // It can execute commands required to configure the system environment in the first iteration. + // It must not execute a single command in the second iteration. + // + // Run app image packaging once. + // It must not execute a single command because app image packaging should not require native commands (Unless + // it is macOS where it will sign the app image with an ad hoc signature + // using the codesign tool. But: #1 - it is not a variable part of the system environment; + // #2 - jpackage should bail out earlier). + // + + final var type = op.packageTypeValue(); + final int iterationCount; + if (op.packageType() instanceof AppImagePackageType) { + iterationCount = 1; + } else { + iterationCount = 2; + } + + for (var i = 0; i != iterationCount; i++) { + var result = new Executor().toolProvider(jpackage).saveOutput().args( + "--type=" + type, + "--input", inputDir.toString(), + "--main-jar", appDesc.jarFileName()).execute(); + + assertEquals(1, result.getExitCode()); + + // Assert it bailed out with the expected error. + assertEquals(List.of( + I18N.format("message.error-header", I18N.format("error.no-main-class-with-main-jar", appDesc.jarFileName())), + I18N.format("message.advice-header", I18N.format("error.no-main-class-with-main-jar.advice", appDesc.jarFileName())) + ), result.stderr()); + + TKit.trace("The list of executed commands:"); + executedCommands.forEach(cmdline -> { + TKit.trace(" " + cmdline); + }); + TKit.trace("Done"); + + if (i == 0) { + executedCommands.clear(); + } + } + + assertEquals(List.of(), executedCommands); + assertEquals(List.of(), script.incompleteMocks()); + } + + public static List testInitializedOnce() { + return StandardBundlingOperation.ofPlatform(OperatingSystem.current()) + .filter(StandardBundlingOperation::isCreateBundle).map(v -> { + return new Object[] {v}; + }).toList(); + } + + private static Script createMockScript(StandardBundlingOperation op) { + + if (op.packageType() instanceof AppImagePackageType) { + return Script.build().createSequence(); + } + + switch (op.os()) { + case WINDOWS -> { + return createWinMockScript(); + } + case LINUX -> { + return createLinuxMockScript(op.packageType()); + } + case MACOS -> { + return createMacMockScript(); + } + default -> { + throw new AssertionError(); + } + } + } + + private static Script createWinMockScript() { + + // Make "candle.exe" and "light.exe" always fail. + var candle = ioerror("candle-mock"); + var light = ioerror("light-mock"); + + // Make the "wix.exe" functional. + var wix = CommandActionSpecs.build() + .stdout("5.0.2+aa65968c") + .exit(CommandMockExit.SUCCEED) + .toCommandMockBuilder().name("wix-mock").create(); + + var script = Script.build() + .map(Script.cmdlineStartsWith("candle.exe"), candle) + .map(Script.cmdlineStartsWith("light.exe"), light) + .map(Script.cmdlineStartsWith("wix.exe"), wix) + .createLoop(); + + return script; + } + + private static Script createMacMockScript() { + + @SuppressWarnings("unchecked") + var setfilePaths = (List)toSupplier(() -> { + return Class.forName(String.join(".", + DefaultBundlingEnvironmentTest.class.getPackageName(), + "MacDmgSystemEnvironment" + )).getDeclaredField("SETFILE_KNOWN_PATHS").get(null); + }).get(); + + var script = Script.build(); + + for (var setfilePath: setfilePaths) { + script.map(Script.cmdlineStartsWith(setfilePath), ioerror(setfilePath.toString() + "-mock")); + } + + script.map(Script.cmdlineStartsWith("/usr/bin/xcrun"), succeed("/usr/bin/xcrun-mock")); + + return script.createLoop(); + } + + private static Script createLinuxMockScript(PackageType pkgType) { + + final Map mocks = new HashMap<>(); + + var script = Script.build(); + + final Set debCommandNames = Set.of("dpkg", "dpkg-deb", "fakeroot"); + final Set rpmCommandNames = Set.of("rpm", "rpmbuild"); + + final Set succeedCommandNames; + switch (pkgType) { + case StandardPackageType.LINUX_DEB -> { + succeedCommandNames = debCommandNames; + // Simulate "dpkg --print-architecture". + var dpkg = CommandActionSpecs.build() + .stdout("foo-arch") + .exit(CommandMockExit.SUCCEED) + .toCommandMockBuilder().name("dpkg-mock").create(); + mocks.put("dpkg", dpkg); + } + case StandardPackageType.LINUX_RPM -> { + succeedCommandNames = rpmCommandNames; + // Simulate "rpmbuild --version" prints the minimal acceptable version. + var rpmbuild = CommandActionSpecs.build() + .stdout("RPM version 4.10") + .exit(CommandMockExit.SUCCEED) + .toCommandMockBuilder().name("rpmbuild-mock").create(); + mocks.put("rpmbuild", rpmbuild); + } + default -> { + throw new IllegalArgumentException(); + } + } + + script.map(Script.cmdlineStartsWith("ldd"), succeed("ldd-mock")); + + for (var commandName : succeedCommandNames) { + if (!mocks.containsKey(commandName)) { + mocks.put(commandName, succeed(commandName + "-mock")); + } + } + + Stream.of(debCommandNames, rpmCommandNames).flatMap(Set::stream).forEach(commandName -> { + var mock = Optional.ofNullable(mocks.get(commandName)).orElseGet(() -> { + return ioerror(commandName + "-mock"); + }); + script.map(Script.cmdlineStartsWith(commandName), mock); + }); + + return script.createLoop(); + } } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ExecutorTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ExecutorTest.java new file mode 100644 index 00000000000..551ef15e991 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/ExecutorTest.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; +import jdk.jpackage.internal.util.CommandOutputControl.UnexpectedResultException; +import jdk.jpackage.test.mock.CommandActionSpecs; +import jdk.jpackage.test.mock.CommandMockExit; +import jdk.jpackage.test.mock.CompletableCommandMock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class ExecutorTest { + + @ParameterizedTest + @MethodSource + public void test_retryOnKnownErrorMessage(RetryOnKnownErrorMessageTestSpec test) { + test.run(); + } + + private static Stream test_retryOnKnownErrorMessage() { + var data = new ArrayList(); + + final var subject = "French fries"; + + Supplier build = () -> { + return RetryOnKnownErrorMessageTestSpec.build().subject(subject); + }; + + for (var exit : Stream.of(CommandMockExit.values()).filter(CommandMockExit::exitNormally).toList()) { + // These should succeed as there is no "French fries" in stderr. + Stream.of( + build.get().mock(CommandActionSpecs.build().stderr("Coleslaw").exit(exit)), + build.get().mock(CommandActionSpecs.build().stdout(subject).exit(exit)), + build.get() + // Fail in the first attempt (triggering text in the stderr) + .mock(CommandActionSpecs.build().stderr(subject).exit()) + // Fail in the second attempt (same reason) + .repeatLastMoc() + // Pass in the next attempt (no triggering text in the stderr) + .mock(CommandActionSpecs.build().stderr("Coleslaw").exit(exit)), + build.get() + // Fail in the first attempt (triggering text in the stderr) + .mock(CommandActionSpecs.build().stderr(subject)) + // Fail in the second attempt (error running the command) + .mock(CommandActionSpecs.build().exit(CommandMockExit.THROW_MOCK_IO_EXCEPTION)) + // Pass in the next attempt (no triggering text in the stderr) + .mock(CommandActionSpecs.build().exit(exit)) + ).map(RetryOnKnownErrorMessageTestSpec.Builder::success).forEach(data::add); + } + + // These should fail as there is "French fries" in stderr. + data.addAll(List.of( + // Try once and fail. + build.get().mock(CommandActionSpecs.build().stderr(subject).exit()), + // Try twice and fail. + build.get().mock(CommandActionSpecs.build().stderr(subject).exit()).repeatLastMoc() + )); + + return data.stream().map(RetryOnKnownErrorMessageTestSpec.Builder::create); + } + + record RetryOnKnownErrorMessageTestSpec(List mockSpecs, String subject, boolean success) { + + RetryOnKnownErrorMessageTestSpec { + Objects.requireNonNull(mockSpecs); + Objects.requireNonNull(subject); + + if (mockSpecs.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + void run() { + var mock = mockSpecs.stream() + .reduce(CommandActionSpecs::andThen) + .orElseThrow().toCommandMockBuilder() + // Ensure attempts to run the command more times than expected will fail. + .noRepeats().create(); + + var retry = new Executor().toolProvider(mock).retryOnKnownErrorMessage(subject) + .setAttemptTimeout(null) + .setMaxAttemptsCount(mockSpecs.size()); + + if (success) { + assertDoesNotThrow(retry::execute); + } else { + assertThrowsExactly(UnexpectedResultException.class, retry::execute); + } + + assertTrue(((CompletableCommandMock)mock).completed()); + } + + static Builder build() { + return new Builder(); + } + + static final class Builder { + + RetryOnKnownErrorMessageTestSpec create() { + return new RetryOnKnownErrorMessageTestSpec(mockSpecs, subject, success); + } + + public Builder mock(CommandActionSpecs v) { + mockSpecs.add(Objects.requireNonNull(v)); + return this; + } + + public Builder mock(CommandActionSpecs.Builder v) { + return mock(v.create()); + } + + public Builder repeatLastMoc() { + return mock(mockSpecs.getLast()); + } + + public Builder subject(String v) { + subject = v; + return this; + } + + public Builder success(boolean v) { + success = v; + return this; + } + + public Builder success() { + return success(true); + } + + private final List mockSpecs = new ArrayList<>(); + private String subject; + private boolean success; + } + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java new file mode 100644 index 00000000000..e88077a6c9d --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/MockUtils.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2026, 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. + */ + +package jdk.jpackage.internal; + +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + +import java.io.PrintWriter; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import java.util.spi.ToolProvider; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.cli.CliBundlingEnvironment; +import jdk.jpackage.internal.cli.Main; +import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.test.mock.Script; +import jdk.jpackage.test.mock.ToolProviderCommandMock; +import jdk.jpackage.test.mock.VerbatimCommandMock; + +/** + * Bridges "jdk.jpackage.internal" and "jdk.jpackage.test.mock" packages. + */ +public final class MockUtils { + + private MockUtils() { + } + + public static JPackageToolProviderBuilder buildJPackage() { + return new JPackageToolProviderBuilder(); + } + + public static final class JPackageToolProviderBuilder { + + public ToolProvider create() { + return createJPackageToolProvider(os(), createObjectFactory()); + } + + public Consumer createGlobalsMutator() { + var objectFactory = createObjectFactory(); + return globals -> { + globals.objectFactory(objectFactory); + }; + } + + public void applyToGlobals() { + createGlobalsMutator().accept(Globals.instance()); + } + + ExecutorFactory createExecutorFactory() { + var commandMocksExecutorFactory = Optional.ofNullable(script).map(MockUtils::withCommandMocks).map(mapper -> { + return mapper.apply(ExecutorFactory.DEFAULT); + }).orElse(ExecutorFactory.DEFAULT); + + var recordingExecutorFactory = Optional.ofNullable(listener).map(MockUtils::withCommandListener).map(mapper -> { + return mapper.apply(commandMocksExecutorFactory); + }).orElse(commandMocksExecutorFactory); + + return recordingExecutorFactory; + } + + ObjectFactory createObjectFactory() { + var executorFactory = createExecutorFactory(); + if (executorFactory == ExecutorFactory.DEFAULT) { + return ObjectFactory.DEFAULT; + } else { + return ObjectFactory.build().executorFactory(executorFactory).create(); + } + } + + public JPackageToolProviderBuilder listener(Consumer> v) { + listener = v; + return this; + } + + public JPackageToolProviderBuilder script(Script v) { + script = v; + return this; + } + + public JPackageToolProviderBuilder os(OperatingSystem v) { + os = v; + return this; + } + + private OperatingSystem os() { + return Optional.ofNullable(os).orElseGet(OperatingSystem::current); + } + + private Consumer> listener; + private OperatingSystem os; + private Script script; + } + + public static ToolProvider createJPackageToolProvider(OperatingSystem os, Script script) { + return buildJPackage() + .os(Objects.requireNonNull(os)) + .script(Objects.requireNonNull(script)) + .create(); + } + + public static ToolProvider createJPackageToolProvider(Script script) { + return createJPackageToolProvider(OperatingSystem.current(), script); + } + + private static UnaryOperator withCommandListener(Consumer> listener) { + Objects.requireNonNull(listener); + return executorFactory -> { + Objects.requireNonNull(executorFactory); + return () -> { + var executor = executorFactory.executor(); + + Optional> oldMapper = executor.mapper(); + + UnaryOperator newMapper = exec -> { + listener.accept(exec.commandLine()); + return exec; + }; + + return executor.mapper(oldMapper.map(newMapper::compose).orElse(newMapper)::apply); + }; + }; + } + + private static UnaryOperator withCommandMocks(Script script) { + return executorFactory -> { + Objects.requireNonNull(executorFactory); + return () -> { + var executor = executorFactory.executor(); + + Optional> oldMapper = executor.mapper(); + + UnaryOperator newMapper = exec -> { + var commandLine = exec.commandLine(); + var mock = Objects.requireNonNull(script.map(commandLine)); + switch (mock) { + case VerbatimCommandMock.INSTANCE -> { + // No mock for this command line. + return exec; + } + case ToolProviderCommandMock tp -> { + // Create a copy of the executor with the old mapper to prevent further recursion. + var copy = exec.copy().mapper(oldMapper.orElse(null)); + copy.toolProvider(tp); + copy.args().clear(); + copy.args(commandLine.subList(1, commandLine.size())); + return copy; + } + default -> { + // Unreachable because there are no other cases for this switch. + throw ExceptionBox.reachedUnreachable(); + } + } + }; + + return executor.mapper(oldMapper.map(newMapper::compose).orElse(newMapper)::apply); + }; + }; + } + + public static CliBundlingEnvironment createBundlingEnvironment(OperatingSystem os) { + Objects.requireNonNull(os); + + String bundlingEnvironmentClassName; + switch (os) { + case WINDOWS -> { + bundlingEnvironmentClassName = "WinBundlingEnvironment"; + } + case LINUX -> { + bundlingEnvironmentClassName = "LinuxBundlingEnvironment"; + } + case MACOS -> { + bundlingEnvironmentClassName = "MacBundlingEnvironment"; + } + default -> { + throw new IllegalArgumentException(); + } + } + + return toSupplier(() -> { + var ctor = Class.forName(String.join(".", + DefaultBundlingEnvironment.class.getPackageName(), + bundlingEnvironmentClassName + )).getConstructor(); + return (CliBundlingEnvironment)ctor.newInstance(); + }).get(); + } + + static ToolProvider createJPackageToolProvider(OperatingSystem os, ObjectFactory of) { + Objects.requireNonNull(os); + Objects.requireNonNull(of); + + var impl = new Main.Provider(DefaultBundlingEnvironment.runOnce(() -> { + return createBundlingEnvironment(os); + })); + + return new ToolProvider() { + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + return Globals.main(() -> { + Globals.instance().objectFactory(of); + return impl.run(out, err, args); + }); + } + + @Override + public String name() { + return impl.name(); + } + }; + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes index 0e7545bd83d..07757211927 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.excludes @@ -4,7 +4,6 @@ ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, 1.]; errors=[mess ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, 1.b.3]; errors=[message.error-header+[error.version-string-invalid-component, 1.b.3, b.3]]) ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--app-version, ]; errors=[message.error-header+[error.version-string-empty]]) ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --add-modules]; errors=[message.error-header+[error.blocked.option, --add-modules]]) -ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --foo]; errors=[message.error-header+[error.jlink.failed, Error: unknown option: --foo]]) ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --module-path]; errors=[message.error-header+[error.blocked.option, --module-path]]) ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--jlink-options, --output]; errors=[message.error-header+[error.blocked.option, --output]]) ErrorTest.test(IMAGE; app-desc=Hello; args-add=[--main-jar, non-existent.jar]; errors=[message.error-header+[error.main-jar-does-not-exist, non-existent.jar]]) diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java index a85b5015e73..e15d5130d43 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/OptionsValidationFailTest.java @@ -200,13 +200,13 @@ public class OptionsValidationFailTest { Stream.of("--jpt-run=ErrorTest") ).flatMap(x -> x).toArray(String[]::new)).map(dynamicTest -> { return DynamicTest.dynamicTest(dynamicTest.getDisplayName(), () -> { - JPackageCommand.withToolProvider(jpackageToolProviderMock, () -> { + JPackageCommand.withToolProvider(() -> { try { dynamicTest.getExecutable().execute(); } catch (Throwable t) { throw ExceptionBox.toUnchecked(ExceptionBox.unbox(t)); } - }); + }, jpackageToolProviderMock); }); }); } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java index b786f484cb4..4aa3d5f72c1 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/cli/StandardOptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -341,9 +341,33 @@ public class StandardOptionTest extends JUnitAdapter.TestSrcInitializer { Arguments.of("abc", List.of("abc")), Arguments.of("a b c", List.of("a", "b", "c")), Arguments.of("a=10 -Dorg.acme.name='John Smith' c=\\\"foo\\\"", List.of("a=10", "-Dorg.acme.name=John Smith", "c=\"foo\"")), + Arguments.of(" foo \"a b c\" v=' John Smith ' 'H e ll o' ", List.of("foo", "a b c", "v= John Smith ", "H e ll o")), Arguments.of("\"\"", List.of("")), Arguments.of(" ", List.of()), - Arguments.of("", List.of()) + Arguments.of(" ", List.of()), + Arguments.of(" foo ", List.of("foo")), + Arguments.of("", List.of()), + Arguments.of("'fo\"o'\\ buzz \"b a r\"", List.of("fo\"o\\ buzz", "b a r")), + Arguments.of("a\\ 'b\"c'\\ d", List.of("a\\ b\"c\\ d")), + Arguments.of("\"a 'bc' d\"", List.of("a 'bc' d")), + Arguments.of("\'a 'bc' d\'", List.of("a bc d")), + Arguments.of("\"a \\'bc\\' d\"", List.of("a 'bc' d")), + Arguments.of("\'a \\'bc\\' d\'", List.of("a 'bc' d")), + Arguments.of("'a b c' 'd e f'", List.of("a b c", "d e f")), + Arguments.of("'a b c' \"'d e f' h", List.of("a b c", "'d e f' h")), + Arguments.of("'a b c' \"'d e f' \t ", List.of("a b c", "'d e f'")), + Arguments.of(" a='' '' \t '\\'\\'' \"\" \"\\\"\\\"\" ", List.of("a=", "", "\'\'", "", "\"\"")), + Arguments.of("' \'foo '", List.of(" foo", "")), + Arguments.of("' \'foo ' bar", List.of(" foo", " bar")), + Arguments.of("' \'foo\\ '", List.of(" foo\\ ")), + Arguments.of("'fo\"o buzz \"b a r\"", List.of("fo\"o buzz \"b a r\"")), + Arguments.of("'", List.of("")), + Arguments.of("' f g ", List.of(" f g")), + Arguments.of("' f g", List.of(" f g")), + Arguments.of("'\\'", List.of("'")), + Arguments.of("'\\' ", List.of("'")), + Arguments.of("'\\' a ", List.of("' a")), + Arguments.of("\"" + "\\\"".repeat(10000) + "A", List.of("\"".repeat(10000) + "A")) ); } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java new file mode 100644 index 00000000000..f1d8c142eb9 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTest.java @@ -0,0 +1,1846 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.internal.util; + +import static java.util.stream.Collectors.joining; +import static jdk.jpackage.internal.util.CommandOutputControlTestUtils.isInterleave; +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; +import static jdk.jpackage.test.JUnitUtils.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.spi.ToolProvider; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import jdk.internal.util.OperatingSystem; +import jdk.jpackage.internal.util.function.ExceptionBox; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class CommandOutputControlTest { + + @DisabledIf("cherryPickSavedOutputTestCases") + @ParameterizedTest + @MethodSource + public void testSavedOutput(OutputTestSpec spec) { + spec.test(); + } + + /** + * Runs cherry-picked {@link OutputTestSpec} test cases. + *

    + * This test method is mutual exclusive with + * {@link #testSavedOutput(OutputTestSpec)} and is aimed for debugging + * {@code OutputTestSpec} test cases. + *

    + * It is disabled by default. To enable it, manually edit {@link #testSomeSavedOutput()}. + * + * @see #testSomeSavedOutput() + * + * @param spec the test case + */ + @EnabledIf("cherryPickSavedOutputTestCases") + @ParameterizedTest + @MethodSource + public void testSomeSavedOutput(OutputTestSpec spec) { + System.out.println(spec); + spec.test(); + } + + @ParameterizedTest + @MethodSource + public void testDumpStreams(OutputTestSpec spec) { + spec.test(); + } + + @ParameterizedTest + @MethodSource + public void testCharset(CharsetTestSpec spec) throws IOException, InterruptedException { + spec.test(); + } + + @ParameterizedTest + @MethodSource + public void test_description(CommandOutputControlSpec spec) { + // This test is mostly for coverage. + var desc = spec.create().description(); + assertFalse(desc.isBlank()); + } + + @Test + public void test_copy() { + var orig = new CommandOutputControl(); + var copy = orig.copy(); + assertNotSame(orig, copy); + } + + @ParameterizedTest + @EnumSource(names = "SAVE_NOTHING", mode = Mode.EXCLUDE) + public void test_flag(OutputControl flag) { + var coc = new CommandOutputControl(); + assertFalse(flag.get(coc)); + flag.set(coc); + assertTrue(flag.get(coc)); + if (flag.canUnset()) { + flag.unset(coc); + assertFalse(flag.get(coc)); + } + } + + @ParameterizedTest + @MethodSource + public void test_mutual_exclusive_flags(List controls) { + if (controls.isEmpty()) { + throw new IllegalArgumentException(); + } + + var coc = new CommandOutputControl(); + for (var c : controls) { + c.set(coc); + } + + for (var c : controls.subList(0, controls.size() - 1)) { + assertFalse(c.get(coc)); + } + assertTrue(controls.getLast().get(coc)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_ExecutableAttributes(boolean toolProvider) { + var coc = new CommandOutputControl(); + CommandOutputControl.Executable exec; + if (toolProvider) { + exec = coc.createExecutable(new ToolProvider() { + + @Override + public String name() { + return "runme"; + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + fail("Should never be called"); + return 0; + } + + }, "--foo", "--baz=10"); + } else { + exec = coc.createExecutable(new ProcessBuilder("runme", "--foo", "--baz=10")); + } + + assertEquals("runme --foo --baz=10", exec.attributes().toString()); + } + + @Test + public void test_Result_no_args_ctor() { + var result = new CommandOutputControl.Result(7); + assertFalse(result.findContent().isPresent()); + assertFalse(result.findStdout().isPresent()); + assertFalse(result.findStderr().isPresent()); + assertEquals(7, result.getExitCode()); + assertSame(Objects.requireNonNull(CommandOutputControl.EMPTY_EXECUTABLE_ATTRIBUTES), result.execAttrs()); + } + + @Test + public void test_Result_expectExitCode() throws IOException { + var result = new CommandOutputControl.Result(7); + + assertSame(result, result.expectExitCode(7)); + assertSame(result, result.expectExitCode(7, 2)); + assertSame(result, result.expectExitCode(2, 7)); + + assertSame(result, result.expectExitCode(List.of(7))); + assertSame(result, result.expectExitCode(Set.of(7, 2))); + assertSame(result, result.expectExitCode(List.of(2, 7))); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_Result_expectExitCode_negative(boolean collection) { + var result = new CommandOutputControl.Result(3); + + var ex = assertThrowsExactly(CommandOutputControl.UnexpectedExitCodeException.class, () -> { + if (collection) { + result.expectExitCode(List.of(17, 12)); + } else { + result.expectExitCode(17, 12); + } + }); + + assertNull(ex.getCause()); + assertSame(result, ex.getResult()); + assertEquals("Unexpected exit code 3 from executing the command ", ex.getMessage()); + } + + @ParameterizedTest + @MethodSource + public void test_Result_toCharacterResult(ToCharacterResultTestSpec spec) throws IOException, InterruptedException { + spec.test(); + } + + @Test + public void test_Result_toCharacterResult_nop() throws IOException, InterruptedException { + + var charset = StandardCharsets.UTF_8; + + var emptyResult = new CommandOutputControl.Result(7); + assertSame(emptyResult, emptyResult.toCharacterResult(charset, true)); + assertSame(emptyResult, emptyResult.toCharacterResult(charset, false)); + + var coc = new CommandOutputControl().saveOutput(true); + + var result = coc.createExecutable(new Command(List.of("foo"), List.of()).asToolProvider()).execute(); + + assertSame(result, result.toCharacterResult(charset, true)); + assertSame(result, result.toCharacterResult(charset, false)); + } + + @Test + public void test_Result_toCharacterResult_copyWithExecutableAttributes() { + + var empty = new CommandOutputControl.Result(0); + + var execAttrs = new CommandOutputControl.ExecutableAttributes() { + @Override + public String toString() { + return "foo"; + } + + @Override + public List commandLine() { + return List.of(); + } + }; + + var copy = empty.copyWithExecutableAttributes(execAttrs); + + assertSame(empty.exitCode(), copy.exitCode()); + assertSame(empty.output(), copy.output()); + assertSame(empty.byteOutput(), copy.byteOutput()); + assertSame(execAttrs, copy.execAttrs()); + } + + @ParameterizedTest + @EnumSource(ExecutableType.class) + public void test_timeout_expires(ExecutableType mode) throws InterruptedException, IOException { + + final var toolProvider = (mode == ExecutableType.TOOL_PROVIDER); + final var storeOutputInFiles = (mode == ExecutableType.PROCESS_BUILDER_WITH_STREAMS_IN_FILES); + + var actions = List.of( + CommandAction.echoStdout("The quick brown fox jumps"), + CommandAction.sleep(5), + CommandAction.echoStdout("over the lazy dog") + ); + + var coc = new CommandOutputControl().saveOutput(true).dumpOutput(true).storeOutputInFiles(storeOutputInFiles); + + CommandOutputControl.Executable exec; + + InterruptibleToolProvider tp; + + if (toolProvider) { + tp = new InterruptibleToolProvider(Command.createToolProvider(actions)); + exec = coc.createExecutable(tp); + } else { + var cmdline = Command.createShellCommandLine(actions); + tp = null; + exec = coc.createExecutable(new ProcessBuilder(cmdline)); + } + + var result = exec.execute(1, TimeUnit.SECONDS); + assertFalse(result.exitCode().isPresent()); + + var getExitCodeEx = assertThrowsExactly(IllegalStateException.class, result::getExitCode); + assertEquals(("Exit code is unavailable for timed-out command"), getExitCodeEx.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. + // This happens when the test case is executed together with other test cases + // and never when it is executed individually. + if (!toolProvider || !result.content().isEmpty()) { + assertEquals(List.of("The quick brown fox jumps"), result.content()); + } + + if (toolProvider) { + assertTrue(tp.interrupted()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_timeout(boolean toolProvider) throws InterruptedException, IOException { + + var actions = List.of( + CommandAction.echoStdout("Sphinx of black quartz,"), + CommandAction.echoStdout("judge my vow") + ); + + var coc = new CommandOutputControl().saveOutput(true).dumpOutput(true); + + CommandOutputControl.Executable exec; + + if (toolProvider) { + var tp = Command.createToolProvider(actions); + exec = coc.createExecutable(tp); + } else { + var cmdline = Command.createShellCommandLine(actions); + exec = coc.createExecutable(new ProcessBuilder(cmdline)); + } + + var result = exec.execute(10, TimeUnit.SECONDS); + assertTrue(result.exitCode().isPresent()); + assertEquals(List.of("Sphinx of black quartz,", "judge my vow"), result.content()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_passthrough_exceptions(boolean withTimeout) throws IOException { + + var expected = new RuntimeException("Kaput!"); + + var exec = new CommandOutputControl().createExecutable(new ToolProvider() { + + @Override + public String name() { + return "foo"; + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + throw expected; + } + }); + + var actual = assertThrowsExactly(expected.getClass(), () -> { + if (withTimeout) { + exec.execute(10, TimeUnit.SECONDS); + } else { + exec.execute(); + } + }); + + assertSame(expected, actual); + } + + @Test + public void test_externally_terminated() throws InterruptedException, IOException { + var cmdline = Command.createShellCommandLine(List.of( + CommandAction.echoStderr("The five boxing wizards"), + CommandAction.sleep(10), + CommandAction.echoStderr("jump quickly") + )); + + var processDestroyer = Slot.>createEmpty(); + + var coc = new CommandOutputControl().saveOutput(true).dumpOutput(true).processListener(process -> { + // Once we are notified the process has been started, schedule its destruction. + // Give it a second to warm up and print some output and then destroy it. + processDestroyer.set(CompletableFuture.runAsync(toRunnable(() -> { + Thread.sleep(Duration.ofSeconds(1)); + // On Windows, CommandAction#sleep is implemented with the "ping" command. + // By some reason, when the parent "cmd" process is destroyed, + // the child "ping" command stays alive, and the test waits when it completes, + // making it last for at least 10 seconds. + // To optimize the test work time, destroy the entire subprocess tree. + // Even though this is essential on Windows keep this logic on all platforms for simplicity. + var descendants = List.of(); + try (var descendantsStream = process.descendants()) { + descendants = descendantsStream.toList(); + } finally { + process.destroyForcibly(); + } + descendants.forEach(ProcessHandle::destroyForcibly); + }))); + }); + var exec = coc.createExecutable(new ProcessBuilder(cmdline)); + + var result = exec.execute(); + assertNotEquals(0, result.getExitCode()); + assertEquals(List.of("The five boxing wizards"), result.content()); + processDestroyer.get().join(); + } + + @DisabledOnOs(value = OS.MAC, disabledReason = "Closing a stream doesn't consistently cause a trouble as it should") + @ParameterizedTest + @EnumSource(OutputStreams.class) + public void test_close_streams(OutputStreams action) throws InterruptedException, IOException { + var cmdline = Command.createShellCommandLine(List.of( + CommandAction.echoStdout("Hello stdout"), + CommandAction.echoStderr("Bye stderr") + )); + + var coc = new CommandOutputControl().saveOutput(true).dumpOutput(true).processListener(toConsumer(process -> { + // Close process output stream(s). This should make corresponding stream gobbler(s) throw IOException. + switch (action) { + case STDOUT -> { + process.getInputStream().close(); + } + case STDERR -> { + process.getErrorStream().close(); + } + case STDOUT_AND_STDERR -> { + process.getInputStream().close(); + process.getErrorStream().close(); + } + } + })); + var exec = coc.createExecutable(new ProcessBuilder(cmdline)); + + var ex = assertThrows(IOException.class, exec::execute); + System.out.println("test_close_streams: " + action); + ex.printStackTrace(System.out); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_interleaved(boolean customDumpStreams) throws IOException, InterruptedException { + var cmdline = Command.createShellCommandLine(List.of( + CommandAction.echoStdout("Eat some more"), + CommandAction.echoStderr("of these"), + CommandAction.echoStdout("soft French pastries"), + CommandAction.echoStderr("and drink some tea") + )); + + var coc = new CommandOutputControl(); + var exec = coc.createExecutable(new ProcessBuilder(cmdline)); + + coc.saveOutput(true).dumpOutput(true); + + CommandOutputControl.Result result; + + if (customDumpStreams) { + // Execute the command so that its stdout and stderr are dumped to the same sink. + var sink = new ByteArrayOutputStream(); + var ps = new PrintStream(sink); + + coc.dumpStdout(ps).dumpStderr(ps); + + result = exec.execute(); + + var commandStdout = List.of("Eat some more", "soft French pastries"); + var commandStderr = List.of("of these", "and drink some tea"); + + var sinkContent = toStringList(sink.toByteArray(), StandardCharsets.US_ASCII); + + if (!isInterleave(sinkContent, commandStdout, commandStderr)) { + fail(String.format("Unexpected combined output=%s; stdout=%s; stderr=%s", + sinkContent, commandStdout, commandStderr)); + } + + // CommandOutputControl was not configured to redirect stderr in stdout, + // hence the output is ordered: stdout goes first, stderr follows. + assertEquals(Stream.of(commandStdout, commandStderr).flatMap(List::stream).toList(), result.content()); + + // Saved stdout an stderr can be accessed individually. + assertEquals(commandStdout, result.stdout()); + assertEquals(commandStderr, result.stderr()); + } else { + // Execute the command so that its stdout and stderr are dumped into System.out. + coc.redirectStderr(true); + result = exec.execute(); + + // CommandOutputControl was configured to redirect stderr in stdout, + // hence the output is interleaved. + assertEquals(List.of("Eat some more", "of these", "soft French pastries", "and drink some tea"), result.content()); + + // Saved stdout an stderr can NOT be accessed individually because they are interleaved. + assertTrue(result.findStdout().isEmpty()); + assertTrue(result.findStderr().isEmpty()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true}) + public void stressTest(boolean binaryOutput, @TempDir Path workDir) throws Exception { + + // Execute multiple subprocesses asynchronously. + // Each subprocess writes a few chunks of data each larger than the default buffer size (8192 bytes) + + final var chunkCount = 5; + final var subprocessCount = 100; + final var subprocessExecutor = Executors.newVirtualThreadPerTaskExecutor(); + + final var md = MessageDigest.getInstance("MD5"); + + var cmdline = Command.createShellCommandLine(IntStream.range(0, chunkCount).mapToObj(chunk -> { + byte[] bytes = new byte[10 * 1024]; // 10K to exceed the default BufferedOutputStream's buffer size of 8192. + new Random().nextBytes(bytes); + md.update(bytes); + var path = workDir.resolve(Integer.toString(chunk)); + try { + Files.write(path, bytes); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return path; + }).map(CommandAction::cat).toList()); + + final var digest = HexFormat.of().formatHex(md.digest()); + + // Schedule to start every subprocess in a separate virtual thread. + // Start and suspend threads, waiting until all scheduled threads have started. + // After all scheduled threads start, resume them. + // This should result in starting all scheduled subprocesses simultaneously. + + var readyLatch = new CountDownLatch(subprocessCount); + var startLatch = new CountDownLatch(1); + + var futures = IntStream.range(0, subprocessCount).mapToObj(_ -> { + return CompletableFuture.supplyAsync(toSupplier(() -> { + + var exec = new CommandOutputControl() + .saveOutput(true) + .binaryOutput(binaryOutput) + .createExecutable(new ProcessBuilder(cmdline)); + + readyLatch.countDown(); + startLatch.await(); + + var result = exec.execute(); + + var localMd = MessageDigest.getInstance("MD5"); + localMd.update(result.byteContent()); + + return HexFormat.of().formatHex(localMd.digest()); + + }), subprocessExecutor); + }).toList(); + + readyLatch.await(); + startLatch.countDown(); + + CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join(); + + futures.forEach(future -> { + var actualDigest = future.join(); + assertEquals(digest, actualDigest); + }); + } + + public enum OutputStreams { + STDOUT, + STDERR, + STDOUT_AND_STDERR + } + + private static List test_description() { + List testCases = new ArrayList<>(); + testCases.add(new CommandOutputControlSpec(Set.of())); + for (var outputControl : OutputControl.variants()) { + testCases.add(new CommandOutputControlSpec(outputControl)); + } + return testCases; + } + + private static List> test_mutual_exclusive_flags() { + List> data = new ArrayList<>(); + + var flags = List.of(OutputControl.SAVE_ALL, OutputControl.SAVE_FIRST_LINE, OutputControl.SAVE_NOTHING); + + List seq = new ArrayList<>(); + for (var _1 : flags) { + seq.add(_1); + var flags2 = flags.stream().filter(Predicate.isEqual(_1).negate()).toList(); + for (var _2 : flags2) { + seq.add(_2); + var flags3 = flags2.stream().filter(Predicate.isEqual(_2).negate()).toList(); + for (var _3 : flags3) { + seq.add(_3); + data.add(List.copyOf(seq)); + seq.removeLast(); + } + seq.removeLast(); + } + seq.removeLast(); + } + + return data; + } + + public record ToCharacterResultTestSpec(OutputTestSpec execSpec, boolean keepByteContent) { + + public ToCharacterResultTestSpec { + Objects.requireNonNull(execSpec); + } + + @Override + public String toString() { + final List tokens = new ArrayList<>(); + + tokens.add(execSpec.toString()); + if (keepByteContent) { + tokens.add("keepByteContent"); + } + + return String.join(", ", tokens.toArray(String[]::new)); + } + + void test() throws IOException, InterruptedException { + var coc = execSpec.cocSpec().create(); + + var command = execSpec.commandSpec().command().asToolProvider(); + + var expected = coc.binaryOutput(false).createExecutable(command).execute(); + + var byteResult = coc.binaryOutput(true).createExecutable(command).execute(); + + var actual = byteResult.toCharacterResult(coc.charset(), keepByteContent); + + CommandOutputControl.Result expectedByteContent; + if (keepByteContent) { + expectedByteContent = byteResult; + } else { + expectedByteContent = expected; + } + + assertArrayEquals(expectedByteContent.findByteContent().orElse(null), actual.findByteContent().orElse(null)); + assertArrayEquals(expectedByteContent.findByteStdout().orElse(null), actual.findByteStdout().orElse(null)); + assertArrayEquals(expectedByteContent.findByteStderr().orElse(null), actual.findByteStderr().orElse(null)); + + assertEquals(expected.findContent(), actual.findContent()); + assertEquals(expected.findStdout(), actual.findStdout()); + assertEquals(expected.findStderr(), actual.findStderr()); + + assertSame(byteResult.execAttrs(), actual.execAttrs()); + assertEquals(expected.exitCode(), actual.exitCode()); + } + } + + private static Stream test_Result_toCharacterResult() { + List testCases = new ArrayList<>(); + + var skip = Set.of(OutputControl.BINARY_OUTPUT, OutputControl.DUMP, OutputControl.SAVE_FIRST_LINE); + + for (var outputControl : OutputControl.variants().stream().filter(spec -> { + return !skip.stream().anyMatch(spec::contains); + }).toList()) { + for (var stdoutContent : List.of(OutputData.EMPTY, OutputData.MANY)) { + for (var stderrContent : List.of(OutputData.EMPTY, OutputData.MANY)) { + var commandSpec = new CommandSpec(stdoutContent, stderrContent); + testCases.add(new OutputTestSpec(false, new CommandOutputControlSpec(outputControl), commandSpec)); + } + } + } + + return testCases.stream().flatMap(execSpec -> { + return Stream.of(true, false).map(keepByteContent -> { + return new ToCharacterResultTestSpec(execSpec, keepByteContent); + }); + }); + } + + private static boolean cherryPickSavedOutputTestCases() { + return !testSomeSavedOutput().isEmpty(); + } + + /** + * Returns test cases for {@link #testSomeSavedOutput(OutputTestSpec)}. + *

    + * Aimed to simplify debugging of {@link #OutputTestSpec} test cases. + *

    + * The total number of {@code #OutputTestSpec} test cases is ~1500. When some + * fail and need debugging, it is a waste of time to run them all. This method + * allows running only selected test cases. It works this way: + *

      + *
    • Run CommandOutputControlTest test. + *
    • If some {@linke #testSavedOutput(OutputTestSpec)} invocations fail, + * capture their IDs (test case ID is an index starting from 1). + *
    • Replace "/* 10, 67, 456 */" comment in the body of this method with + * the captured test case IDs. + *
    • Rerun CommandOutputControlTest test. This time, it will run + * {@link #testSomeSavedOutput(OutputTestSpec)} method instead of + * {@link #testSavedOutput(OutputTestSpec)} with the list of the captured test + * case IDs. + *
    + */ + private static List testSomeSavedOutput() { + var testIds = List.of(/* 10, 67, 456 */); + if (testIds.isEmpty()) { + return List.of(); + } else { + var allTestCases = testSavedOutput(); + return testIds.stream().map(testId -> { + return allTestCases.get(testId - 1); + }).toList(); + } + } + + private static List testSavedOutput() { + List testCases = new ArrayList<>(); + for (final var executableType : List.of(ExecutableType.values())) { + for (var outputControl : OutputControl.variants()) { + for (final var stdoutContent : List.of(OutputData.values())) { + for (final var stderrContent : List.of(OutputData.values())) { + + if (outputControl.contains(OutputControl.BINARY_OUTPUT) + && (stdoutContent == OutputData.ONE_LINE || stderrContent == OutputData.ONE_LINE)) { + // Skip a test case if it runs a command writing + // a single line in stdout or stderr, and handles command output as a byte stream. + // It duplicates test cases that write multiple lines in stdout or stderr. + continue; + } + + final var commandSpec = new CommandSpec(stdoutContent, stderrContent); + boolean toolProvider; + switch (executableType) { + case PROCESS_BUILDER -> { + toolProvider = false; + } + case PROCESS_BUILDER_WITH_STREAMS_IN_FILES -> { + outputControl = new SetBuilder() + .add(outputControl) + .add(OutputControl.STORE_STREAMS_IN_FILES) + .create(); + toolProvider = false; + } + case TOOL_PROVIDER -> { + toolProvider = true; + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + testCases.add(new OutputTestSpec( + toolProvider, + new CommandOutputControlSpec(outputControl), + commandSpec)); + } + } + } + } + return testCases; + } + + private static List testDumpStreams() { + List testCases = new ArrayList<>(); + final var commandSpec = new CommandSpec(OutputData.MANY, OutputData.MANY); + for (var discardStdout : withAndWithout(OutputControl.DISCARD_STDOUT)) { + for (var discardStderr : withAndWithout(OutputControl.DISCARD_STDERR)) { + for (var redirectStderr : withAndWithout(OutputControl.REDIRECT_STDERR)) { + for (var binaryOutput : withAndWithout(OutputControl.BINARY_OUTPUT)) { + for (var dumpStdout : withAndWithout(OutputControl.DUMP_STDOUT_IN_SYSTEM_OUT)) { + for (var dumpStderr : withAndWithout(OutputControl.DUMP_STDERR_IN_SYSTEM_ERR)) { + + if (dumpStderr.isEmpty() && dumpStdout.isEmpty()) { + // Output dumping disabled + continue; + } + + if (discardStderr.isPresent() && discardStdout.isPresent()) { + // Output dumping enabled, but all stream discarded + continue; + } + + if (dumpStderr.isPresent() == discardStderr.isPresent() && dumpStdout.isEmpty()) { + // Stderr dumping enabled but discarded, stdout dumping disabled + continue; + } + + if (dumpStdout.isPresent() == discardStdout.isPresent() && dumpStderr.isEmpty()) { + // Stdout dumping enabled but discarded, stderr dumping disabled + continue; + } + + final var outputControl = new HashSet(); + outputControl.add(OutputControl.DUMP); + discardStdout.ifPresent(outputControl::add); + discardStderr.ifPresent(outputControl::add); + redirectStderr.ifPresent(outputControl::add); + binaryOutput.ifPresent(outputControl::add); + dumpStdout.ifPresent(outputControl::add); + dumpStderr.ifPresent(outputControl::add); + + testCases.add(new OutputTestSpec( + false, + new CommandOutputControlSpec(outputControl), + commandSpec)); + } + } + } + } + } + } + return testCases; + } + + private static List testCharset() { + List testCases = new ArrayList<>(); + + for (boolean toolProvider : BOOLEAN_VALUES) { + for (var redirectStderr : withAndWithout(OutputControl.REDIRECT_STDERR)) { + for (var charset : withAndWithout(OutputControl.CHARSET_UTF16LE)) { + var stdoutSink = new CharsetTestSpec.DumpOutputSink(StandardCharsets.US_ASCII, OutputStreams.STDOUT); + var stderrSink = new CharsetTestSpec.DumpOutputSink(StandardCharsets.UTF_32LE, OutputStreams.STDERR); + var outputControl = new HashSet(); + redirectStderr.ifPresent(outputControl::add); + charset.ifPresent(outputControl::add); + outputControl.add(stdoutSink); + outputControl.add(stderrSink); + testCases.add(new CharsetTestSpec(toolProvider, new CommandOutputControlSpec(outputControl))); + } + } + } + + return testCases; + } + + private enum ExecutableType { + TOOL_PROVIDER, + PROCESS_BUILDER, + PROCESS_BUILDER_WITH_STREAMS_IN_FILES, + ; + } + + private sealed interface CommandAction { + static SleepCommandAction sleep(int seconds) { + return new SleepCommandAction(seconds); + } + + static EchoCommandAction echoStdout(String str) { + return new EchoCommandAction(str, false); + } + + static EchoCommandAction echoStderr(String str) { + return new EchoCommandAction(str, true); + } + + static WriteCommandAction writeStdout(byte[] binary) { + return new WriteCommandAction(binary, false); + } + + static WriteCommandAction writeStderr(byte[] binary) { + return new WriteCommandAction(binary, true); + } + + static CatCommandAction cat(Path file) { + return new CatCommandAction(file); + } + } + + private record EchoCommandAction(String value, boolean stderr) implements CommandAction { + EchoCommandAction { + Objects.requireNonNull(value); + } + } + + private record WriteCommandAction(byte[] value, boolean stderr) implements CommandAction { + WriteCommandAction { + Objects.requireNonNull(value); + } + } + + private record CatCommandAction(Path file) implements CommandAction { + CatCommandAction { + Objects.requireNonNull(file); + } + } + + private record SleepCommandAction(int seconds) implements CommandAction { + SleepCommandAction { + if (seconds < 0) { + throw new IllegalArgumentException(); + } + } + } + + private record Command(List stdout, List stderr) { + Command { + stdout.forEach(Objects::requireNonNull); + stderr.forEach(Objects::requireNonNull); + } + + List asExecutable() { + return createShellCommandLine(actions()); + } + + ToolProvider asToolProvider() { + return createToolProvider(actions()); + } + + // + // Type of shell for which to create a command line. + // On Unix it is always the "sh". + // On Windows, it is "cmd" by default and "powershell" when a command needs to write binary data to output stream(s). + // Extra complexity on Windows is because "powershell" is times slower than "cmd", + // and the latter doesn't support binary output. + // + private enum ShellType { + SH(OperatingSystem.LINUX, OperatingSystem.MACOS), + CMD(OperatingSystem.WINDOWS), + POWERSHELL(OperatingSystem.WINDOWS), + ; + + ShellType(OperatingSystem... os) { + if (os.length == 0) { + throw new IllegalArgumentException(); + } + this.os = Set.of(os); + } + + boolean isSupportedOnCurrentOS() { + return os.contains(OperatingSystem.current()); + } + + private final Set os; + } + + private List actions() { + return Stream.concat( + stdout.stream().map(CommandAction::echoStdout), + stderr.stream().map(CommandAction::echoStderr) + ).toList(); + } + + static List createShellCommandLine(List actions) { + final var shellType = detectShellType(actions); + final List commandline = new ArrayList<>(); + final String commandSeparator; + switch (shellType) { + case SH -> { + commandline.addAll(List.of("sh", "-c")); + commandSeparator = " && "; + } + case CMD -> { + commandline.addAll(List.of("cmd", "/C")); + commandSeparator = " && "; + } + case POWERSHELL -> { + commandline.addAll(List.of("powershell", "-NoProfile", "-Command")); + commandSeparator = "; "; + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + commandline.add(actions.stream().map(action -> { + return Command.toString(action, shellType); + }).collect(joining(commandSeparator))); + return commandline; + } + + static ToolProvider createToolProvider(List actions) { + var copiedActions = List.copyOf(actions); + return new ToolProvider() { + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + throw new UnsupportedOperationException(); + } + + @Override + public int run(PrintStream out, PrintStream err, String... args) { + for (var action : copiedActions) { + switch (action) { + case EchoCommandAction echo -> { + if (echo.stderr()) { + err.println(echo.value()); + } else { + out.println(echo.value()); + } + } + case WriteCommandAction write -> { + try { + if (write.stderr()) { + err.write(write.value()); + } else { + out.write(write.value()); + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + case SleepCommandAction sleep -> { + toRunnable(() -> { + synchronized (this) { + var millis = Duration.ofSeconds(sleep.seconds()).toMillis(); + this.wait(millis); + } + }).run(); + } + case CatCommandAction _ -> { + // Not used, no point to implement. + throw new UnsupportedOperationException(); + } + } + } + return 0; + } + + @Override + public String name() { + return "test"; + } + }; + } + + private static ShellType detectShellType(List actions) { + var supportedShellTypes = Stream.of(ShellType.values()) + .filter(ShellType::isSupportedOnCurrentOS) + .collect(Collectors.toCollection(HashSet::new)); + for (var action : actions) { + if (action instanceof WriteCommandAction) { + supportedShellTypes.remove(ShellType.CMD); + } + } + return supportedShellTypes.stream() + .sorted(Comparator.comparingInt(Enum::ordinal)) + .findFirst().orElseThrow(); + } + + private static String toString(CommandAction action, ShellType shellType) { + switch (action) { + case EchoCommandAction a -> { + return toString(a, shellType); + } + case WriteCommandAction a -> { + return toString(a, shellType); + } + case SleepCommandAction a -> { + return toString(a, shellType); + } + case CatCommandAction a -> { + return toString(a, shellType); + } + } + } + + private static String toString(EchoCommandAction echo, ShellType shellType) { + String str; + switch (shellType) { + case SH -> { + str = "echo " + echo.value(); + if (echo.stderr()) { + str += ">&2"; + } + } + case CMD -> { + str = "(echo " + echo.value() + ")"; + if (echo.stderr()) { + str += ">&2"; + } + } + case POWERSHELL -> { + str = String.format("[Console]::%s.WriteLine(\\\"%s\\\")", + echo.stderr() ? "Error" : "Out", echo.value()); + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + return str; + } + + private static String toString(WriteCommandAction write, ShellType shellType) { + String str; + switch (shellType) { + case SH -> { + // Convert byte[] to octal string to make it work with POSIX printf. + // POSIX printf doesn't recognize hex strings, so can't use handy HexFormat. + var sb = new StringBuilder(); + sb.append("printf "); + for (var b : write.value()) { + sb.append("\\\\").append(Integer.toOctalString(b & 0xFF)); + } + if (write.stderr()) { + sb.append(">&2"); + } + str = sb.toString(); + } + case CMD -> { + throw new UnsupportedOperationException("Can't output binary data with 'cmd'"); + } + case POWERSHELL -> { + var base64 = Base64.getEncoder().encodeToString(write.value()); + str = String.format( + "$base64 = '%s'; " + + "$bytes = [Convert]::FromBase64String($base64); " + + "[Console]::%s().Write($bytes, 0, $bytes.Length)", + base64, write.stderr() ? "OpenStandardError" : "OpenStandardOutput"); + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + return str; + } + + private static String toString(SleepCommandAction sleep, ShellType shellType) { + switch (shellType) { + case SH -> { + return "sleep " + sleep.seconds(); + } + case CMD -> { + // The standard way to sleep in "cmd" is to use the "ping" command. + // It sends packets every second. + // To wait N seconds, it should send N+1 packets. + // The "timeout" command works only in a console. + return String.format("(ping -n %d localhost > nul)", sleep.seconds() + 1); + } + case POWERSHELL -> { + return "Start-Sleep -Seconds " + sleep.seconds(); + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + } + + private static String toString(CatCommandAction cat, ShellType shellType) { + switch (shellType) { + case SH -> { + return "cat " + cat.file(); + } + case CMD -> { + return "type " + cat.file(); + } + case POWERSHELL -> { + // Not used, no point to implement. + throw new UnsupportedOperationException(); + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + } + + } + + private enum OutputData { + EMPTY(List.of()), + ONE_LINE(List.of("Jupiter")), + MANY(List.of("Uranus", "Saturn", "Earth")); + + OutputData(List data) { + data.forEach(Objects::requireNonNull); + this.data = data; + } + + final List data; + } + + private record CommandSpec(OutputData stdout, OutputData stderr) { + CommandSpec { + Objects.requireNonNull(stdout); + Objects.requireNonNull(stderr); + } + + @Override + public String toString() { + return String.format("[stdout=%s, stderr=%s]", stdout, stderr); + } + + Command command() { + return new Command(stdout.data.stream().map(line -> { + return "stdout." + line; + }).toList(), stderr.data.stream().map(line -> { + return "stderr." + line; + }).toList()); + } + } + + public interface CommandOutputControlMutator { + String name(); + void mutate(CommandOutputControl coc); + + static Function> addToSet(Set set) { + return m -> { + return new SetBuilder().add(set).add(m).create(); + }; + } + } + + public enum OutputControl implements CommandOutputControlMutator { + DUMP(CommandOutputControl::dumpOutput, CommandOutputControl::isDumpOutput), + SAVE_ALL(CommandOutputControl::saveOutput, CommandOutputControl::isSaveOutput), + SAVE_FIRST_LINE(CommandOutputControl::saveFirstLineOfOutput, CommandOutputControl::isSaveFirstLineOfOutput), + SAVE_NOTHING(coc -> { + coc.saveOutput(false); + }, coc -> { + return !coc.isSaveOutput() && !coc.isSaveFirstLineOfOutput(); + }), + DISCARD_STDOUT(CommandOutputControl::discardStdout, CommandOutputControl::isDiscardStdout), + DISCARD_STDERR(CommandOutputControl::discardStderr, CommandOutputControl::isDiscardStderr), + REDIRECT_STDERR(CommandOutputControl::redirectStderr, CommandOutputControl::isRedirectStderr), + STORE_STREAMS_IN_FILES(CommandOutputControl::storeOutputInFiles, CommandOutputControl::isStoreOutputInFiles), + BINARY_OUTPUT(CommandOutputControl::binaryOutput, CommandOutputControl::isBinaryOutput), + DUMP_STDOUT_IN_SYSTEM_OUT(coc -> { + coc.dumpStdout(new PrintStreamWrapper(System.out)); + }, coc -> { + return coc.dumpStdout() instanceof PrintStreamWrapper; + }), + DUMP_STDERR_IN_SYSTEM_ERR(coc -> { + coc.dumpStderr(new PrintStreamWrapper(System.err)); + }, coc -> { + return coc.dumpStderr() instanceof PrintStreamWrapper; + }), + CHARSET_UTF16LE(coc -> { + coc.charset(StandardCharsets.UTF_16LE); + }, coc -> { + return coc.charset() == StandardCharsets.UTF_16LE; + }), + ; + + OutputControl(Consumer setter, Function getter) { + this.setter = Objects.requireNonNull(setter); + this.unsetter = null; + this.getter = Objects.requireNonNull(getter); + } + + OutputControl(BiConsumer setter, Function getter) { + Objects.requireNonNull(setter); + this.setter = coc -> { + setter.accept(coc, true); + }; + this.unsetter = coc -> { + setter.accept(coc, false); + }; + this.getter = Objects.requireNonNull(getter); + } + + @Override + public void mutate(CommandOutputControl coc) { + set(coc); + } + + CommandOutputControl set(CommandOutputControl coc) { + setter.accept(coc); + return coc; + } + + CommandOutputControl unset(CommandOutputControl coc) { + Objects.requireNonNull(unsetter).accept(coc); + return coc; + } + + boolean canUnset() { + return unsetter != null; + } + + boolean get(CommandOutputControl coc) { + return getter.apply(coc); + } + + static List> variants() { + final List> variants = new ArrayList<>(); + for (final var binaryOutput : withAndWithout(BINARY_OUTPUT)) { + for (final var redirectStderr : withAndWithout(REDIRECT_STDERR)) { + for (final var withDump : withAndWithout(DUMP)) { + variants.addAll(Stream.of( + Set.of(), + Set.of(SAVE_ALL), + Set.of(SAVE_FIRST_LINE), + Set.of(DISCARD_STDOUT), + Set.of(DISCARD_STDERR), + Set.of(SAVE_ALL, DISCARD_STDOUT), + Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT), + Set.of(SAVE_ALL, DISCARD_STDERR), + Set.of(SAVE_FIRST_LINE, DISCARD_STDERR), + Set.of(SAVE_ALL, DISCARD_STDOUT, DISCARD_STDERR), + Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT, DISCARD_STDERR) + ).map(v -> { + return withDump.map(CommandOutputControlMutator.addToSet(v)).orElse(v); + }).map(v -> { + return redirectStderr.filter(_ -> { + return !v.containsAll(List.of(DISCARD_STDOUT, DISCARD_STDERR)); + }).map(CommandOutputControlMutator.addToSet(v)).orElse(v); + }).map(v -> { + return binaryOutput.map(CommandOutputControlMutator.addToSet(v)).orElse(v); + }).toList()); + } + } + } + return variants.stream().distinct().toList(); + } + + private static final class PrintStreamWrapper extends PrintStream { + PrintStreamWrapper(PrintStream out) { + super(out, true); + } + } + + private final Consumer setter; + private final Consumer unsetter; + private final Function getter; + + static final Set SAVE = Set.of(SAVE_ALL, SAVE_FIRST_LINE); + } + + public record CommandOutputControlSpec(Set outputControl) { + public CommandOutputControlSpec { + outputControl.forEach(Objects::requireNonNull); + if (outputControl.containsAll(OutputControl.SAVE)) { + throw new IllegalArgumentException(); + } + } + + @Override + public String toString() { + return outputControl.stream().map(CommandOutputControlMutator::name).sorted().collect(joining("+")); + } + + boolean contains(OutputControl v) { + return outputControl.contains(Objects.requireNonNull(v)); + } + + boolean dumpOutput() { + return contains(OutputControl.DUMP); + } + + boolean saveOutput() { + return !Collections.disjoint(outputControl, OutputControl.SAVE); + } + + boolean discardStdout() { + return contains(OutputControl.DISCARD_STDOUT); + } + + boolean discardStderr() { + return contains(OutputControl.DISCARD_STDERR); + } + + boolean redirectStderr() { + return contains(OutputControl.REDIRECT_STDERR); + } + + CommandOutputControl create() { + final CommandOutputControl coc = new CommandOutputControl(); + outputControl.forEach(control -> control.mutate(coc)); + return coc; + } + } + + public record OutputTestSpec(boolean toolProvider, CommandOutputControlSpec cocSpec, CommandSpec commandSpec) { + public OutputTestSpec { + Objects.requireNonNull(cocSpec); + Objects.requireNonNull(commandSpec); + } + + @Override + public String toString() { + final List tokens = new ArrayList<>(); + + if (toolProvider) { + tokens.add("tool-provider"); + } + + tokens.add("output=" + cocSpec.toString()); + tokens.add("command=" + commandSpec); + + return String.join(", ", tokens.toArray(String[]::new)); + } + + void test() { + final var command = commandSpec.command(); + + final Slot result = Slot.createEmpty(); + final var dumpCapture = DumpCapture.captureDump(toRunnable(() -> { + result.set(createExecutable(command).execute()); + })); + + assertEquals(0, result.get().getExitCode()); + + verifyDump(dumpCapture, command); + if (contains(OutputControl.BINARY_OUTPUT)) { + verifyByteResultContent(result.get(), command, StandardCharsets.UTF_8); + } else { + verifyResultContent(result.get(), command); + } + } + + private boolean contains(OutputControl v) { + return cocSpec.contains(v); + } + + private boolean dumpOutput() { + return cocSpec.dumpOutput(); + } + + private boolean saveOutput() { + return cocSpec.saveOutput(); + } + + private boolean discardStdout() { + return cocSpec.discardStdout(); + } + + private boolean discardStderr() { + return cocSpec.discardStderr(); + } + + private boolean redirectStderr() { + return cocSpec.redirectStderr(); + } + + private boolean replaceStdoutWithStderr() { + return redirectStderr() && discardStdout() && !discardStderr(); + } + + private boolean stdoutInherited() { + if (toolProvider || saveOutput() || replaceStdoutWithStderr()) { + return false; + } + return dumpOutput() && !discardStdout() && !contains(OutputControl.DUMP_STDOUT_IN_SYSTEM_OUT); + } + + private boolean stderrInherited() { + if (toolProvider || saveOutput() || redirectStderr()) { + return false; + } + return dumpOutput() && !discardStderr() && !contains(OutputControl.DUMP_STDERR_IN_SYSTEM_ERR); + } + + private void verifyDump(DumpCapture dumpCapture, Command command) { + if (!dumpOutput()) { + assertEquals(List.of(), dumpCapture.outLines()); + assertEquals(List.of(), dumpCapture.errLines()); + return; + } + + if (replaceStdoutWithStderr()) { + // STDERR replaces STDOUT + assertEquals(command.stderr(), dumpCapture.outLines()); + assertEquals(List.of(), dumpCapture.errLines()); + return; + } + + verifyDumpedStdout(dumpCapture, command); + verifyDumpedStderr(dumpCapture, command); + } + + private void verifyDumpedStdout(DumpCapture dumpCapture, Command command) { + if (stdoutInherited()) { + // A subprocess wrote its STDOUT into a file descriptor associated + // with the Java process's STDOUT, not into System.out. Can't capture it. + assertEquals(List.of(), dumpCapture.outLines()); + return; + } + + if (redirectStderr() && !discardStderr()) { + // Interleaved STDOUT and STDERR + if (!isInterleave(dumpCapture.outLines(), command.stdout(), command.stderr())) { + fail(String.format("Unexpected combined output=%s; stdout=%s; stderr=%s", + dumpCapture.outLines(), command.stdout(), command.stderr())); + } + } else if (discardStdout()) { + assertEquals(List.of(), dumpCapture.outLines()); + } else { + assertEquals(command.stdout(), dumpCapture.outLines()); + } + } + + private void verifyDumpedStderr(DumpCapture dumpCapture, Command command) { + if (stderrInherited()) { + // A subprocess wrote its STDERR into a file descriptor associated + // with the Java process's STDERR, not into System.err. Can't capture it. + assertEquals(List.of(), dumpCapture.errLines()); + return; + } + + if (redirectStderr() || discardStderr()) { + assertEquals(List.of(), dumpCapture.errLines()); + } else { + assertEquals(command.stderr(), dumpCapture.errLines()); + } + } + + private void verifyResultContent(CommandOutputControl.Result result, Command command) { + Objects.requireNonNull(result); + Objects.requireNonNull(command); + + assertTrue(result.findByteContent().isEmpty()); + assertTrue(result.findByteStdout().isEmpty()); + assertTrue(result.findByteStderr().isEmpty()); + + if (!saveOutput()) { + assertTrue(result.findContent().isEmpty()); + assertTrue(result.findStdout().isEmpty()); + assertTrue(result.findStderr().isEmpty()); + return; + } + + assertTrue(result.findContent().isPresent()); + + command = filterSavedStreams(command); + + var content = result.content(); + + if (contains(OutputControl.SAVE_FIRST_LINE)) { + assertTrue(content.size() <= 2, String.format("The number of saved lines must be less than or equal to two. Actual: %d", result.content().size())); + } + + if (!redirectStderr()) { + var stdout = result.stdout(); + var stderr = result.stderr(); + + assertEquals(command.stdout(), stdout); + assertEquals(command.stderr(), stderr); + assertEquals(Stream.of( + stdout, + stderr + ).flatMap(List::stream).toList(), content); + } else { + assertEquals(discardStderr(), result.findStdout().isPresent()); + assertTrue(result.findStderr().isEmpty()); + if (contains(OutputControl.SAVE_FIRST_LINE)) { + assertTrue(List.of(command.stdout(), command.stderr()).contains(result.content()), + String.format("Saved content %s is either %s or %s", + content, command.stdout(), command.stderr())); + } else if (contains(OutputControl.SAVE_ALL)) { + if (!isInterleave(content, command.stdout(), command.stderr())) { + fail(String.format("Unexpected combined saved content=%s; stdout=%s; stderr=%s", + content, command.stdout(), command.stderr())); + } + } else { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + } + + private void verifyByteResultContent(CommandOutputControl.Result result, Command command, Charset charset) { + Objects.requireNonNull(result); + Objects.requireNonNull(command); + Objects.requireNonNull(charset); + + assertTrue(result.findContent().isEmpty()); + assertTrue(result.findStdout().isEmpty()); + assertTrue(result.findStderr().isEmpty()); + + if (!saveOutput()) { + assertTrue(result.findByteContent().isEmpty()); + assertTrue(result.findByteStdout().isEmpty()); + assertTrue(result.findByteStderr().isEmpty()); + return; + } + + assertTrue(result.findByteContent().isPresent()); + + command = filterSavedStreams(command); + + if (!redirectStderr()) { + assertEquals(command.stdout(), toStringList(result.byteStdout(), charset)); + assertEquals(command.stderr(), toStringList(result.byteStderr(), charset)); + assertEquals(Stream.of( + command.stdout(), + command.stderr() + ).flatMap(List::stream).toList(), toStringList(result.byteContent(), charset)); + } else { + assertEquals(discardStderr(), result.findByteStdout().isPresent()); + assertTrue(result.findByteStderr().isEmpty()); + + var combined = toStringList(result.byteContent(), charset); + if (!isInterleave(combined, command.stdout(), command.stderr())) { + fail(String.format("Unexpected combined saved content=%s; stdout=%s; stderr=%s", + combined, command.stdout(), command.stderr())); + } + } + } + + private List expectedSavedStream(List commandOutput) { + Objects.requireNonNull(commandOutput); + if (contains(OutputControl.SAVE_ALL) || (contains(OutputControl.SAVE_FIRST_LINE) && contains(OutputControl.BINARY_OUTPUT))) { + return commandOutput; + } else if (contains(OutputControl.SAVE_FIRST_LINE)) { + return commandOutput.stream().findFirst().map(List::of).orElseGet(List::of); + } else { + throw new IllegalStateException(); + } + } + + private Command filterSavedStreams(Command command) { + return new Command( + (discardStdout() ? List.of() : expectedSavedStream(command.stdout())), + (discardStderr() ? List.of() : expectedSavedStream(command.stderr()))); + } + + private record DumpCapture(byte[] out, byte[] err, Charset outCharset, Charset errCharset) { + DumpCapture { + Objects.requireNonNull(out); + Objects.requireNonNull(err); + Objects.requireNonNull(outCharset); + Objects.requireNonNull(errCharset); + } + + List outLines() { + return toStringList(out, outCharset); + } + + List errLines() { + return toStringList(err, errCharset); + } + + static DumpCapture captureDump(Runnable runnable) { + final var captureOut = new ByteArrayOutputStream(); + final var captureErr = new ByteArrayOutputStream(); + + final var out = System.out; + final var err = System.err; + try { + final var outCharset = System.out.charset(); + final var errCharset = System.err.charset(); + System.setOut(new PrintStream(captureOut, true, outCharset)); + System.setErr(new PrintStream(captureErr, true, errCharset)); + runnable.run(); + return new DumpCapture(captureOut.toByteArray(), captureErr.toByteArray(), outCharset, errCharset); + } finally { + try { + System.setOut(out); + } finally { + System.setErr(err); + } + } + } + } + + private CommandOutputControl.Executable createExecutable(Command command) { + final var coc = cocSpec.create(); + if (toolProvider) { + return coc.createExecutable(command.asToolProvider()); + } else { + return coc.createExecutable(new ProcessBuilder(command.asExecutable())); + } + } + } + + record CharsetTestSpec(boolean toolProvider, CommandOutputControlSpec cocSpec) { + + void test() throws IOException, InterruptedException { + if (cocSpec.outputControl().stream().noneMatch(DumpOutputSink.class::isInstance)) { + throw new IllegalArgumentException(); + } + + final var expectedString = "veni-vidi-vici"; + + var coc = cocSpec.create().dumpOutput(true); + + CommandOutputControl.Executable exec; + if (toolProvider) { + var tp = Command.createToolProvider(Stream.of(expectedString).mapMulti((str, sink) -> { + sink.accept(CommandAction.echoStdout(str)); + sink.accept(CommandAction.echoStderr(str)); + }).toList()); + exec = coc.createExecutable(tp); + } else { + var cmdline = Command.createShellCommandLine(Stream.of(expectedString).map(str -> { + return (str + System.lineSeparator()).getBytes(coc.charset()); + }).mapMulti((bytes, sink) -> { + sink.accept(CommandAction.writeStdout(bytes)); + sink.accept(CommandAction.writeStderr(bytes)); + }).toList()); + exec = coc.createExecutable(new ProcessBuilder(cmdline)); + } + + exec.execute(); + + for (var outputContolMutator : cocSpec.outputControl()) { + if (outputContolMutator instanceof DumpOutputSink sink) { + var actual = sink.lines(); + List expected; + if (cocSpec.redirectStderr()) { + switch (sink.streams()) { + case STDERR -> { + expected = List.of(); + } + default -> { + expected = List.of(expectedString, expectedString); + } + } + } else { + expected = List.of(expectedString); + } + assertEquals(expected, actual); + } + } + + } + + record DumpOutputSink(Charset charset, ByteArrayOutputStream buffer, OutputStreams streams) implements CommandOutputControlMutator { + DumpOutputSink { + Objects.requireNonNull(charset); + Objects.requireNonNull(buffer); + Objects.requireNonNull(streams); + } + + DumpOutputSink(Charset charset, OutputStreams streams) { + this(charset, new ByteArrayOutputStream(), streams); + } + + List lines() { + var str = buffer.toString(charset); + return new BufferedReader(new StringReader(str)).lines().toList(); + } + + @Override + public String name() { + return String.format("DUMP-%s-%s", streams, charset.name()); + } + + @Override + public void mutate(CommandOutputControl coc) { + var ps = new PrintStream(buffer, false, charset); + switch (streams) { + case STDOUT -> { + coc.dumpStdout(ps); + } + case STDERR -> { + coc.dumpStderr(ps); + } + case STDOUT_AND_STDERR -> { + // Easy to implement, but not used. + throw new IllegalArgumentException(); + } + default -> { + // Unreachable + throw ExceptionBox.reachedUnreachable(); + } + } + } + } + } + + private final static class InterruptibleToolProvider implements ToolProvider { + + InterruptibleToolProvider(ToolProvider impl) { + this.impl = impl; + } + + @Override + public String name() { + return impl.name(); + } + + @Override + public int run(PrintStream out, PrintStream err, String... args) { + return run(_ -> { + return impl.run(out, err, args); + }, args); + } + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + return run(_ -> { + return impl.run(out, err, args); + }, args); + } + + boolean interrupted() { + return interrupted.join(); + } + + private int run(Function workload, String... args) { + boolean interruptedValue = false; + try { + return workload.apply(args); + } catch (ExceptionBox ex) { + if (ex.getCause() instanceof InterruptedException) { + interruptedValue = true; + return 1; + } else { + throw ex; + } + } finally { + interrupted.complete(interruptedValue); + } + } + + private final ToolProvider impl; + private final CompletableFuture interrupted = new CompletableFuture<>(); + } + + private static List toStringList(byte[] data, Charset charset) { + try (var bufReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data), charset))) { + return bufReader.lines().toList(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static List> withAndWithout (T value) { + return List.of(Optional.empty(), Optional.of(value)); + } + + private static final List BOOLEAN_VALUES = List.of(true, false); +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTestUtils.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTestUtils.java new file mode 100644 index 00000000000..dec0c5d6b0a --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/CommandOutputControlTestUtils.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class CommandOutputControlTestUtils { + + @ParameterizedTest + @MethodSource + public void test_isInterleave(TestSpec test) { + test.run(); + } + + private static Stream test_isInterleave() { + var data = new ArrayList(); + + data.addAll(List.of( + interleaved("Toaday", "Today", "a"), + interleaved("Todanaay", "Today", "ana"), + interleaved("aaaababaaa", "aaaba", "aabaa"), + interleaved("xxxxxxxxxxxyxxyx", "xxxxxxxy", "xxxxxyxx"), + interleaved("xyxxxxyxxxxxxxxx", "yxxxxxxx", "xxxyxxxx"), + interleaved("xxxxxxxyxxxxyxxx", "xxxyxxxx", "xxxxxxyx"), + interleaved("cbdddcdaadacdbddbdcdddccdabbadba", "cdddaaddbcdcdbab", "bdcadcbddddcabda"), + interleaved("ddbdcacddddbddbdbddadcaaccdcabab", "dbccdddbbddacdaa", "ddaddbdddacaccbb"), + interleaved("adccbacbacaacddadddcdbbddbbddddd", "acbcaacddddbdbdd", "dcabcadadcbdbddd"), + interleaved("abdbdabdaacdcdbddddadbbccddcddac", "addbaccbdddbcdda", "bbadaddddabcdcdc"), + interleaved("cdaacbddaabdddbddbddbddadbacccdc", "dabdadddbddabccc", "cacdabdbddbddacd"), + notInterleaved("Toady", "Today", "a"), + notInterleaved("", "Today", "a") + )); + + data.addAll(generateTestData("abcdefghijklmnopqrstuvwxyz", 10)); + data.addAll(generateTestData("xxxxxxxy", 8)); + data.addAll(generateTestData("aaabbbcccddddddd", 50)); + + return data.stream().flatMap(test -> { + return Stream.of(test, test.flip()); + }); + } + + private static List generateTestData(String src, int iteration) { + + var srcCodePoints = new ArrayList(); + src.codePoints().mapToObj(Integer::valueOf).forEach(srcCodePoints::add); + + var data = new ArrayList(); + + Function, String> toString = codePoints -> { + var arr = codePoints.stream().mapToInt(Integer::intValue).toArray(); + return new String(arr, 0, arr.length); + }; + + for (int i = 0; i < 10; i++) { + Collections.shuffle(srcCodePoints); + var a = List.copyOf(srcCodePoints); + + Collections.shuffle(srcCodePoints); + var b = List.copyOf(srcCodePoints); + + var zip = new int[srcCodePoints.size() * 2]; + for (int codePointIdx = 0; codePointIdx != a.size(); codePointIdx++) { + var dstIdx = codePointIdx * 2; + zip[dstIdx] = a.get(codePointIdx); + zip[dstIdx + 1] = b.get(codePointIdx); + } + + data.add(interleaved(toString.apply(Arrays.stream(zip).boxed().toList()), toString.apply(a), toString.apply(b))); + } + + return data; + } + + public record TestSpec(String combined, String a, String b, boolean expected) { + + public TestSpec { + Objects.requireNonNull(combined); + Objects.requireNonNull(a); + Objects.requireNonNull(b); + } + + TestSpec flip() { + return new TestSpec(combined, b, a, expected); + } + + void run() { + assertEquals(expected, isInterleave( + combined.chars().mapToObj(Integer::valueOf).toList(), + a.chars().mapToObj(Integer::valueOf).toList(), + b.chars().mapToObj(Integer::valueOf).toList()), + String.format("combined: %s; a=%s; b=%s", combined, a, b)); + } + } + + private static TestSpec interleaved(String combined, String a, String b) { + return new TestSpec(combined, a, b, true); + } + + private static TestSpec notInterleaved(String combined, String a, String b) { + return new TestSpec(combined, a, b, false); + } + + // Solves the standard "Find if a string C is an interleave of strings A and B." + // problem but use containers instead of strings. + static boolean isInterleave(List combined, List a, List b) { + + if (a.size() + b.size() != combined.size()) { + return false; + } + + final var n = a.size(); + final var m = b.size(); + + var prev = new boolean[m + 1]; + final var cur = new boolean[m + 1]; + + prev[0] = true; + + for (int j = 1; j <= m; j++) { + prev[j] = prev[j - 1] && Objects.equals(b.get(j - 1), combined.get(j - 1)); + } + + for (int i = 1; i <= n; i++) { + cur[0] = prev[0] && Objects.equals(a.get(i - 1), combined.get(i - 1)); + + for (int j = 1; j <= m; j++) { + int k = i + j; + cur[j] = (prev[j] && Objects.equals(a.get(i - 1), combined.get(k - 1))) + || (cur[j - 1] && Objects.equals(b.get(j - 1), combined.get(k - 1))); + } + + prev = cur.clone(); + } + + return prev[m]; + } +} diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/EnquoterTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/EnquoterTest.java similarity index 57% rename from test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/EnquoterTest.java rename to test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/EnquoterTest.java index 95b0e54a0cd..8dc44f3f2fd 100644 --- a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/EnquoterTest.java +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/EnquoterTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -21,11 +21,13 @@ * questions. */ -package jdk.jpackage.internal; +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -45,27 +47,41 @@ public class EnquoterTest { assertEquals(expected, actual); } - private static Stream testForShellLiterals() { + @ParameterizedTest + @MethodSource + public void testIdentity(String input) { + var actual = Enquoter.identity().applyTo(input); + assertEquals(input, actual); + } + + @ParameterizedTest + @MethodSource("testIdentity") + public void testNoEscaper(String input) { + var actual = Enquoter.identity().setEnquotePredicate(_ -> true).applyTo(input); + assertEquals('"' + input + '"', actual); + } + + private static Stream testForShellLiterals() { return Stream.of( - makeArguments("''", ""), - makeArguments("'foo'", "foo"), - makeArguments("' foo '", " foo "), - makeArguments("'foo bar'", "foo bar"), - makeArguments("'foo\\' bar'", "foo' bar") + Arguments.of("''", ""), + Arguments.of("'foo'", "foo"), + Arguments.of("' foo '", " foo "), + Arguments.of("'foo bar'", "foo bar"), + Arguments.of("'foo\\' bar'", "foo' bar") ); } - private static Stream testForPropertyValues() { + private static Stream testForPropertyValues() { return Stream.of( - makeArguments("", ""), - makeArguments("foo", "foo"), - makeArguments("\" foo \"", " foo "), - makeArguments("\"foo bar\"", "foo bar"), - makeArguments("\"foo' bar\"", "foo' bar") + Arguments.of("", ""), + Arguments.of("foo", "foo"), + Arguments.of("\" foo \"", " foo "), + Arguments.of("\"foo bar\"", "foo bar"), + Arguments.of("\"foo' bar\"", "foo' bar") ); } - static org.junit.jupiter.params.provider.Arguments makeArguments(Object ... args) { - return org.junit.jupiter.params.provider.Arguments.of(args); + private static Stream testIdentity() { + return Stream.of("", "foo", " foo ", "foo bar", "foo' bar"); } } diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/RetryExecutorTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/RetryExecutorTest.java new file mode 100644 index 00000000000..abb0363da80 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/RetryExecutorTest.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2026, 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. + */ +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import jdk.jpackage.internal.util.RetryExecutor.Context; +import jdk.jpackage.internal.util.function.ExceptionBox; +import jdk.jpackage.internal.util.function.ThrowingFunction; +import jdk.jpackage.internal.util.function.ThrowingSupplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class RetryExecutorTest { + + @Test + public void test_defaults() { + + var executor = new AttemptCounter(context -> { + throw new AttemptFailedException(); + }); + + var defaultTimeout = Duration.ofSeconds(2); + var defaultAttemptCount = 5; + + var timeout = Slot.createEmpty(); + + assertThrowsExactly(AttemptFailedException.class, new RetryExecutor(Exception.class) + .setExecutable(executor) + .setSleepFunction(t -> { + assertEquals(defaultTimeout, t); + timeout.set(t); + return; + })::execute); + + assertEquals(defaultTimeout, timeout.get()); + assertEquals(defaultAttemptCount, executor.count()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, -4}) + public void test_N_attempts_fail(int maxAttemptsCount) throws AttemptFailedException { + + var retry = new RetryExecutor(AttemptFailedException.class) + .setMaxAttemptsCount(maxAttemptsCount) + .setAttemptTimeout(null) + .setExecutable(context -> { + if (context.attempt() == (maxAttemptsCount - 1)) { + assertTrue(context.isLastAttempt()); + } else { + assertFalse(context.isLastAttempt()); + } + throw new AttemptFailedException("Attempt: " + context.attempt()); + }); + + if (maxAttemptsCount <= 0) { + assertNull(retry.execute()); + } else { + var ex = assertThrowsExactly(AttemptFailedException.class, retry::execute); + assertEquals("Attempt: " + (maxAttemptsCount - 1), ex.getMessage()); + } + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + public void test_N_attempts_last_succeed(int maxAttemptsCount) throws AttemptFailedException { + test_N_attempts_M_succeed(maxAttemptsCount, maxAttemptsCount - 1, false); + } + + @ParameterizedTest + @ValueSource(ints = {2, 3}) + public void test_N_attempts_first_succeed(int maxAttemptsCount) throws AttemptFailedException { + test_N_attempts_M_succeed(maxAttemptsCount, 0, false); + } + + @Test + public void test_N_attempts_2nd_succeed() throws AttemptFailedException { + test_N_attempts_M_succeed(4, 1, false); + } + + @Test + public void test_N_attempts_2nd_succeed_unchecked() throws AttemptFailedException { + test_N_attempts_M_succeed(4, 1, true); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_null_executor(boolean dynamic) { + var retry = new RetryExecutor(AttemptFailedException.class) + .setAttemptTimeout(null).setMaxAttemptsCount(1000); + + if (dynamic) { + int maxAttemptsCount = 3; + var executor = new AttemptCounter(context -> { + assertTrue(context.attempt() <= (maxAttemptsCount - 1)); + if (context.attempt() == (maxAttemptsCount - 1)) { + context.executor().setExecutable((ThrowingSupplier)null); + } + throw new AttemptFailedException("foo"); + }); + + retry.setExecutable(executor); + + var ex = assertThrowsExactly(IllegalStateException.class, retry::execute); + assertEquals("No executable", ex.getMessage()); + assertEquals(3, executor.count()); + } else { + var ex = assertThrowsExactly(IllegalStateException.class, retry::execute); + assertEquals("No executable", ex.getMessage()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_unexpected_exception(boolean executeUnchecked) { + var cause = new UnsupportedOperationException("foo"); + + var executor = new AttemptCounter(context -> { + assertEquals(0, context.attempt()); + throw cause; + }); + + var retry = new RetryExecutor(IOException.class).setExecutable(executor) + .setMaxAttemptsCount(10).setAttemptTimeout(null); + + UnsupportedOperationException ex; + if (executeUnchecked) { + ex = assertThrowsExactly(UnsupportedOperationException.class, retry::executeUnchecked); + } else { + ex = assertThrowsExactly(UnsupportedOperationException.class, retry::execute); + } + assertSame(cause, ex); + assertEquals(1, executor.count()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_dynamic(boolean abort) { + int maxAttemptsCount = 4; + + var secondExecutor = new AttemptCounter(context -> { + throw new AttemptFailedException("bar"); + }); + + var firstExecutor = new AttemptCounter(context -> { + assertTrue(context.attempt() <= (maxAttemptsCount - 1)); + if (context.attempt() == (maxAttemptsCount - 1)) { + if (abort) { + context.executor().setMaxAttemptsCount(maxAttemptsCount); + } else { + // Let it go two more times. + context.executor().setMaxAttemptsCount(maxAttemptsCount + 2); + } + context.executor().setExecutable(secondExecutor); + } + throw new AttemptFailedException("foo"); + }); + + var retry = new RetryExecutor(AttemptFailedException.class) + .setExecutable(firstExecutor) + .setMaxAttemptsCount(1000000) + .setAttemptTimeout(null); + + var ex = assertThrowsExactly(AttemptFailedException.class, retry::execute); + if (abort) { + assertEquals("foo", ex.getMessage()); + assertEquals(0, secondExecutor.count()); + } else { + assertEquals("bar", ex.getMessage()); + assertEquals(2, secondExecutor.count()); + } + assertEquals(maxAttemptsCount, firstExecutor.count()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_supplier_executor(boolean isNull) throws Exception { + var retry = new RetryExecutor(Exception.class).setMaxAttemptsCount(1); + if (isNull) { + retry.setExecutable((ThrowingSupplier)null); + var ex = assertThrowsExactly(IllegalStateException.class, retry::execute); + assertEquals("No executable", ex.getMessage()); + } else { + retry.setExecutable(() -> "Hello"); + assertEquals("Hello", retry.execute()); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_executeUnchecked_fail(boolean withExceptionMapper) throws AttemptFailedException { + var retry = new RetryExecutor(AttemptFailedException.class).setExecutable(() -> { + throw new AttemptFailedException("kaput!"); + }).setMaxAttemptsCount(1); + + Class expectedExceptionType; + if (withExceptionMapper) { + retry.setExceptionMapper((AttemptFailedException ex) -> { + assertEquals("kaput!", ex.getMessage()); + return new UncheckedAttemptFailedException(ex); + }); + expectedExceptionType = UncheckedAttemptFailedException.class; + } else { + expectedExceptionType = ExceptionBox.class; + } + + var ex = assertThrowsExactly(expectedExceptionType, retry::executeUnchecked); + assertEquals(AttemptFailedException.class, ex.getCause().getClass()); + assertEquals("kaput!", ex.getCause().getMessage()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void test_setSleepFunction(boolean withTimeout) { + + var timeout = Slot.createEmpty(); + + assertDoesNotThrow(new RetryExecutor(AttemptFailedException.class) + .setMaxAttemptsCount(2) + .mutate(retry -> { + if (withTimeout) { + retry.setAttemptTimeout(Duration.ofDays(100)); + } else { + retry.setAttemptTimeout(null); + } + }) + .setExecutable(context -> { + if (context.isLastAttempt()) { + return null; + } else { + throw new AttemptFailedException(); + } + }) + .setSleepFunction(timeout::set)::execute); + + assertEquals(withTimeout, timeout.find().isPresent()); + if (withTimeout) { + assertEquals(Duration.ofDays(100), timeout.get()); + } + } + + private static void test_N_attempts_M_succeed(int maxAttempts, int failedAttempts, boolean unchecked) throws AttemptFailedException { + + var countingExecutor = new AttemptCounter(context -> { + if (context.attempt() == failedAttempts) { + return "You made it!"; + } else { + throw new AttemptFailedException(); + } + }); + + var retry = new RetryExecutor(AttemptFailedException.class) + .setMaxAttemptsCount(maxAttempts) + .setAttemptTimeout(null) + .setExecutable(countingExecutor); + + assertEquals("You made it!", unchecked ? retry.execute() : retry.executeUnchecked()); + assertEquals(failedAttempts, countingExecutor.count() - 1); + } + + private static final class AttemptCounter implements ThrowingFunction>, T, E> { + + AttemptCounter(ThrowingFunction>, T, E> impl) { + this.impl = Objects.requireNonNull(impl); + } + + @Override + public T apply(Context> context) throws E { + counter++; + return impl.apply(context); + } + + int count() { + return counter; + } + + private int counter; + private final ThrowingFunction>, T, E> impl; + } + + private static final class AttemptFailedException extends Exception { + + AttemptFailedException(String msg) { + super(msg); + } + + AttemptFailedException() { + } + + private static final long serialVersionUID = 1L; + } + + private static final class UncheckedAttemptFailedException extends RuntimeException { + + UncheckedAttemptFailedException(AttemptFailedException ex) { + super(ex); + } + + private static final long serialVersionUID = 1L; + } +} diff --git a/test/jdk/tools/jpackage/macosx/HostArchPkgTest.java b/test/jdk/tools/jpackage/macosx/HostArchPkgTest.java index 7498043c14f..7aebff7cb00 100644 --- a/test/jdk/tools/jpackage/macosx/HostArchPkgTest.java +++ b/test/jdk/tools/jpackage/macosx/HostArchPkgTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, 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 @@ -64,8 +64,7 @@ public class HostArchPkgTest { dbf.setFeature("http://apache.org/xml/features/" + "nonvalidating/load-external-dtd", false); DocumentBuilder b = dbf.newDocumentBuilder(); - org.w3c.dom.Document doc - = b.parse(Files.newInputStream(distributionFile)); + org.w3c.dom.Document doc = b.parse(distributionFile.toFile()); XPath xPath = XPathFactory.newInstance().newXPath(); diff --git a/test/jdk/tools/jpackage/share/ErrorTest.java b/test/jdk/tools/jpackage/share/ErrorTest.java index bc12577fa20..ca1189a191e 100644 --- a/test/jdk/tools/jpackage/share/ErrorTest.java +++ b/test/jdk/tools/jpackage/share/ErrorTest.java @@ -441,10 +441,7 @@ public final class ErrorTest { .error("error.no-module-in-path", "com.foo.bar"), // non-existing argument file testSpec().noAppDesc().notype().addArgs("@foo") - .error("ERR_CannotParseOptions", "foo"), - // invalid jlink option - testSpec().addArgs("--jlink-options", "--foo") - .error("error.jlink.failed", "Error: unknown option: --foo") + .error("ERR_CannotParseOptions", "foo") ).map(TestSpec.Builder::create).toList()); // --main-jar and --module-name diff --git a/test/jdk/tools/jpackage/share/PostImageScriptTest.java b/test/jdk/tools/jpackage/share/PostImageScriptTest.java index 655658f036d..81cdfffbf46 100644 --- a/test/jdk/tools/jpackage/share/PostImageScriptTest.java +++ b/test/jdk/tools/jpackage/share/PostImageScriptTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -229,7 +229,7 @@ public class PostImageScriptTest { cmd.saveConsoleOutput(true); }).addBundleVerifier((cmd, result) -> { - final var imageDir = result.stdout().getOutput().stream().map(String::stripLeading).filter(str -> { + final var imageDir = result.stdout().stream().map(String::stripLeading).filter(str -> { return str.startsWith(imageDirOutputPrefix); }).map(str -> { return str.substring(imageDirOutputPrefix.length()); diff --git a/test/jdk/tools/jpackage/windows/WinLongVersionTest.java b/test/jdk/tools/jpackage/windows/WinLongVersionTest.java index 2ee76bcaa15..13650b2dfd3 100644 --- a/test/jdk/tools/jpackage/windows/WinLongVersionTest.java +++ b/test/jdk/tools/jpackage/windows/WinLongVersionTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2022, 2026, 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 @@ -180,9 +180,8 @@ public class WinLongVersionTest { cmd.setFakeRuntime(); // Create package without Upgrade table - Document doc = XmlUtils.initDocumentBuilder().parse( - Files.newInputStream(TKit.SRC_ROOT.resolve( - "windows/classes/jdk/jpackage/internal/resources/main.wxs"))); + Document doc = XmlUtils.initDocumentBuilder().parse(TKit.SRC_ROOT.resolve( + "windows/classes/jdk/jpackage/internal/resources/main.wxs").toFile()); XPath xPath = XPathFactory.newInstance().newXPath(); NodeList nodes = (NodeList) xPath.evaluate("/Wix/Product/Upgrade", doc, XPathConstants.NODESET); diff --git a/test/langtools/jdk/jshell/ErrorTranslationTest.java b/test/langtools/jdk/jshell/ErrorTranslationTest.java index 1aa1b2c41dd..235f4443004 100644 --- a/test/langtools/jdk/jshell/ErrorTranslationTest.java +++ b/test/langtools/jdk/jshell/ErrorTranslationTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -149,7 +149,6 @@ public class ErrorTranslationTest extends ReplToolTesting { return sb.toString(); } - @Test public String getKind(Diagnostic.Kind kind) { switch (kind) { case WARNING: diff --git a/test/langtools/jdk/jshell/IdGeneratorTest.java b/test/langtools/jdk/jshell/IdGeneratorTest.java index 6c7f6177e03..521c87b0265 100644 --- a/test/langtools/jdk/jshell/IdGeneratorTest.java +++ b/test/langtools/jdk/jshell/IdGeneratorTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -44,7 +44,6 @@ import org.junit.jupiter.api.Test; public class IdGeneratorTest { - @Test public JShell.Builder getBuilder() { TestingInputStream inStream = new TestingInputStream(); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); diff --git a/test/langtools/jdk/jshell/KullaCompletenessStressTest.java b/test/langtools/jdk/jshell/KullaCompletenessStressTest.java index 69830880dcb..16274fa924e 100644 --- a/test/langtools/jdk/jshell/KullaCompletenessStressTest.java +++ b/test/langtools/jdk/jshell/KullaCompletenessStressTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, 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 @@ -32,13 +32,11 @@ import java.io.File; import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class KullaCompletenessStressTest extends CompletenessStressTest { @Override - @Test public File[] getDirectoriesToTest() { String src = System.getProperty("test.src"); File file; diff --git a/test/lib/jdk/test/lib/cds/CDSAppTester.java b/test/lib/jdk/test/lib/cds/CDSAppTester.java index fd244c6acc6..18356dd8aa9 100644 --- a/test/lib/jdk/test/lib/cds/CDSAppTester.java +++ b/test/lib/jdk/test/lib/cds/CDSAppTester.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026, 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 @@ -57,6 +57,8 @@ abstract public class CDSAppTester { private int numProductionRuns = 0; private String whiteBoxJar = null; private boolean inOneStepTraining = false; + private boolean generateBaseArchive = false; + private String[] baseArchiveOptions = new String[0]; /** * All files created in the CDS/AOT workflow will be name + extension. E.g. @@ -343,7 +345,7 @@ abstract public class CDSAppTester { // VM options used by this test, we need to create a temporary static archive to be used with -XX:ArchiveClassesAtExit. private String getBaseArchiveForDynamicArchive() throws Exception { WhiteBox wb = WhiteBox.getWhiteBox(); - if (wb.isSharingEnabled()) { + if (wb.isSharingEnabled() && !generateBaseArchive) { // This current JVM is able to use a default CDS archive included by the JDK, so // if we launch a JVM child process (with the same set of options as the current JVM), // that process is also able to use the same default CDS archive for creating @@ -356,6 +358,7 @@ abstract public class CDSAppTester { if (!f.exists()) { CDSOptions opts = new CDSOptions(); opts.setArchiveName(tempBaseArchiveFile); + opts.addSuffix(baseArchiveOptions); opts.addSuffix("-Djava.class.path="); OutputAnalyzer out = CDSTestUtils.createArchive(opts); CDSTestUtils.checkBaseDump(out); @@ -364,6 +367,16 @@ abstract public class CDSAppTester { } } + public CDSAppTester setGenerateBaseArchive(boolean b) { + this.generateBaseArchive = b; + return this; + } + + public CDSAppTester setBaseArchiveOptions(String... opts) { + this.baseArchiveOptions = opts; + return this; + } + private OutputAnalyzer dumpDynamicArchive() throws Exception { RunMode runMode = RunMode.DUMP_DYNAMIC; String[] cmdLine = new String[0]; diff --git a/test/lib/jdk/test/lib/cds/SimpleCDSAppTester.java b/test/lib/jdk/test/lib/cds/SimpleCDSAppTester.java index c869cfa366b..a8bba9c76e2 100644 --- a/test/lib/jdk/test/lib/cds/SimpleCDSAppTester.java +++ b/test/lib/jdk/test/lib/cds/SimpleCDSAppTester.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, 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 @@ -58,9 +58,11 @@ public class SimpleCDSAppTester { private String modulepath; private String[] appCommandLine; private String[] vmArgs = new String[] {}; + private Tester tester; private SimpleCDSAppTester(String name) { this.name = name; + this.tester = new Tester(name); } public static SimpleCDSAppTester of(String name) { @@ -101,6 +103,16 @@ public class SimpleCDSAppTester { return this; } + public SimpleCDSAppTester setGenerateBaseArchive(boolean b) { + tester.setGenerateBaseArchive(b); + return this; + } + + public SimpleCDSAppTester setBaseArchiveOptions(String... opts) { + tester.setBaseArchiveOptions(opts); + return this; + } + public SimpleCDSAppTester setTrainingChecker(BiConsumer checker) { this.trainingChecker = checker; return this; @@ -181,17 +193,22 @@ public class SimpleCDSAppTester { } public SimpleCDSAppTester runStaticWorkflow() throws Exception { - (new Tester(name)).runStaticWorkflow(); + tester.runStaticWorkflow(); + return this; + } + + public SimpleCDSAppTester runDynamicWorkflow() throws Exception { + tester.runDynamicWorkflow(); return this; } public SimpleCDSAppTester runAOTWorkflow() throws Exception { - (new Tester(name)).runAOTWorkflow(); + tester.runAOTWorkflow(); return this; } public SimpleCDSAppTester run(String args[]) throws Exception { - (new Tester(name)).run(args); + tester.run(args); return this; } } diff --git a/test/lib/jdk/test/lib/jvmti/jvmti_common.hpp b/test/lib/jdk/test/lib/jvmti/jvmti_common.hpp index 11600bd524b..3832d934b1e 100644 --- a/test/lib/jdk/test/lib/jvmti/jvmti_common.hpp +++ b/test/lib/jdk/test/lib/jvmti/jvmti_common.hpp @@ -123,6 +123,12 @@ char* julong_to_string(julong value, char *string) { return string; } +static void +fatal(const char* msg) { + LOG("FATAL ERROR: %s\n", msg); + abort(); +} + static void fatal(JNIEnv* jni, const char* msg) { jni->FatalError(msg); @@ -313,6 +319,17 @@ get_thread_name(jvmtiEnv *jvmti, JNIEnv* jni, jthread thread) { return tname; } +static char* +get_method_name(jvmtiEnv *jvmti, jmethodID method) { + char* mname = nullptr; + jvmtiError err; + + err = jvmti->GetMethodName(method, &mname, nullptr, nullptr); + check_jvmti_error(err, "get_method_name: error in JVMTI GetMethodName call"); + + return mname; +} + static char* get_method_name(jvmtiEnv *jvmti, JNIEnv* jni, jmethodID method) { char* mname = nullptr; diff --git a/test/micro/org/openjdk/bench/java/awt/image/PNGImageDecoder_8bit_uninterlaced.java b/test/micro/org/openjdk/bench/java/awt/image/PNGImageDecoder_8bit_uninterlaced.java new file mode 100644 index 00000000000..c91b3e24ba7 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/awt/image/PNGImageDecoder_8bit_uninterlaced.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2026, 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. + */ +package org.openjdk.bench.java.awt.image; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import javax.imageio.ImageIO; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.ImageConsumer; +import java.awt.image.IndexColorModel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Hashtable; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 20) +@Fork(3) +@State(Scope.Thread) +public class PNGImageDecoder_8bit_uninterlaced { + + byte[] pngImageData; + + @Setup + public void setup() throws Exception { + pngImageData = createImageData(2_500); + } + + @Benchmark + public void measurePNGImageDecoder(Blackhole bh) throws Exception { + Image img = Toolkit.getDefaultToolkit().createImage(pngImageData); + BufferedImage bi = createBufferedImage(img); + bi.flush(); + bh.consume(bi); + } + + /** + * Create a large sample image stored as an 8-bit PNG. + * + * @return the byte representation of the PNG image. + */ + private static byte[] createImageData(int squareSize) throws Exception { + BufferedImage bi = new BufferedImage(squareSize, squareSize, + BufferedImage.TYPE_BYTE_INDEXED); + Random r = new Random(0); + Graphics2D g = bi.createGraphics(); + for (int a = 0; a < 20000; a++) { + g.setColor(new Color(r.nextInt(0xffffff))); + int radius = 10 + r.nextInt(90); + g.fillOval(r.nextInt(bi.getWidth()), r.nextInt(bi.getHeight()), + radius, radius); + } + g.dispose(); + + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + ImageIO.write(bi, "png", out); + return out.toByteArray(); + } + } + + static BufferedImage createBufferedImage(Image img) + throws ExecutionException, InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + img.getSource().startProduction(new ImageConsumer() { + private int imageWidth, imageHeight; + private BufferedImage bi; + + @Override + public void setDimensions(int width, int height) { + imageWidth = width; + imageHeight = height; + } + + @Override + public void setProperties(Hashtable props) { + // intentionally empty + } + + @Override + public void setColorModel(ColorModel model) { + // intentionally empty + } + + @Override + public void setHints(int hintflags) { + // intentionally empty + } + + @Override + public void setPixels(int x, int y, int w, int h, ColorModel model, + byte[] pixels, int off, int scansize) { + if (bi == null) { + bi = new BufferedImage(imageWidth, imageHeight, + BufferedImage.TYPE_BYTE_INDEXED, + (IndexColorModel) model); + } + if (h != 1) + throw new UnsupportedOperationException( + "this test expects sequential rows of pixels"); + if (off != 0) + throw new UnsupportedOperationException( + "this test expects the incoming pixels to start " + + "at index zero"); + + bi.getRaster().setDataElements(x, y, w, 1, pixels); + } + + @Override + public void setPixels(int x, int y, int w, int h, ColorModel model, + int[] pixels, int off, int scansize) { + throw new UnsupportedOperationException(); + } + + @Override + public void imageComplete(int status) { + future.complete(bi); + } + }); + return future.get(); + } +} \ No newline at end of file diff --git a/test/micro/org/openjdk/bench/java/io/FileWrite.java b/test/micro/org/openjdk/bench/java/io/FileWrite.java index 21b0c2f8f54..c5a5aee70d8 100644 --- a/test/micro/org/openjdk/bench/java/io/FileWrite.java +++ b/test/micro/org/openjdk/bench/java/io/FileWrite.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, 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 @@ -25,6 +25,7 @@ package org.openjdk.bench.java.io; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.RandomAccessFile; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -81,4 +82,29 @@ public class FileWrite { } } + @State(Scope.Benchmark) + @Warmup(iterations = 3, time = 2) + @Measurement(iterations = 5, time = 5) + @BenchmarkMode(Mode.SampleTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @Threads(1) + @Fork(value = 10) + public static class OpenFileForWritingBench { + final byte[] payload = "something".getBytes(); + final String path = System.getProperty("os.name", "unknown").toLowerCase().contains("win") ? "NUL" : "/dev/null"; + + @Benchmark + public void testFileOutputStream() throws IOException { + try (FileOutputStream f = new FileOutputStream(path)) { + f.write(payload); + } + } + + @Benchmark + public void testRandomAccessFile() throws IOException { + try (RandomAccessFile f = new RandomAccessFile(path, "rw")) { + f.write(payload); + } + } + } }