001/*--------------------------------------------------------------------------
002 *  Copyright 2007 Taro L. Saito
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 *--------------------------------------------------------------------------*/
016package org.fusesource.jansi.internal;
017
018import java.io.File;
019import java.io.FileInputStream;
020import java.io.FileOutputStream;
021import java.io.FilenameFilter;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.net.URL;
026import java.util.Arrays;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Properties;
030import java.util.Random;
031
032import org.fusesource.jansi.AnsiConsole;
033
034/**
035 * Set the system properties, org.jansi.lib.path, org.jansi.lib.name,
036 * appropriately so that jansi can find *.dll, *.jnilib and
037 * *.so files, according to the current OS (win, linux, mac).
038 * <p>
039 * The library files are automatically extracted from this project's package
040 * (JAR).
041 * <p>
042 * usage: call {@link #initialize()} before using Jansi.
043 */
044public class JansiLoader {
045
046    private static boolean loaded = false;
047    private static String nativeLibraryPath;
048    private static String nativeLibrarySourceUrl;
049
050    /**
051     * Loads Jansi native library.
052     *
053     * @return True if jansi native library is successfully loaded; false
054     * otherwise.
055     */
056    public static synchronized boolean initialize() {
057        // only cleanup before the first extract
058        if (!loaded) {
059            cleanup();
060        }
061        try {
062            loadJansiNativeLibrary();
063        } catch (Exception e) {
064            if (!Boolean.parseBoolean(System.getProperty(AnsiConsole.JANSI_GRACEFUL, "true"))) {
065                throw new RuntimeException("Unable to load jansi native library. You may want set the `jansi.graceful` system property to true to be able to use Jansi on your platform", e);
066            }
067        }
068        return loaded;
069    }
070
071    public static String getNativeLibraryPath() {
072        return nativeLibraryPath;
073    }
074
075    public static String getNativeLibrarySourceUrl() {
076        return nativeLibrarySourceUrl;
077    }
078
079    private static File getTempDir() {
080        return new File(System.getProperty("jansi.tmpdir", System.getProperty("java.io.tmpdir")));
081    }
082
083    /**
084     * Deleted old native libraries e.g. on Windows the DLL file is not removed
085     * on VM-Exit (bug #80)
086     */
087    static void cleanup() {
088        String tempFolder = getTempDir().getAbsolutePath();
089        File dir = new File(tempFolder);
090
091        File[] nativeLibFiles = dir.listFiles(new FilenameFilter() {
092            private final String searchPattern = "jansi-" + getVersion();
093
094            public boolean accept(File dir, String name) {
095                return name.startsWith(searchPattern) && !name.endsWith(".lck");
096            }
097        });
098        if (nativeLibFiles != null) {
099            for (File nativeLibFile : nativeLibFiles) {
100                File lckFile = new File(nativeLibFile.getAbsolutePath() + ".lck");
101                if (!lckFile.exists()) {
102                    try {
103                        nativeLibFile.delete();
104                    } catch (SecurityException e) {
105                        System.err.println("Failed to delete old native lib" + e.getMessage());
106                    }
107                }
108            }
109        }
110    }
111
112    private static int readNBytes(InputStream in, byte[] b) throws IOException {
113        int n = 0;
114        int len = b.length;
115        while (n < len) {
116            int count = in.read(b, n, len - n);
117            if (count <= 0)
118                break;
119            n += count;
120        }
121        return n;
122    }
123
124    private static String contentsEquals(InputStream in1, InputStream in2) throws IOException {
125        byte[] buffer1 = new byte[8192];
126        byte[] buffer2 = new byte[8192];
127        int numRead1;
128        int numRead2;
129        while (true) {
130            numRead1 = readNBytes(in1, buffer1);
131            numRead2 = readNBytes(in2, buffer2);
132            if (numRead1 > 0) {
133                if (numRead2 <= 0) {
134                    return "EOF on second stream but not first";
135                }
136                if (numRead2 != numRead1) {
137                    return "Read size different (" + numRead1 + " vs " + numRead2 + ")";
138                }
139                // Otherwise same number of bytes read
140                if (!Arrays.equals(buffer1, buffer2)) {
141                    return "Content differs";
142                }
143                // Otherwise same bytes read, so continue ...
144            } else {
145                // Nothing more in stream 1 ...
146                if (numRead2 > 0) {
147                    return "EOF on first stream but not second";
148                } else {
149                    return null;
150                }
151            }
152        }
153    }
154
155    /**
156     * Extracts and loads the specified library file to the target folder
157     *
158     * @param libFolderForCurrentOS Library path.
159     * @param libraryFileName       Library name.
160     * @param targetFolder          Target folder.
161     * @return
162     */
163    private static boolean extractAndLoadLibraryFile(String libFolderForCurrentOS, String libraryFileName,
164                                                     String targetFolder) {
165        String nativeLibraryFilePath = libFolderForCurrentOS + "/" + libraryFileName;
166        // Include architecture name in temporary filename in order to avoid conflicts
167        // when multiple JVMs with different architectures running at the same time
168        String uuid = randomUUID();
169        String extractedLibFileName = String.format("jansi-%s-%s-%s", getVersion(), uuid, libraryFileName);
170        String extractedLckFileName = extractedLibFileName + ".lck";
171
172        File extractedLibFile = new File(targetFolder, extractedLibFileName);
173        File extractedLckFile = new File(targetFolder, extractedLckFileName);
174
175        try {
176            // Extract a native library file into the target directory
177            try (InputStream in = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
178                if (!extractedLckFile.exists()) {
179                    new FileOutputStream(extractedLckFile).close();
180                }
181                try (OutputStream out = new FileOutputStream(extractedLibFile)) {
182                    copy(in, out);
183                }
184            } finally {
185                // Delete the extracted lib file on JVM exit.
186                extractedLibFile.deleteOnExit();
187                extractedLckFile.deleteOnExit();
188            }
189
190            // Set executable (x) flag to enable Java to load the native library
191            extractedLibFile.setReadable(true);
192            extractedLibFile.setWritable(true);
193            extractedLibFile.setExecutable(true);
194
195            // Check whether the contents are properly copied from the resource folder
196            try (InputStream nativeIn = JansiLoader.class.getResourceAsStream(nativeLibraryFilePath)) {
197                try (InputStream extractedLibIn = new FileInputStream(extractedLibFile)) {
198                    String eq = contentsEquals(nativeIn, extractedLibIn);
199                    if (eq != null) {
200                        throw new RuntimeException(String.format("Failed to write a native library file at %s because %s", extractedLibFile, eq));
201                    }
202                }
203            }
204
205            // Load library
206            if (loadNativeLibrary(extractedLibFile)) {
207                nativeLibrarySourceUrl = JansiLoader.class.getResource(nativeLibraryFilePath).toExternalForm();
208                return true;
209            }
210        } catch (IOException e) {
211            System.err.println(e.getMessage());
212        }
213        return false;
214    }
215
216    private static String randomUUID() {
217        return Long.toHexString(new Random().nextLong());
218    }
219
220    private static void copy(InputStream in, OutputStream out) throws IOException {
221        byte[] buf = new byte[8192];
222        int n;
223        while ((n = in.read(buf)) > 0) {
224            out.write(buf, 0, n);
225        }
226    }
227
228    /**
229     * Loads native library using the given path and name of the library.
230     *
231     * @param libPath Path of the native library.
232     * @return True for successfully loading; false otherwise.
233     */
234    private static boolean loadNativeLibrary(File libPath) {
235        if (libPath.exists()) {
236            try {
237                String path = libPath.getAbsolutePath();
238                System.load(path);
239                nativeLibraryPath = path;
240                return true;
241            } catch (UnsatisfiedLinkError e) {
242                if (!libPath.canExecute()) {
243                    // NOTE: this can be tested using something like:
244                    // docker run --rm --tmpfs /tmp -v $PWD:/jansi openjdk:11 java -jar /jansi/target/jansi-xxx-SNAPSHOT.jar
245                    System.err.printf("Failed to load native library:%s. The native library file at %s is not executable, "
246                            + "make sure that the directory is mounted on a partition without the noexec flag, or set the "
247                            + "jansi.tmpdir system property to point to a proper location.  osinfo: %s%n",
248                            libPath.getName(), libPath, OSInfo.getNativeLibFolderPathForCurrentOS());
249                } else {
250                    System.err.printf("Failed to load native library:%s. osinfo: %s%n",
251                            libPath.getName(), OSInfo.getNativeLibFolderPathForCurrentOS());
252                }
253                System.err.println(e);
254                return false;
255            }
256
257        } else {
258            return false;
259        }
260    }
261
262    /**
263     * Loads jansi library using given path and name of the library.
264     *
265     * @throws
266     */
267    private static void loadJansiNativeLibrary() throws Exception {
268        if (loaded) {
269            return;
270        }
271
272        List<String> triedPaths = new LinkedList<String>();
273
274        // Try loading library from library.jansi.path library path */
275        String jansiNativeLibraryPath = System.getProperty("library.jansi.path");
276        String jansiNativeLibraryName = System.getProperty("library.jansi.name");
277        if (jansiNativeLibraryName == null) {
278            jansiNativeLibraryName = System.mapLibraryName("jansi");
279            assert jansiNativeLibraryName != null;
280            if (jansiNativeLibraryName.endsWith(".dylib")) {
281                jansiNativeLibraryName = jansiNativeLibraryName.replace(".dylib", ".jnilib");
282            }
283        }
284
285        if (jansiNativeLibraryPath != null) {
286            String withOs = jansiNativeLibraryPath + "/" + OSInfo.getNativeLibFolderPathForCurrentOS();
287            if (loadNativeLibrary(new File(withOs, jansiNativeLibraryName))) {
288                loaded = true;
289                return;
290            } else {
291                triedPaths.add(withOs);
292            }
293
294            if (loadNativeLibrary(new File(jansiNativeLibraryPath, jansiNativeLibraryName))) {
295                loaded = true;
296                return;
297            } else {
298                triedPaths.add(jansiNativeLibraryPath);
299            }
300        }
301
302        // Load the os-dependent library from the jar file
303        String packagePath = JansiLoader.class.getPackage().getName().replace('.', '/');
304        jansiNativeLibraryPath = String.format("/%s/native/%s", packagePath, OSInfo.getNativeLibFolderPathForCurrentOS());
305        boolean hasNativeLib = hasResource(jansiNativeLibraryPath + "/" + jansiNativeLibraryName);
306
307
308        if (hasNativeLib) {
309            // temporary library folder
310            String tempFolder = getTempDir().getAbsolutePath();
311            // Try extracting the library from jar
312            if (extractAndLoadLibraryFile(jansiNativeLibraryPath, jansiNativeLibraryName, tempFolder)) {
313                loaded = true;
314                return;
315            } else {
316                triedPaths.add(jansiNativeLibraryPath);
317            }
318        }
319
320        // As a last resort try from java.library.path
321        String javaLibraryPath = System.getProperty("java.library.path", "");
322        for (String ldPath : javaLibraryPath.split(File.pathSeparator)) {
323            if (ldPath.isEmpty()) {
324                continue;
325            }
326            if (loadNativeLibrary(new File(ldPath, jansiNativeLibraryName))) {
327                loaded = true;
328                return;
329            } else {
330                triedPaths.add(ldPath);
331            }
332        }
333
334        throw new Exception(String.format("No native library found for os.name=%s, os.arch=%s, paths=[%s]",
335                OSInfo.getOSName(), OSInfo.getArchName(), join(triedPaths, File.pathSeparator)));
336    }
337
338    private static boolean hasResource(String path) {
339        return JansiLoader.class.getResource(path) != null;
340    }
341
342
343    /**
344     * @return The major version of the jansi library.
345     */
346    public static int getMajorVersion() {
347        String[] c = getVersion().split("\\.");
348        return (c.length > 0) ? Integer.parseInt(c[0]) : 1;
349    }
350
351    /**
352     * @return The minor version of the jansi library.
353     */
354    public static int getMinorVersion() {
355        String[] c = getVersion().split("\\.");
356        return (c.length > 1) ? Integer.parseInt(c[1]) : 0;
357    }
358
359    /**
360     * @return The version of the jansi library.
361     */
362    public static String getVersion() {
363
364        URL versionFile = JansiLoader.class.getResource("/org/fusesource/jansi/jansi.properties");
365
366        String version = "unknown";
367        try {
368            if (versionFile != null) {
369                Properties versionData = new Properties();
370                versionData.load(versionFile.openStream());
371                version = versionData.getProperty("version", version);
372                version = version.trim().replaceAll("[^0-9.]", "");
373            }
374        } catch (IOException e) {
375            System.err.println(e);
376        }
377        return version;
378    }
379
380    private static String join(List<String> list, String separator) {
381        StringBuilder sb = new StringBuilder();
382        boolean first = true;
383        for (String item : list) {
384            if (first)
385                first = false;
386            else
387                sb.append(separator);
388
389            sb.append(item);
390        }
391        return sb.toString();
392    }
393
394}