3

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.

5
  • If your Web application creates the file, then your Web application should delete it when it is no longer needed. Or did I misunderstand your problem? Commented Dec 28, 2024 at 8:55
  • @Abra the problem seems to be that they cannot find which class creates the temporary file. The application may be running for long enough time that the file should be deleted before exiting the application. AFAIU at least. Commented Dec 28, 2024 at 9:00
  • 1
    Using File/Path.createNewFile with a meaningful prefix parameter per web app / class helps with this type of issue. And of course, try with resources handling. Commented Dec 28, 2024 at 10:53
  • 1
    This other post may be similar. Commented Dec 28, 2024 at 12:13
  • 2
    The easy way would be using a unique naming scheme for temporary files in each or even more granular in each package or class. Then you would directly see from the file name where the problem is located. Alternatively add logging code that records each created file and the class that has created it. Commented Dec 28, 2024 at 16:34

1 Answer 1

2

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.