Better selection of natives extraction directory

As mentioned in this post, WebStart launch under Firefox currently fails due to the current directory being the Firefox program directory.



Rather than continuing to litter libraries around the filesystem, I think it is better to choose one location for them. This patch attempts to use the following paths, in order, and chooses the first one that is writeable:



the directory manually set by the developer using Natives.setExtractionDir(), if any

{user.home}/.jme3-natives

{java.io.tmpdir}/.jme3-natives

previous method of using jar’s parent directory

{user.dir} (current working directory)



The only problem I can see with this is if the user has two jME3 programs running simultaneously and they require different native library versions (which would have been a problem anyway if they were run from the same directory, e.g. Downloads directory).



If this is perceived to be a problem, the extraction could be changed to use a temporary filename for extracting each library on each run, and delete these temp files using a shutdown hook.



[patch]

Index: src/desktop/com/jme3/system/Natives.java

===================================================================

— src/desktop/com/jme3/system/Natives.java (revision 6661)

+++ src/desktop/com/jme3/system/Natives.java (revision )

@@ -39,7 +39,8 @@

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

-import java.net.MalformedURLException;

+import java.net.URI;

+import java.net.URISyntaxException;

import java.net.URL;

import java.util.logging.Level;

import java.util.logging.Logger;

@@ -52,10 +53,81 @@



private static final Logger logger = Logger.getLogger(Natives.class.getName());

private static final byte[] buf = new byte[1024];

  • private static File workingDir = new File("").getAbsoluteFile();
  • private static File extractionDir = null;



    +
  • protected static void autoDetectExtractionDir() {

    +
  •    for (int i = 0; i &lt; 4; ++i) {<br />
    
  •        File dir = null;<br />
    
  •        switch (i) {<br />
    
  •            case 0:<br />
    
  •                dir = new File(System.getProperty(&quot;user.home&quot;), &quot;.jme3-natives&quot;);<br />
    
  •                dir.mkdirs(); // don't check return result, let testDirectoryWriteable handle it<br />
    
  •                break;<br />
    
  •            case 1:<br />
    
  •                dir = new File(System.getProperty(&quot;java.io.tmpdir&quot;, &quot;.jme3-natives&quot;));<br />
    
  •                dir.mkdirs(); // don't check return result, let testDirectoryWriteable handle it<br />
    
  •                break;<br />
    
  •            case 2:<br />
    
  •                dir = getParentDir();<br />
    
  •                break;<br />
    
  •            case 3:<br />
    
  •                dir = new File(System.getProperty(&quot;user.dir&quot;));<br />
    
  •                break;<br />
    
  •        }<br />
    

+

  •        logger.log(Level.INFO, &quot;Extraction Directory #&quot; + (i + 1) + &quot;: &quot; + dir);<br />
    
  •        if (dir == null) continue;<br />
    

+

  •        if (testDirectoryWriteable(dir)) {<br />
    
  •            extractionDir = dir.getAbsoluteFile();<br />
    
  •            logger.log(Level.INFO, &quot;Chosen Extraction Directory: &quot; + dir);<br />
    
  •            return;<br />
    
  •        }<br />
    
  •    }<br />
    

+

  •    logger.log(Level.SEVERE, &quot;Unable to find a writeable extraction directory.&quot;);<br />
    
  •    throw new RuntimeException(&quot;Unable to find a writeable directory to extract native libraries.&quot;);<br />
    
  • }

    +

    +
  • /**
  • * Test that the given directory exists and can be written in.<br />
    
  • *<br />
    
  • * @param dir the directory to test.<br />
    
  • * @return true if the directory exists and is writeable<br />
    
  • */<br />
    
  • public static boolean testDirectoryWriteable(File dir) {
  •    if (!dir.isDirectory()) {<br />
    
  •        return false;<br />
    
  •    }<br />
    

+

  •    File testFile;<br />
    
  •    try {<br />
    
  •        testFile = File.createTempFile(&quot;jme3&quot;, null, dir);<br />
    

+

  •        //test write<br />
    
  •        FileOutputStream os = new FileOutputStream(testFile);<br />
    
  •        try {<br />
    
  •            os.write(65);<br />
    
  •        } finally {<br />
    
  •            os.close();<br />
    
  •        }<br />
    

+

  •        if (!testFile.delete()) {<br />
    
  •            logger.log(Level.WARNING, &quot;Unable to delete test file &quot; + testFile);<br />
    
  •            return false;<br />
    
  •        }<br />
    

+

  •        return true;<br />
    
  •    } catch (IOException e) {<br />
    
  •        return false;<br />
    
  •    }<br />
    
  • }

    +

    public static void setExtractionDir(String name){
  •    workingDir = new File(name).getAbsoluteFile();<br />
    
  •    extractionDir = new File(name).getAbsoluteFile();<br />
    

}



protected static void extractNativeLib(String sysName, String name) throws IOException{

@@ -65,11 +137,11 @@

InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);

//InputStream in = Natives.class.getResourceAsStream();

if (in == null) {

  •        logger.log(Level.WARNING, &quot;Cannot locate native library: {0}/{1}&quot;,<br />
    
  •        logger.log(Level.WARNING, &quot;Cannot locate native library: {0}/{1}&quot;,<br />
    

new String[]{ sysName, fullname} );

return;

}

  •    File targetFile = new File(workingDir, fullname);<br />
    
  •    File targetFile = new File(extractionDir, fullname);<br />
    

try {

OutputStream out = new FileOutputStream(targetFile);

int len;

@@ -86,9 +158,11 @@

}



logger.log(Level.FINE, "Copied {0} to {1}", new Object[]{fullname, targetFile});

+

  •    Runtime.getRuntime().load(targetFile.getAbsolutePath());<br />
    

}


  • private static String getExtractionDir(){
  • private static File getParentDir() {

    URL temp = Natives.class.getResource("");

    if (temp != null) {

    StringBuilder sb = new StringBuilder(temp.toString());

    @@ -98,11 +172,17 @@

    sb.delete(sb.lastIndexOf("/") + 1, sb.length());

    }

    try {
  •            return new URL(sb.toString()).toString();<br />
    
  •        } catch (MalformedURLException ex) {<br />
    
  •            URI uri = new URI(sb.toString());<br />
    
  •            String scheme = uri.getScheme();<br />
    
  •            if (scheme != null &amp;&amp; scheme.equalsIgnoreCase(&quot;file&quot;)) {<br />
    
  •                return new File(uri);<br />
    
  •            } else {<br />
    
  •            return null;<br />
    
  •        }<br />
    
  •                return null;<br />
    
  •            }<br />
    
  •        } catch (URISyntaxException e) {<br />
    
  •            return null;<br />
    
  •    }<br />
    
  •        }<br />
    
  •    }<br />
    

return null;

}



@@ -135,15 +215,19 @@

}

needJInput = settings.useJoysticks();


  •    if (extractionDir == null) {<br />
    
  •        autoDetectExtractionDir();<br />
    
  •    }<br />
    

+

if (needLWJGL){

  •        logger.log(Level.INFO, &quot;Extraction Directory #1: {0}&quot;, getExtractionDir());<br />
    
  •        logger.log(Level.INFO, &quot;Extraction Directory #2: {0}&quot;, workingDir.toString());<br />
    
  •        logger.log(Level.INFO, &quot;Extraction Directory #3: {0}&quot;, System.getProperty(&quot;user.dir&quot;));<br />
    

// LWJGL supports this feature where

// it can load libraries from this path.

// This is a fallback method in case the OS doesn’t load

// native libraries from the working directory (e.g Linux).

  •        System.setProperty(&quot;org.lwjgl.librarypath&quot;, workingDir.toString());<br />
    
  •        System.setProperty(&quot;org.lwjgl.librarypath&quot;, extractionDir.toString());<br />
    

}



switch (platform){

[/patch]





-davidc

Personally I dont like when applications just create files in my home directory. I think the current directory should be the first way to go. As you say, there might be different versions of jME3 games and we don’t want them to clash. The other paths really should only come into play when theres permission issues or similar.



Practically, a released game would create the dll next to its exe file on windows (which is not perceivable by the user who uses a start menu entry) and on OSX it is stored inside the application package. Both locations are exactly where the file is supposed to be and can be written normally. Same for Linux when you use a desktop manager.

Well, feel free to reorder the switch then if that’s what you want. I just find it a bit ugly that a WebStart game with one visible JNLP file (and jars hidden somewhere in Java’s cache) should litter a user’s Downloads or Desktop directory with mysterious and perhaps worrying DLL files. Maybe a compromise would be to prefer temp over home, i.e. swap cases 0 and 1.



A released game in Program Files or /Applications wouldn’t be writeable to a normal user though, or are you suggesting that the developer ought to put them there with their installer and load the natives manually? In that case there needs to be a way to disable the call to Natives.extractNativeLibs().



-davidc

davidc said:
A released game in Program Files or /Applications wouldn't be writeable to a normal user though

What?? A program cannot write to its own folder on windows? That must be a joke, there has to be some way. For OSX, the rights in the Application bundle are kept, extraction to that "folder" works fine.

A program runs with the permissions of the user who is running it.



Under Windows versions with UAC (Vista onward), or previous versions where the user isn’t an administrator, the user cannot write to Program Files without elevating or Run As administrator.



The same applies to Mac OS X as far as I’m aware; a consent dialog is popped up to install into /Applications if the user isn’t an administrator.



Same on Linux, a normal user would need to su/sudo to write to /usr/games for example.



David

On OSX and Linux (and I hope on windows as well) you can normally specify the rights for the applications subfolders. Its true it cannot write to the Application folder itself but thats not necessary.