├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── java └── com └── sk89q └── spilledgrounds ├── Arguments.java ├── FatalException.java ├── SimpleLogFormatter.java ├── SpilledGrounds.java └── agent ├── ClassDump.java ├── ClassDumpAgent.java ├── ClassDumpMBean.java └── ClassDumpTransformer.java /.gitignore: -------------------------------------------------------------------------------- 1 | ### Java ### 2 | *.class 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.jar 9 | *.war 10 | *.ear 11 | 12 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 13 | hs_err_pid* 14 | 15 | 16 | ### Maven ### 17 | target/ 18 | pom.xml.tag 19 | pom.xml.releaseBackup 20 | pom.xml.versionsBackup 21 | pom.xml.next 22 | release.properties 23 | dependency-reduced-pom.xml 24 | buildNumber.properties 25 | .mvn/timing.properties 26 | 27 | 28 | ### Gradle ### 29 | .gradle 30 | build/ 31 | 32 | # Ignore Gradle GUI config 33 | gradle-app.setting 34 | 35 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 36 | !gradle-wrapper.jar 37 | 38 | 39 | ### Eclipse ### 40 | *.pydevproject 41 | .metadata 42 | .gradle 43 | bin/ 44 | tmp/ 45 | *.tmp 46 | *.bak 47 | *.swp 48 | *~.nib 49 | local.properties 50 | .settings/ 51 | .loadpath 52 | 53 | # Eclipse Core 54 | .project 55 | 56 | # External tool builders 57 | .externalToolBuilders/ 58 | 59 | # Locally stored "Eclipse launch configurations" 60 | *.launch 61 | 62 | # CDT-specific 63 | .cproject 64 | 65 | # JDT-specific (Eclipse Java Development Tools) 66 | .classpath 67 | 68 | # Java annotation processor (APT) 69 | .factorypath 70 | 71 | # PDT-specific 72 | .buildpath 73 | 74 | # sbteclipse plugin 75 | .target 76 | 77 | # TeXlipse plugin 78 | .texlipse 79 | 80 | 81 | ### Intellij ### 82 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 83 | 84 | *.iml 85 | 86 | ## Directory-based project format: 87 | .idea/ 88 | # if you remove the above rule, at least ignore the following: 89 | 90 | # User-specific stuff: 91 | # .idea/workspace.xml 92 | # .idea/tasks.xml 93 | # .idea/dictionaries 94 | 95 | # Sensitive or high-churn files: 96 | # .idea/dataSources.ids 97 | # .idea/dataSources.xml 98 | # .idea/sqlDataSources.xml 99 | # .idea/dynamic.xml 100 | # .idea/uiDesigner.xml 101 | 102 | # Gradle: 103 | # .idea/gradle.xml 104 | # .idea/libraries 105 | 106 | # Mongo Explorer plugin: 107 | # .idea/mongoSettings.xml 108 | 109 | ## File-based project format: 110 | *.ipr 111 | *.iws 112 | 113 | ## Plugin-specific files: 114 | 115 | # IntelliJ 116 | /out/ 117 | 118 | # mpeltonen/sbt-idea plugin 119 | .idea_modules/ 120 | 121 | # JIRA plugin 122 | atlassian-ide-plugin.xml 123 | 124 | # Crashlytics plugin (for Android Studio and IntelliJ) 125 | com_crashlytics_export_strings.xml 126 | crashlytics.properties 127 | crashlytics-build.properties 128 | 129 | 130 | ### NetBeans ### 131 | nbproject/private/ 132 | build/ 133 | nbbuild/ 134 | dist/ 135 | nbdist/ 136 | nbactions.xml 137 | nb-configuration.xml 138 | .nb-gradle/ 139 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Albert Pham 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spilled Grounds 2 | 3 | A tool to dump Java .class files from a running JVM instance. 4 | 5 | ## Compiling 6 | 7 | ./gradlew clean build 8 | 9 | ## Usage 10 | 11 | `$JAVA_HOME` must point to a JDK installation. 12 | 13 | java -Djava.library.path=$JAVA_HOME/jre/bin \ 14 | -cp $JAVA_HOME/lib/tools.jar;spilledgrounds-1.0-SNAPSHOT-all.jar \ 15 | com.sk89q.spilledgrounds.SpilledGrounds \ 16 | --dir dumps com\.example1\..* com\.example2\..* 17 | 18 | Options: 19 | --dir 20 | directory to dump classes to 21 | Default: dumps 22 | -h, -?, --help 23 | show help 24 | Default: false 25 | -i, --interactive 26 | accept interactive input of class names to dump 27 | Default: false 28 | -p, --pid 29 | PID of VM to connect to 30 | -v, --verbose 31 | show exceptions 32 | Default: false 33 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { jcenter() } 3 | dependencies { 4 | classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' 5 | } 6 | } 7 | 8 | import javax.tools.ToolProvider 9 | 10 | group 'com.sk89q' 11 | version '1.0-SNAPSHOT' 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'com.github.johnrengelman.shadow' 15 | 16 | sourceCompatibility = 1.7 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | compile 'com.beust:jcommander:1.48' 24 | compile files(((URLClassLoader) ToolProvider.getSystemToolClassLoader()).getURLs()) 25 | } 26 | 27 | shadowJar { 28 | dependencies { 29 | include(dependency('com.beust:jcommander')) 30 | } 31 | } 32 | 33 | jar { 34 | manifest { 35 | attributes( 36 | "Main-Class": "com.sk89q.spilledgrounds.SpilledGrounds", 37 | "Agent-Class": "com.sk89q.spilledgrounds.agent.ClassDumpAgent", 38 | "Can-Redefine-Classes": "true", 39 | "Can-Retransform-Classes": "true" 40 | ) 41 | } 42 | } 43 | 44 | build.dependsOn(shadowJar) 45 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sk89q/java-app-class-dumper/5e6b55e85a5fce424a6e8e3f32f3151c9dbed2e7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 22 09:30:21 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spilledgrounds' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/Arguments.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds; 2 | 3 | import com.beust.jcommander.Parameter; 4 | 5 | import java.io.File; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class Arguments { 10 | 11 | @Parameter(names = {"-h", "-?", "--help"}, help = true, description = "show help") 12 | public boolean help = false; 13 | 14 | @Parameter(names = {"-v", "--verbose"}, description = "show exceptions") 15 | public boolean verbose = false; 16 | 17 | @Parameter(names = {"-i", "--interactive"}, description = "accept interactive input of class names to dump") 18 | public boolean interactive; 19 | 20 | @Parameter(names = {"-p", "--pid"}, description = "PID of VM to connect to") 21 | public String pid; 22 | 23 | @Parameter(names = "--dir", description = "directory to dump classes to") 24 | public File dir = new File("dumps"); 25 | 26 | @Parameter(description = "list of class names to dump") 27 | public List patterns = new ArrayList(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/FatalException.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds; 2 | 3 | public class FatalException extends Exception { 4 | 5 | public FatalException() { 6 | } 7 | 8 | public FatalException(String message) { 9 | super(message); 10 | } 11 | 12 | public FatalException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public FatalException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/SimpleLogFormatter.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | import java.util.logging.*; 6 | 7 | public final class SimpleLogFormatter extends Formatter { 8 | 9 | private static final Logger log = Logger.getLogger(SimpleLogFormatter.class.getName()); 10 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 11 | 12 | @Override 13 | public String format(LogRecord record) { 14 | StringBuilder sb = new StringBuilder(); 15 | 16 | sb.append("* ") 17 | .append(formatMessage(record)) 18 | .append(LINE_SEPARATOR); 19 | 20 | if (record.getThrown() != null) { 21 | try { 22 | StringWriter sw = new StringWriter(); 23 | PrintWriter pw = new PrintWriter(sw); 24 | record.getThrown().printStackTrace(pw); 25 | pw.close(); 26 | sb.append(sw.toString()); 27 | } catch (Exception e) { 28 | } 29 | } 30 | 31 | return sb.toString(); 32 | } 33 | 34 | public static void configureGlobalLogger() { 35 | Logger globalLogger = Logger.getLogger(""); 36 | 37 | // Set formatter 38 | for (Handler handler : globalLogger.getHandlers()) { 39 | handler.setFormatter(new SimpleLogFormatter()); 40 | } 41 | 42 | // Set level 43 | String logLevel = System.getProperty( 44 | SimpleLogFormatter.class.getCanonicalName() + ".logLevel", "INFO"); 45 | try { 46 | Level level = Level.parse(logLevel); 47 | globalLogger.setLevel(level); 48 | } catch (IllegalArgumentException e) { 49 | log.log(Level.WARNING, "Invalid log level of " + logLevel, e); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/SpilledGrounds.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds; 2 | 3 | import com.beust.jcommander.JCommander; 4 | import com.sun.tools.attach.*; 5 | 6 | import javax.management.*; 7 | import javax.management.remote.JMXConnector; 8 | import javax.management.remote.JMXConnectorFactory; 9 | import javax.management.remote.JMXServiceURL; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.MalformedURLException; 13 | import java.net.URISyntaxException; 14 | import java.util.List; 15 | import java.util.NoSuchElementException; 16 | import java.util.Scanner; 17 | import java.util.logging.Level; 18 | import java.util.logging.Logger; 19 | 20 | public final class SpilledGrounds { 21 | 22 | private static final Logger log = Logger.getLogger(SpilledGrounds.class.getName()); 23 | 24 | private SpilledGrounds() { 25 | } 26 | 27 | private static VirtualMachine getVirtualMachine(String providedId) throws FatalException { 28 | try { 29 | if (providedId != null) { 30 | return VirtualMachine.attach(providedId); 31 | } else { 32 | List descriptors = VirtualMachine.list(); 33 | int index = 1; 34 | System.out.println("Choose target VM:"); 35 | for (VirtualMachineDescriptor descriptor : descriptors) { 36 | System.out.println("[" + (index++) + "] " + descriptor.displayName() + " (pid: " + descriptor.id() + ")"); 37 | } 38 | 39 | Scanner scanner = new Scanner(System.in); 40 | try { 41 | int choice = scanner.nextInt(); 42 | return VirtualMachine.attach(descriptors.get(choice - 1)); 43 | } catch (IndexOutOfBoundsException | NoSuchElementException e) { 44 | throw new FatalException("Unknown choice selected!"); 45 | } 46 | } 47 | } catch (AttachNotSupportedException | IOException e) { 48 | throw new FatalException("Could not attach to Java virtual machine", e); 49 | } 50 | } 51 | 52 | private static void loadAgent(VirtualMachine vm) throws FatalException { 53 | try { 54 | String agentPath = new File(SpilledGrounds.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()).getAbsolutePath(); 55 | if (vm.getSystemProperties().getProperty("com.sk89q.spilledgrounds.agent.installed") == null) { 56 | log.info("Trying to install the Java agent..."); 57 | vm.loadAgent(agentPath); 58 | } else { 59 | log.info("Java agent already installed in target process (nothing needs to be done)"); 60 | } 61 | } catch (AgentInitializationException | IOException | AgentLoadException e) { 62 | throw new FatalException("Failed to install the SpilledGrounds Java agent into the target process", e); 63 | } catch (URISyntaxException e) { 64 | throw new FatalException("Failed to get the path to the agent .jar file that would be installed into the target process", e); 65 | } 66 | } 67 | 68 | private static String getJMXConnectionAddress(VirtualMachine vm) throws FatalException { 69 | String address; 70 | try { 71 | address = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null); 72 | } catch (IOException e) { 73 | throw new FatalException("Failed to connect to the target process to get agent properties", e); 74 | } 75 | if (address != null) { 76 | return address; 77 | } else { 78 | try { 79 | String javaHome = vm.getSystemProperties().getProperty("java.home"); 80 | File managementAgentJarFile = new File(javaHome + File.separator + "lib" + File.separator + "management-agent.jar"); 81 | vm.loadAgent(managementAgentJarFile.getAbsolutePath()); 82 | return vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null); 83 | } catch (AgentInitializationException | AgentLoadException | IOException e) { 84 | throw new FatalException("Failed to start the JMX agent in the target process", e); 85 | } 86 | } 87 | } 88 | 89 | private static void dumpClasses(String address, List classNames, File outputDir, boolean interactive) throws FatalException { 90 | JMXConnector connector = null; 91 | try { 92 | ObjectName objectName = new ObjectName("spilledgrounds:type=ClassDumper"); 93 | connector = JMXConnectorFactory.connect(new JMXServiceURL(address)); 94 | MBeanServerConnection server = connector.getMBeanServerConnection(); 95 | 96 | for (String className : classNames) { 97 | dumpClass(server, objectName, className, outputDir); 98 | } 99 | 100 | if (interactive) { 101 | Scanner scanner = new Scanner(System.in); 102 | String line; 103 | 104 | log.info("Interactive mod started. Enter patterns to match classes, line by line:"); 105 | 106 | while (true) { 107 | line = scanner.nextLine().trim(); 108 | if (line.equalsIgnoreCase("exit") || line.equalsIgnoreCase("quit")) { 109 | break; 110 | } else if (!line.isEmpty()) { 111 | dumpClass(server, objectName, line, outputDir); 112 | } 113 | } 114 | } 115 | } catch (MalformedObjectNameException | MalformedURLException | ReflectionException e) { 116 | throw new RuntimeException("Unexpected error", e); 117 | } catch (InstanceNotFoundException e) { 118 | throw new FatalException("The class dumping MBean was installed into the target process (supposedly) but now it can't be found", e); 119 | } catch (IOException e) { 120 | throw new FatalException("Failed to connect to target process", e); 121 | } finally { 122 | if (connector != null) { 123 | try { 124 | connector.close(); 125 | } catch (Exception ignored) { 126 | } 127 | } 128 | } 129 | } 130 | 131 | private static void dumpClass(MBeanServerConnection server, ObjectName objectName, String className, File outputDir) throws InstanceNotFoundException, IOException, ReflectionException { 132 | try { 133 | log.info("Trying to dump " + className + "..."); 134 | server.invoke(objectName, "dumpClass", 135 | new Object[] { className, outputDir.getAbsolutePath() }, 136 | new String[]{ String.class.getName(), String.class.getName() }); 137 | } catch (MBeanException e) { 138 | log.log(Level.WARNING, "Failed to dump class (an exception was raised by the class dumper)", e); 139 | } 140 | } 141 | 142 | public static void main(String[] args) { 143 | SimpleLogFormatter.configureGlobalLogger(); 144 | 145 | Arguments parsed = new Arguments(); 146 | JCommander jCommander = new JCommander(parsed, args); 147 | jCommander.setProgramName("spilledbeans"); 148 | if (parsed.help) { 149 | jCommander.usage(); 150 | return; 151 | } 152 | 153 | try { 154 | VirtualMachine vm = getVirtualMachine(parsed.pid); 155 | if (!parsed.interactive && parsed.patterns.isEmpty()) { 156 | log.info("Interactive mode not enabled and no class patterns to dump provided, so the Java agent will be installed but then this program will quit."); 157 | } 158 | loadAgent(vm); 159 | String jmxAddress = getJMXConnectionAddress(vm); 160 | dumpClasses(jmxAddress, parsed.patterns, parsed.dir, parsed.interactive); 161 | } catch (FatalException e) { 162 | if (e.getCause() != null) { 163 | if (parsed.verbose) { 164 | log.log(Level.SEVERE, "A fatal error occurred", e); 165 | } else { 166 | log.severe("ERROR: " + e.getMessage()); 167 | System.err.println("Use --verbose to see error details"); 168 | } 169 | } else { 170 | log.severe("ERROR: " + e.getMessage()); 171 | } 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/agent/ClassDump.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds.agent; 2 | 3 | import java.io.File; 4 | import java.lang.instrument.Instrumentation; 5 | import java.lang.instrument.UnmodifiableClassException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | import java.util.regex.Pattern; 11 | 12 | public class ClassDump implements ClassDumpMBean { 13 | 14 | private static final Logger log = Logger.getLogger(ClassDump.class.getName()); 15 | private final Instrumentation instrumentation; 16 | 17 | public ClassDump(Instrumentation instrumentation) { 18 | this.instrumentation = instrumentation; 19 | } 20 | 21 | @Override 22 | public void dumpClass(String patternString, String dirName) { 23 | Pattern pattern = Pattern.compile(patternString); 24 | File dir = new File(dirName); 25 | List> classes = new ArrayList<>(); 26 | 27 | for (Class clazz : instrumentation.getAllLoadedClasses()) { 28 | if (pattern.matcher(clazz.getName()).matches()) { 29 | classes.add(clazz); 30 | } 31 | } 32 | 33 | if (!classes.isEmpty()) { 34 | transform(pattern, classes, dir); 35 | } else { 36 | log.log(Level.WARNING, "(SpilledBeans) Failed to match any classes against " + patternString); 37 | } 38 | } 39 | 40 | private void transform(Pattern pattern, List> classes, File dir) { 41 | ClassDumpTransformer transformer = new ClassDumpTransformer(pattern, dir); 42 | instrumentation.addTransformer(transformer, true); 43 | try { 44 | for (Class clazz : classes) { 45 | try { 46 | instrumentation.retransformClasses(clazz); 47 | } catch (UnmodifiableClassException e) { 48 | log.log(Level.WARNING, "Unexpected error", e); 49 | } 50 | } 51 | } finally { 52 | instrumentation.removeTransformer(transformer); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/agent/ClassDumpAgent.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds.agent; 2 | 3 | import javax.management.ObjectName; 4 | import java.lang.instrument.Instrumentation; 5 | import java.lang.management.ManagementFactory; 6 | import java.util.logging.Logger; 7 | 8 | public final class ClassDumpAgent { 9 | 10 | private static final Logger log = Logger.getLogger(ClassDumpAgent.class.getName()); 11 | 12 | private ClassDumpAgent() { 13 | } 14 | 15 | public static void agentmain(String args, Instrumentation inst) throws Exception { 16 | ClassDump provider = new ClassDump(inst); 17 | ObjectName objectName = new ObjectName("spilledgrounds:type=ClassDumper"); 18 | ManagementFactory.getPlatformMBeanServer().registerMBean(provider, objectName); 19 | log.info("Installed SpilledBeans class file dumper into the platform MBeanServer as " + objectName.getCanonicalName()); 20 | System.setProperty("com.sk89q.spilledgrounds.agent.installed", "true"); 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/agent/ClassDumpMBean.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds.agent; 2 | 3 | public interface ClassDumpMBean { 4 | 5 | void dumpClass(String patternString, String dir); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/sk89q/spilledgrounds/agent/ClassDumpTransformer.java: -------------------------------------------------------------------------------- 1 | package com.sk89q.spilledgrounds.agent; 2 | 3 | import java.io.BufferedOutputStream; 4 | import java.io.File; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.lang.instrument.ClassFileTransformer; 8 | import java.lang.instrument.IllegalClassFormatException; 9 | import java.security.ProtectionDomain; 10 | import java.util.logging.Level; 11 | import java.util.logging.Logger; 12 | import java.util.regex.Pattern; 13 | 14 | public class ClassDumpTransformer implements ClassFileTransformer { 15 | 16 | private static final Logger log = Logger.getLogger(ClassDumpTransformer.class.getName()); 17 | private final Pattern pattern; 18 | private final File outputDir; 19 | 20 | public ClassDumpTransformer(Pattern pattern, File outputDir) { 21 | this.pattern = pattern; 22 | this.outputDir = outputDir; 23 | } 24 | 25 | @Override 26 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 27 | if (pattern.matcher(className.replace("/", ".")).matches()) { 28 | File file = new File(outputDir, className + ".class"); 29 | file.getAbsoluteFile().getParentFile().mkdirs(); 30 | try (FileOutputStream fos = new FileOutputStream(file); BufferedOutputStream bos = new BufferedOutputStream(fos)) { 31 | bos.write(classfileBuffer); 32 | log.info("Wrote class dump to " + file.getAbsolutePath()); 33 | } catch (IOException e) { 34 | log.log(Level.WARNING, "Failed to write to " + file, e); 35 | } 36 | } 37 | 38 | return null; 39 | } 40 | 41 | } 42 | --------------------------------------------------------------------------------