Search code examples
javafile-descriptor

How to show which instance has the file descriptor of a specific file in Java


Background

  • I am writing a web application on Windows. This application consists of two or more WARs.
  • These WARs make temporary files in processing.

Problem

  • In program testing, I've found a temporary file is still remains and not deleted. I tried to delete this file from Explorer, but I got the message like The action cannot be completed because the file is open in "java.exe".
  • It is obvious that one of the WARs is still opening the file (because the message says java.exe). But there are two or more WARs on Tomcat, so I couldn't find which application caused this problem.
  • Additionally, these applications are so complecated, it is tough to dig into which class reads/writes (FileInputStream/FileOutputStream, for example) this this file.

Question

Starting with the path of a specific file, is there any way to know which instance of a class has the file descriptor(FileInputStream/FileOutputStream of the file?

A method applicable without shutdown Tomcat (like jcmd) is preferable because other WARs are being tested on the same Tomcat.


Solution

  • I presume you reproduced locally. Doing anything I suggest below in prod is not a great idea (could be too slow).

    I also presume that you can use the location where it was created, even if you cannot find which instance is holding it (because you could figure how it got shared).

    If you are capable of a debugging session, you might be able to add a conditional breakpoint on a few jdk classes that can create tmp files. That would be my first shot. If it is rare enough.

    Otherwise, in the following shots, you need to edit the webserver launcher script.

    My 2nd shot is it's very intrusive and risky; it would be to redefine boot classes, but you need the source and to prepend a bootclasspath to ensure your redefinition class is loaded first. It used to work 15 years ago, dunno today with modules and sealed packages and digital signature and all...I used to log on Thread.start() to see who called it. Would be similar for a File.createTempFile() or similar call sites.

    3rd shot works: instrument those call sites; write a pure java agent to load on the java.exe command line. Fairly good tutorials online. Not that hard, less than a few hours to goal like I just did and it's so cool. You intercept the method call's exit and print the filename created +stacktrace.

    Making an instrumentation agent goes like this:

    a) download javassist.jar (inside the javassist latest zip) and put it in a lib/ folder for example.

    b) construct a manifest, ex: myagent.mf

    Manifest-Version: 1.0
    Main-Class: tests.MyInstrumentationAgent
    Agent-Class: tests.MyInstrumentationAgent
    Premain-Class: tests.MyInstrumentationAgent
    Class-Path: lib/javassist.jar
    Can-Redefine-Classes: true
    Can-Retransform-Classes: true
    

    c) write the agent class and jar it with the manifest. Ex:

    package tests;
    import java.io.ByteArrayInputStream;
    import java.io.File;
    import java.io.IOException;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.lang.instrument.Instrumentation;
    import java.security.ProtectionDomain;
    
    import javassist.ClassPool;
    import javassist.CtClass;
    import javassist.CtMethod;
    
    public class MyInstrumentationAgent {
        public static void premain(String agentArgs, Instrumentation inst) {
            System.out.println("Executing premain with args = '"+agentArgs+"'");
            try {
                inst.addTransformer(new MyClassTransformer(), true);
                inst.retransformClasses(File.class);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        public static class MyClassTransformer implements ClassFileTransformer {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (!"java/io/File".equals(className))
                    return classfileBuffer;
                
                System.out.println("Instrumenting " + className + " ...");
                byte[] byteCode = classfileBuffer;
                try {
                    ClassPool classPool = ClassPool.getDefault();
                    CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
                    CtMethod m = ctClass.getMethod("createTempFile", "(Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Ljava/io/File;");
                    if (m != null)
                        m.insertAfter("""
                            Throwable t = new Throwable(
                                "["+new java.util.Date()
                                +"] ["+Thread.currentThread().getName()
                                +"]: new temp file '"
                                + $_ +"' created");
                            t.printStackTrace(System.out);
                            """
                        );
                    else
                        System.out.println("method not found");
                    
                    byteCode = ctClass.toBytecode();
                    ctClass.detach();
                } catch (Throwable t) {
                    t.printStackTrace(System.out);
                }
                return byteCode;
            }
        }
        
        public static void main(String[] args) throws IOException {
            File tmp = File.createTempFile("myagent_demo_", ".tmp", new File("."));
            tmp.deleteOnExit();
            System.out.println("created "+tmp);
        }
    }
    

    d) launch with the agent: (making sure you have the lib/javassist.jar (or whichever class-path you might adjust) relative to the myagent.jar)

    java -javaagent:myagent.jar ......
    

    The output of its own main() looks like this:

    Executing premain with args = 'null'
    Instrumenting java/io/File ...
    java.lang.Throwable: [Sat Dec 28 11:56:29 EST 2024] [main]: new temp file '.\myagent_demo_9579502737453321678.tmp' created
        at java.base/java.io.File.createTempFile(File.java:2173)
        at tests.MyInstrumentationAgent.main(MyInstrumentationAgent.java:60)
    created .\myagent_demo_9579502737453321678.tmp
    

    Good luck.

    Update: just a note on the added code in .insertAfter(): it's tempting to call one of your own delegate class static method to make it lighter to embed, but in the case here, we are instrumenting a boot class, so its classloader will not see your delegate class (unless you put it in the boot classpath I guess). This is why I only print out instead of trying to append some file.