diff --git a/.github/workflows/reproduce-issue-7.yml b/.github/workflows/reproduce-issue-7.yml new file mode 100644 index 0000000..5761661 --- /dev/null +++ b/.github/workflows/reproduce-issue-7.yml @@ -0,0 +1,79 @@ +name: Reproduce WindowsAttachProvider ServiceConfigurationError + +on: + workflow_dispatch: + push: + branches: + - claude/reproduce-issue-7-Qj92G + +jobs: + reproduce: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Temurin JDK 8 (JAVA_HOME will point here) + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + + - name: Build jar + run: mvn package -DskipTests -q + + - name: Download Temurin JRE-only archive (simulates Oracle standalone JRE) + shell: pwsh + run: | + $url = "https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u442-b06/OpenJDK8U-jre_x64_windows_hotspot_8u442b06.zip" + Invoke-WebRequest -Uri $url -OutFile jre8.zip + Expand-Archive jre8.zip -DestinationPath jre8 + $jreHome = (Get-ChildItem jre8 -Directory)[0].FullName + echo "STANDALONE_JRE=$jreHome" >> $env:GITHUB_ENV + + - name: Show preconditions + shell: pwsh + run: | + Write-Host "=== JDK (JAVA_HOME) ===" + Write-Host "Path : $env:JAVA_HOME" + Write-Host "tools.jar : $(Test-Path "$env:JAVA_HOME\lib\tools.jar")" + Write-Host "attach.dll : $(Test-Path "$env:JAVA_HOME\jre\bin\attach.dll")" + Write-Host "" + Write-Host "=== Standalone JRE (on PATH in real-world scenario) ===" + Write-Host "Path : $env:STANDALONE_JRE" + Write-Host "attach.dll : $(Test-Path "$env:STANDALONE_JRE\bin\attach.dll")" + Write-Host "tools.jar : $(Test-Path "$env:STANDALONE_JRE\lib\tools.jar")" + Write-Host "" + Write-Host "JVM library paths when launched from standalone JRE:" + & "$env:STANDALONE_JRE\bin\java.exe" -XshowSettings:property -version 2>&1 | + Select-String "library.path" + + - name: Reproduce error + shell: pwsh + # Explicitly invoke the standalone JRE's java.exe (not the JDK's). + # JAVA_HOME still points to the JDK so tools.jar is found and + # the URLClassLoader path in AgentAttach is taken. + # sun.boot.library.path is derived from the standalone JRE's jvm.dll + # and points to a bin\ with no attach.dll -- causing UnsatisfiedLinkError + # inside WindowsAttachProvider's static initializer, which the SPI loader + # wraps into: + # ServiceConfigurationError: com.sun.tools.attach.spi.AttachProvider: + # Provider sun.tools.attach.WindowsAttachProvider could not be instantiated + run: | + # Remove all JDK bin directories from PATH so java.library.path + # also can't find any attach.dll as a fallback + $env:PATH = ($env:PATH -split ';' | + Where-Object { $_ -notmatch 'jdk|jre|java|hostedtoolcache' }) -join ';' + + $java = "$env:STANDALONE_JRE\bin\java.exe" + $jar = (Get-ChildItem target\extract-tls-secrets-*.jar)[0].FullName + + Write-Host "PATH: $env:PATH" + Write-Host "java.exe : $java (standalone JRE - no attach.dll)" + Write-Host "JAVA_HOME: $env:JAVA_HOME (JDK - has tools.jar)" + Write-Host "jar : $jar" + Write-Host "" + Write-Host "Running..." + & $java -jar $jar list + continue-on-error: true + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca81663..2fa34a8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,15 +3,45 @@ name: Integration Tests on: [push] jobs: - build: - - runs-on: ubuntu-latest +# build-linux: +# +# runs-on: ubuntu-latest +# +# steps: +# - uses: actions/checkout@v2 +# - name: Set up JDK 1.8 +# uses: actions/setup-java@v2 +# with: +# java-version: 1.8 +# distribution: adopt +# - name: Build and run tests +# run: mvn -B verify +# + build-windows: + runs-on: windows-latest steps: - - uses: actions/checkout@v1 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build and run tests - run: mvn -B verify + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v2 + with: + java-version: 8 + distribution: adopt + - name: Set up JRE 1.8 + uses: actions/setup-java@v2 + with: + java-version: 8 + distribution: adopt + java-package: jre + - name: Build and run tests + run: | + $env:JAVA_HOME = "C:\hostedtoolcache\windows\Java_Adopt_jdk\8.0.292-10\x64" + mvn -B package + $env:JAVA_HOME + $env:PATH + dir $env:JAVA_HOME + dir $env:JAVA_HOME\bin + # dir $env:JAVA_HOME\jre\bin + which java + java -version + java -jar target\extract-tls-secrets-4.1.0-SNAPSHOT.jar list diff --git a/src/main/java/name/neykov/secrets/AgentAttach.java b/src/main/java/name/neykov/secrets/AgentAttach.java index b6a0141..b298692 100644 --- a/src/main/java/name/neykov/secrets/AgentAttach.java +++ b/src/main/java/name/neykov/secrets/AgentAttach.java @@ -1,6 +1,7 @@ package name.neykov.secrets; import java.io.File; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; @@ -40,7 +41,7 @@ public static void main(String[] args) throws Exception { try { attach(jarUrl, jarFile, pid, logFile); - } catch (MessageException e) { + } catch (FailureMessageException e) { for (String line : e.msg) { System.err.println(line); } @@ -50,10 +51,11 @@ public static void main(String[] args) throws Exception { public static void attach(URL jarUrl, File jarFile, String pid, String logFile) throws Exception { if (isAttachApiAvailable()) { - // Either Java 9 or tools.jar already on classpath + // Either Java 9+ or tools.jar already on classpath AttachHelper.handle(jarFile.getAbsolutePath(), pid, logFile); } else { File toolsFile = getToolsFile(); + System.out.println("tools.jar: " + toolsFile.getAbsolutePath()); URL toolsUrl = toolsFile.toURI().toURL(); URL[] cp = new URL[] {jarUrl, toolsUrl}; URLClassLoader classLoader = new URLClassLoader(cp, null); @@ -61,12 +63,33 @@ public static void attach(URL jarUrl, File jarFile, String pid, String logFile) Class helper = classLoader.loadClass("name.neykov.secrets.AttachHelper"); Method handleMethod = helper.getMethod("handle", String.class, String.class, String.class); - handleMethod.invoke(null, jarFile.getAbsolutePath(), pid, logFile); + try { + handleMethod.invoke(null, jarFile.getAbsolutePath(), pid, logFile); + } catch (InvocationTargetException e) { + Throwable cause = e.getCause(); + // The cause class is loaded by "classLoader" and therefore a separate instance failing + // the equality test. It will not get caught by parent exception blocks. + if (cause.getClass().getName().equals(FailureMessageException.class.getName())) { + Field msgField = cause.getClass().getDeclaredField("msg"); + msgField.setAccessible(true); + String[] msg = (String[])msgField.get(cause); + throw new FailureMessageException(msg); + } else { + throw e; + } + } + } + } + static File getJavaHome() throws FailureMessageException { + String javaHomeEnv = System.getenv("JAVA_HOME"); + if (javaHomeEnv != null) { + return new File(javaHomeEnv); } + throw new FailureMessageException("No JAVA_HOME environment variable found. Must point to a local JDK installation."); } - private static File getToolsFile() throws MessageException { + private static File getToolsFile() throws FailureMessageException { File javaHome = getJavaHome(); // javaHome is a JDK @@ -98,27 +121,18 @@ private static File getToolsFile() throws MessageException { // * Java 9 and higher: X.0.0 (e.x. 9.0.0, 11.0.0) if (System.getProperty("java.version").startsWith("1.")) { // JAVA_HOME required - throw new MessageException( + throw new FailureMessageException( "Invalid JAVA_HOME environment variable '" + javaHome.getAbsolutePath() + "'.", "Must point to a local JDK installation containing a 'lib/tools.jar' file." ); } else { // No need for JAVA_HOME. Not executed from a JDK java executable. - throw new MessageException( + throw new FailureMessageException( "No access to JDK classes. Make sure to use the java executable from a JDK install." ); } } - private static File getJavaHome() throws MessageException { - String javaHomeEnv = System.getenv("JAVA_HOME"); - if (javaHomeEnv != null) { - return new File(javaHomeEnv); - } - - throw new MessageException("No JAVA_HOME environment variable found. Must point to a local JDK installation."); - } - private static boolean isAttachApiAvailable() { try { AgentAttach.class.getClassLoader().loadClass("com.sun.tools.attach.VirtualMachine"); diff --git a/src/main/java/name/neykov/secrets/AttachHelper.java b/src/main/java/name/neykov/secrets/AttachHelper.java index cae850f..892fb31 100644 --- a/src/main/java/name/neykov/secrets/AttachHelper.java +++ b/src/main/java/name/neykov/secrets/AttachHelper.java @@ -2,6 +2,7 @@ import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; @@ -15,7 +16,11 @@ //Byte Buddy (https://github.com/raphw/byte-buddy) abstracts //the API, including a fallback implementing the attach api. public class AttachHelper { - public static void handle(String jarPath, String pid, String logFile) throws MessageException { + public static void handle(String jarPath, String pid, String logFile) throws FailureMessageException { + // if (isWindows()) { + // loadAttachLibrary(); + // } + if (pid.equals("list")) { System.out.print(AttachHelper.list()); } else { @@ -24,11 +29,90 @@ public static void handle(String jarPath, String pid, String logFile) throws Mes System.out.println("Successfully attached to process ID " + pid + "."); } catch (IllegalStateException e) { String msg = e.getMessage() != null ? e.getMessage() : "Failed attaching to java process " + pid; - throw new MessageException(msg); + throw new FailureMessageException(msg); } } } + + private static boolean isWindows() { + return System.getProperty("os.name").startsWith("Windows"); + } + + private static void loadAttachLibrary() throws FailureMessageException { + try { + System.loadLibrary("attach"); + // All good - system is set up properly. Nothing to do. + } catch (UnsatisfiedLinkError e) { + // "attach.dll" not on the default search path, let's try some well known locations. + // Could happen if using the JRE with JAVA_HOME pointing to a JDK install. + if (!tryLoadLibrary("jre/bin/attach.dll")) { + throw new FailureMessageException( + "Failed loading attach provider. Make sure you are running with a JDK java executable. " + + "Alternatively locate 'attach.dll' on your system, typically found in " + + "'/jre/bin' folder for Oracle JDK installs, and pass the path at startup as: ", + " java -Djava.library.path=\"/jre/bin\" -jar extract-tls-secrets.jar" + ); + } + } + } + + private static File getJavaHome() throws FailureMessageException { + // Duplicated from AttachHelper, but can't be shared due to ClassLoader boundary + String javaHomeEnv = System.getenv("JAVA_HOME"); + if (javaHomeEnv != null) { + return new File(javaHomeEnv); + } + + throw new FailureMessageException("No JAVA_HOME environment variable found. Must point to a local JDK installation."); + } + + private static boolean tryLoadLibrary(String attachPath) throws FailureMessageException { + File javaHome = getJavaHome(); + File attachAbsolutePath = new File(javaHome, attachPath); + if (attachAbsolutePath.exists()) { + // Check the file path is a valid library + try { + System.load(attachAbsolutePath.getAbsolutePath()); + } catch (UnsatisfiedLinkError ex) { + return false; + } + + // Extend the path. On good installs the path is supposed to come from "sun.boot.library.path". + String initialPath = System.getProperty("java.library.path"); + String extendedPath; + if (initialPath != null && initialPath.length() > 0) { + extendedPath = initialPath + File.pathSeparator + attachAbsolutePath.getParent(); + } else { + extendedPath = attachAbsolutePath.getParent(); + } + System.setProperty("java.library.path", extendedPath); + + // Force reload of the java.library.path property + Field fieldSysPath = null; + try { + fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); + } catch (NoSuchFieldException ex) { + return false; + } + fieldSysPath.setAccessible(true); + try { + fieldSysPath.set(null, null); + } catch (IllegalAccessException ex) { + return false; + } + + // Check patching was successful + try { + System.loadLibrary("attach"); + System.out.println("Loaded attach " + attachAbsolutePath.getAbsolutePath()); + return true; + } catch (UnsatisfiedLinkError ex) { + } + } + return false; + } + private static void attach(String pid, String jarPath, String options) { try { VirtualMachine vm = VirtualMachine.attach(pid); diff --git a/src/main/java/name/neykov/secrets/FailureMessageException.java b/src/main/java/name/neykov/secrets/FailureMessageException.java new file mode 100644 index 0000000..2a4108b --- /dev/null +++ b/src/main/java/name/neykov/secrets/FailureMessageException.java @@ -0,0 +1,9 @@ +package name.neykov.secrets; + +class FailureMessageException extends Exception { + String[] msg; + + protected FailureMessageException(String... msg) { + this.msg = msg; + } +} diff --git a/src/main/java/name/neykov/secrets/MessageException.java b/src/main/java/name/neykov/secrets/MessageException.java deleted file mode 100644 index eee3e9a..0000000 --- a/src/main/java/name/neykov/secrets/MessageException.java +++ /dev/null @@ -1,9 +0,0 @@ -package name.neykov.secrets; - -class MessageException extends Exception { - String[] msg; - - protected MessageException(String... msg) { - this.msg = msg; - } -}