kwhat / jnativehook

Global keyboard and mouse listeners for Java.
Other
1.73k stars 344 forks source link

UnsatisfiedLinkError #148

Open kingarchie opened 7 years ago

kingarchie commented 7 years ago

Hi there,

Exception in thread "main" java.lang.UnsatisfiedLinkError: org.jnativehook.GlobalScreen.getAutoRepeatRate()Ljava/lang/Integer

Why do I get this error?

kwhat commented 7 years ago

Hi,

Did you attempt to compile from source? I suspect that you compiled the Java code but not the native C code. There are full instructions for compiling everything for all supported platforms in the Wiki. Let me know if you have any questions or that does not solve your issue.

Best, Alex

kingarchie commented 7 years ago

I compiled using the JAR - I'll read the wiki. The problem is, is that it has worked for a lot of users but doesn't work for one.

kingarchie commented 7 years ago

Hi there.

All I did was add the JAR to my modules (IntelliJ) and compile the whole program. It works perfectly - yet only one user has this error.

kwhat commented 7 years ago

Ahh, so one of your users is having trouble, but it is working for you? So the Jar that I distribute contains the required binaries for all supported platforms and goes though a somewhat complicated procedure to automatically "lazy-load" the included binary code. (Take a look at the DefaultLibraryLocator code) The error you are seeing suggests that the loading process failed for what could be a number of reasons. Most commonly, the users that experience issues do not have write access to the location provided by the Java property java.io.tmpdir. This is really common on Windows machines that are locked down to unreasonable degree though some kind of Active Directory policy. You can work around the issue in a number of ways:

  1. Provide a java.io.tmpdir location that the user has access to write to.
  2. Extract the correct binary library for the users host system to some location and then specify the java property java.library.path to point to that location. This is the preferred Sun/Oracle method.
  3. Create your own custom library locator class that either extends org.jnativehook.DefaultLibraryLocator or implements the org.jnativehook.NativeLibraryLocator interface. You can specify which class is used to load the native library using the jnativehook.lib.locator Java property.
kwhat commented 7 years ago

See if you cant get a complete stack trace, that may help identify what is happening.

kingarchie commented 7 years ago

Hmm - I think #1 is the best option.

What files are left over from JNativeHook when the application closes? Can you tell me how to actually fix it? What code to use? I literally don't understand. Cheers.

kwhat commented 7 years ago

Look in the java.io.tmpdir for a dynamic library (dll, so or dylib) called "JNativeHook*.dll". The library prefix comes form the property jnativehook.lib.name which defaults to "JNativeHook" and the version information at the end is extracted from the Manifest of the jar or the java property jnativehook.lib.version and if that all fails the sha1 sum is used. The easiest way for you to understand what is going on would be to start reading here and following the code. I can help with with bits of that process you get stick on, but it would probably take 4 or 5 pints of beer to explain the entire thing ;)

kwhat commented 7 years ago

Note, that the static block of code executes when the Jar is loaded into the JVM and before main(...) actually runs.

kwhat commented 7 years ago

If the library is successfully extracted, and you are still seeing that error, it could also be an ABI mismatch.

kingarchie commented 7 years ago

Haha, I see - the temp files. How could I provide someone "access" to create a temp file? Also is there anyway I could make these files .deleteOnExit()?

kwhat commented 7 years ago

I use to do the .deleteOnExit() near here but I started experiencing other "interesting issues" when two instances of the library were running, and one instances terminated before the other. If you want to remove the library on exit, override the default library name with the jnativehook.lib.name property to ensure you don't mess with someone else's program and use your own NativeLibraryLocator class to extract the library. You can probably figure out a way to do this fairly easily by extending the DefaultLibraryLocator, calling super.getLibraries() and adding your own cleanup mechanism.

kwhat commented 7 years ago

Also, remember that you need to tell the GlobalScreen which locator to use with String libLoader = System.getProperty("jnativehook.lib.locator", "org.jnativehook.DefaultLibraryLocator");

kwhat commented 7 years ago

java -Djnativehook.lib.locator="com.kingArchie.AwesomeSauceLibraryLocator" your.jar should do the trick.

kingarchie commented 7 years ago

How would I do this automatically? https://gist.github.com/kingArchie/0947b2e8df5a75dcb6003742d24c9f3e

kingarchie commented 7 years ago

Would that method work? Or how would I add my own file and make GlobalScreen use that lib.?

kwhat commented 7 years ago

Yes, that's pretty much exactly what you want. Just remember, you can only have 1 app instance running or you will have the same problem I did. You can just add something like this to that class:

public class LibraryLocator extends DefaultLibraryLocator {
    static {
        // Be Sure the class is set with the Fully Qualified Class Name.
        System.setProperty("jnativehook.lib.locator", "com.KingArchie.LibraryLocator");
    }
    public LibraryLocator() {
        for (Iterator<File> it = super.getLibraries(); it.hasNext(); ) {
            File f = it.next();
            f.deleteOnExit();
        }
    }
}
kingarchie commented 7 years ago

http://prntscr.com/eagvc2 - this is the dll that is created. Is this right? It didn't delete on exit for some reason.

kwhat commented 7 years ago

that is the class... hmm probably because its still in use while the jvm is running ;( #WindowsProblems

kwhat commented 7 years ago

Delete it before you call super. Write a little bit of code that looks at the location for files with similar names. You can control the library prefix with System.setProperty("jnativehook.lib.name", "KingArchie"); in the same static block

kingarchie commented 7 years ago

Yeah. Thought so. Would this fix the problem that the person was having? Is it possible to create a batch file to deleteOnExit?

kwhat commented 7 years ago

I am not sure if it will fix the problem that user is having because I dont know exactly what is happening on their system. Is there a full stack trace?

kwhat commented 7 years ago

You could probably safely delete all libs that start with JNativeHook also, but only on windows.

kingarchie commented 7 years ago

https://gyazo.com/2ff3c3f19bb00315c20b9fa287942a2d that is the stacktrace I was sent.

I was thinking - if I unregister the nativehook before the shut down, could I then delete the files on exit?

kingarchie commented 7 years ago

Windows is all my program will run on.

kingarchie commented 7 years ago
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    GlobalScreen.unregisterNativeHook();
                } catch (NativeHookException e) {
                    e.printStackTrace();
                }

                for (File f : temps) {
                    f.deleteOnExit();
                }
            }
        }));
kwhat commented 7 years ago

Unregistering the running hook wont help. You really need a System.unloadLibrary(libName) or System.unload(libPath); but as far as I know, no such method is provided by the JVM. You maybe able to do something super sketchy with FreeLibrary in the native code... but that could have some serious unintended consequences because there is no way to know what the JVM is doing with the module reference after it's unloaded. If the user in question removes all libs from the temp location, and is still having the same issue, then there maybe something else going on here. The unsatisfied link error is basically the JVM saying that it cannot run the Jar because it could not find the native method in question. The reason that method was not found is obscured by the loading process and the JVM. It could be that it could not find the library to load, but that should have triggered the log.severe(e.getMessage()); exception in the output of the nested catch in the GlobalScreen static block. If you didn't mess with the Log level, it would show up right above where the screenshot you sent cuts off. This would be reinforced by some warning level messages above that error as well. Maybe something like "Unable to extract the native library ..." Other possibilities would be some kind of runtime linking error like a missing dependency on his system, overzealous antivirus, application binary interface miss-match (32-bit dll on a 64-bit JVM), or something I am not quite thinking for right now. Your best bet would be to set the log level to something like debug, and get to full console output of the crash. At this point we are just speculating.

kingarchie commented 7 years ago

I stopped it posting to console. Alright. I understand, thank you for your time.

kwhat commented 7 years ago

No problem, I leave this open for a while. If you get some more info, just post back here. I would recommend keeping the log level at at least Warning so that you get some important messaging. I have tried to make the warnings and errors only come up in critical situations. If its getting to noisy, let me know.

kingarchie commented 7 years ago

Alright, great.

kwhat commented 7 years ago

Alright, a little update... it may be possible.

Try using a similar library locator:

public class LibraryLocator extends DefaultLibraryLocator {
    static {
        // Be Sure the class is set with the Fully Qualified Class Name.
        System.setProperty("jnativehook.lib.locator", "com.KingArchie.LibraryLocator");
    }

    public void finalize() {
        for (Iterator<File> it = super.getLibraries(); it.hasNext(); ) {
            File f = it.next();
            f.delete();
        }
    }
}

Add a custom class loader:

/**
 * 
 * @author http://codeslices.net team
 * 
 */

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * 
 * Simple custom class loader implementation
 * 
 */
public class CustomClassLoader extends ClassLoader {

    /**
     * The HashMap where the classes will be cached
     */
    private Map<String, Class<?>> classes = new HashMap<String, Class<?>>();

    @Override
    public String toString() {
        return CustomClassLoader.class.getName();
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        if (classes.containsKey(name)) {
            return classes.get(name);
        }

        byte[] classData;

        try {
            classData = loadClassData(name);
        } catch (IOException e) {
            throw new ClassNotFoundException("Class [" + name
                    + "] could not be found", e);
        }

        Class<?> c = defineClass(name, classData, 0, classData.length);
        resolveClass(c);
        classes.put(name, c);

        return c;
    }

    /**
     * Load the class file into byte array
     * 
     * @param name
     *            The name of the class e.g. com.codeslices.test.TestClass}
     * @return The class file as byte array
     * @throws IOException
     */
    private byte[] loadClassData(String name) throws IOException {
        BufferedInputStream in = new BufferedInputStream(
                ClassLoader.getSystemResourceAsStream(name.replace(".", "/")
                        + ".class"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int i;

        while ((i = in.read()) != -1) {
            out.write(i);
        }

        in.close();
        byte[] classData = out.toByteArray();
        out.close();

        return classData;
    }
}

Then do some magic:

public static void main(String[] args) throws Exception {
    CustomClassLoader cl = new CustomClassLoader();
    Class<T> screen = cl.findClass("org.jnativehook.GlobalScreen");
    Method m = screen.getMethod("registerHook", screen);
    m = null;
    screen = null;
    cl = null;
    System.gc();
}

I literally just pulled this out of my ass and haven't tested any of it, however, the approach should work for using the custom class loader to find the class and then deleting files on GC.

kingarchie commented 7 years ago

I'll test this tonight!

kingarchie commented 7 years ago

I assume the last bit I would do on a shutdown hook? Like... m = null etc.?

kingarchie commented 7 years ago

Cannot resolve symbol T

kingarchie commented 7 years ago

java.lang.NoSuchMethodException: org.jnativehook.GlobalScreen.registerHook(org.jnativehook.GlobalScreen)

I tried changing registerHook to registerNativeHook - same thing.