summaryrefslogtreecommitdiffstats
path: root/src/java/jogamp/android/launcher/LauncherTempFileCache.java
blob: b576ed98e25f5ded5745db1771d1936f213f3e02 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
/**
 * Copyright 2011 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package jogamp.android.launcher;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

import com.jogamp.common.util.InterruptSource;

import android.content.Context;

public class LauncherTempFileCache {
    private static final boolean DEBUG = true;

    // Lifecycle: For all JVMs, ClassLoader and times.
    private static final String tmpSubDir = "jogamp";
    private static final String tmpDirPrefix = "file_cache";

    // Get the value of the tmproot system property
    // Lifecycle: For all JVMs and ClassLoader
    /* package */ static final String tmpRootPropName = "jnlp.jogamp.tmp.cache.root";

    // Flag indicating that we got a fatal error in the static initializer.
    private static boolean staticInitError = false;

    private static File tmpBaseDir;

    // String representing the name of the temp root directory relative to the
    // tmpBaseDir. Its value is "jlnNNNNN", which is the unique filename created
    // by File.createTempFile() without the ".tmp" extension.
    //
    // Lifecycle: For all JVMs and ClassLoader
    //
    private static String tmpRootPropValue;

    private static File tmpRootDir;

    // Flag indicating that we got a fatal error in the initializer.
    private boolean initError = false;

    private File individualTmpDir;

    /**
     * Documented way to kick off static initialization
     * @return true is static initialization was successful
     */
    public static synchronized boolean initSingleton(final Context ctx) {
        if(null == tmpRootDir && !staticInitError) {
            // Create / initialize the temp root directory, starting the Reaper
            // thread to reclaim old installations if necessary. If we get an
            // exception, set an error code.
            try {
                initTmpRoot(ctx);
            } catch (final Exception ex) {
                ex.printStackTrace();
                staticInitError = true;
            }
        }
        return !staticInitError;
    }

    /**
     * This method is called by the static initializer to create / initialize
     * the temp root directory that will hold the temp directories for this
     * instance of the JVM. This is done as follows:
     *
     *     1. Synchronize on a global lock. Note that for this purpose we will
     *        use System.out in the absence of a true global lock facility.
     *        We are careful not to hold this lock too long.
     *
     *     2. Check for the existence of the "jnlp.applet.launcher.tmproot"
     *        system property.
     *
     *         a. If set, then some other thread in a different ClassLoader has
     *            already created the tmprootdir, so we just need to
     *            use it. The remaining steps are skipped.
     *            However, we check the existence of the tmpRootDir
     *            and if non existent, we assume a new launch and continue.
     *
     *         b. If not set, then we are the first thread in this JVM to run,
     *            and we need to create the the tmprootdir.
     *
     *     3. Create the tmprootdir, along with the appropriate locks.
     *        Note that we perform the operations in the following order,
     *        prior to creating tmprootdir itself, to work around the fact that
     *        the file creation and file lock steps are not atomic, and we need
     *        to ensure that a newly-created tmprootdir isn't reaped by a
     *        concurrently running JVM.
     *
     *            create jlnNNNN.tmp using File.createTempFile()
     *            lock jlnNNNN.tmp
     *            create jlnNNNN.lck while holding the lock on the .tmp file
     *            lock jlnNNNN.lck
     *
     *        Since the Reaper thread will enumerate the list of *.lck files
     *        before starting, we can guarantee that if there exists a *.lck file
     *        for an active process, then the corresponding *.tmp file is locked
     *        by that active process. This guarantee lets us avoid reaping an
     *        active process' files.
     *
     *     4. Set the "jnlp.applet.launcher.tmproot" system property.
     *
     *     5. Add a shutdown hook to cleanup jlnNNNN.lck and jlnNNNN.tmp. We
     *        don't actually expect that this shutdown hook will ever be called,
     *        but the act of doing this, ensures that the locks never get
     *        garbage-collected, which is necessary for correct behavior when
     *        the first ClassLoader is later unloaded, while subsequent Applets
     *        are still running.
     *
     *     6. Start the Reaper thread to cleanup old installations.
     */
    private static void initTmpRoot(final Context ctx) throws IOException {
        if (DEBUG) {
            System.err.println("TempFileCache: Static Initialization ----------------------------------------------");
            System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(LauncherTempFileCache.class.getClassLoader().hashCode()));
        }

        synchronized (System.out) {
            // Get the name of the tmpbase directory.
            {
                final File tmpRoot = ctx.getDir("temp", Context.MODE_WORLD_READABLE);
                tmpBaseDir = new File(new File(tmpRoot, tmpSubDir), tmpDirPrefix);
            }
            tmpRootPropValue = System.getProperty(tmpRootPropName);

            if (tmpRootPropValue != null) {
                // Make sure that the property is not set to an illegal value
                if (tmpRootPropValue.indexOf('/') >= 0 ||
                        tmpRootPropValue.indexOf(File.separatorChar) >= 0) {
                    throw new IOException("Illegal value of: " + tmpRootPropName);
                }

                // Set tmpRootDir = ${tmpbase}/${jnlp.applet.launcher.tmproot}
                if (DEBUG) {
                    System.err.println("TempFileCache: Trying existing value of: " +
                            tmpRootPropName + "=" + tmpRootPropValue);
                }
                tmpRootDir = new File(tmpBaseDir, tmpRootPropValue);
                if (DEBUG) {
                    System.err.println("TempFileCache: Trying tmpRootDir = " + tmpRootDir.getAbsolutePath());
                }
                if (tmpRootDir.isDirectory()) {
                    if (!tmpRootDir.canWrite()) {
                        throw new IOException("Temp root directory is not writable: " + tmpRootDir.getAbsolutePath());
                    }
                } else {
                    // It is possible to move to a new GlueGen version within the same JVM
                    // In case tmpBaseDir has changed, we should assume a new tmpRootDir.
                    System.err.println("TempFileCache: None existing tmpRootDir = " + tmpRootDir.getAbsolutePath()+", assuming new path due to update");
                    tmpRootPropValue = null;
                    tmpRootDir = null;
                    System.clearProperty(tmpRootPropName);
                }
            }

            if (tmpRootPropValue == null) {
                // Create the tmpbase directory if it doesn't already exist
                tmpBaseDir.mkdirs();
                if (!tmpBaseDir.isDirectory()) {
                    throw new IOException("Cannot create directory " + tmpBaseDir);
                }

                // Create ${tmpbase}/jlnNNNN.tmp then lock the file
                final File tmpFile = File.createTempFile("jln", ".tmp", tmpBaseDir);
                if (DEBUG) {
                    System.err.println("TempFileCache: tmpFile = " + tmpFile.getAbsolutePath());
                }
                final FileOutputStream tmpOut = new FileOutputStream(tmpFile);
                final FileChannel tmpChannel = tmpOut.getChannel();
                final FileLock tmpLock = tmpChannel.lock();

                // Strip off the ".tmp" to get the name of the tmprootdir
                final String tmpFileName = tmpFile.getAbsolutePath();
                final String tmpRootName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));

                // create ${tmpbase}/jlnNNNN.lck then lock the file
                final String lckFileName = tmpRootName + ".lck";
                final File lckFile = new File(lckFileName);
                if (DEBUG) {
                    System.err.println("TempFileCache: lckFile = " + lckFile.getAbsolutePath());
                }
                lckFile.createNewFile();
                final FileOutputStream lckOut = new FileOutputStream(lckFile);
                final FileChannel lckChannel = lckOut.getChannel();
                final FileLock lckLock = lckChannel.lock();

                // Create tmprootdir
                tmpRootDir = new File(tmpRootName);
                if (DEBUG) {
                    System.err.println("TempFileCache: tmpRootDir = " + tmpRootDir.getAbsolutePath());
                }
                if (!tmpRootDir.mkdir()) {
                    throw new IOException("Cannot create " + tmpRootDir);
                }

                // Add shutdown hook to cleanup the OutputStream, FileChannel,
                // and FileLock for the jlnNNNN.lck and jlnNNNN.lck files.
                // We do this so that the locks never get garbage-collected.
                Runtime.getRuntime().addShutdownHook(new InterruptSource.Thread() {
                    /* @Override */
                    public void run() {
                        // NOTE: we don't really expect that this code will ever
                        // be called. If it does, we will close the output
                        // stream, which will in turn close the channel.
                        // We will then release the lock.
                        try {
                            tmpOut.close();
                            tmpLock.release();
                            lckOut.close();
                            lckLock.release();
                        } catch (final IOException ex) {
                            // Do nothing
                        }
                    }
                });

                // Set the system property...
                tmpRootPropValue = tmpRootName.substring(tmpRootName.lastIndexOf(File.separator) + 1);
                System.setProperty(tmpRootPropName, tmpRootPropValue);
                if (DEBUG) {
                    System.err.println("TempFileCache: Setting " + tmpRootPropName + "=" + tmpRootPropValue);
                }

                // Start a new Reaper thread to do stuff...
                final Thread reaperThread = new InterruptSource.Thread() {
                    /* @Override */
                    public void run() {
                        deleteOldTempDirs();
                    }
                };
                reaperThread.setName("TempFileCache-Reaper");
                reaperThread.start();
            }
        }
        if (DEBUG) {
            System.err.println("------------------------------------------------------------------ (static ok: "+(!staticInitError)+")");
        }
    }

    /**
     * Called by the Reaper thread to delete old temp directories
     * Only one of these threads will run per JVM invocation.
     */
    private static void deleteOldTempDirs() {
        if (DEBUG) {
            System.err.println("TempFileCache: *** Reaper: deleteOldTempDirs in " +
                    tmpBaseDir.getAbsolutePath());
        }

        // enumerate list of jnl*.lck files, ignore our own jlnNNNN file
        final String ourLockFile = tmpRootPropValue + ".lck";
        final FilenameFilter lckFilter = new FilenameFilter() {
            /* @Override */
            public boolean accept(final File dir, final String name) {
                return name.endsWith(".lck") && !name.equals(ourLockFile);
            }
        };

        // For each file <file>.lck in the list we will first try to lock
        // <file>.tmp if that succeeds then we will try to lock <file>.lck
        // (which should always succeed unless there is a problem). If we can
        // get the lock on both files, then it must be an old installation, and
        // we will delete it.
        final String[] fileNames = tmpBaseDir.list(lckFilter);
        if (fileNames != null) {
            for (int i = 0; i < fileNames.length; i++) {
                final String lckFileName = fileNames[i];
                final String tmpDirName = lckFileName.substring(0, lckFileName.lastIndexOf(".lck"));
                final String tmpFileName = tmpDirName + ".tmp";

                final File lckFile = new File(tmpBaseDir, lckFileName);
                final File tmpFile = new File(tmpBaseDir, tmpFileName);
                final File tmpDir = new File(tmpBaseDir, tmpDirName);

                if (lckFile.exists() && tmpFile.exists() && tmpDir.isDirectory()) {
                    FileOutputStream tmpOut = null;
                    FileChannel tmpChannel = null;
                    FileLock tmpLock = null;

                    try {
                        tmpOut = new FileOutputStream(tmpFile);
                        tmpChannel = tmpOut.getChannel();
                        tmpLock = tmpChannel.tryLock();
                    } catch (final Exception ex) {
                        // Ignore exceptions
                        if (DEBUG) {
                            ex.printStackTrace();
                        }
                    }

                    if (tmpLock != null) {
                        FileOutputStream lckOut = null;
                        FileChannel lckChannel = null;
                        FileLock lckLock = null;

                        try {
                            lckOut = new FileOutputStream(lckFile);
                            lckChannel = lckOut.getChannel();
                            lckLock = lckChannel.tryLock();
                        } catch (final Exception ex) {
                            if (DEBUG) {
                                ex.printStackTrace();
                            }
                        }

                        if (lckLock != null) {
                            // Recursively remove the old tmpDir and all of
                            // its contents
                            removeAll(tmpDir);

                            // Close the streams and delete the .lck and .tmp
                            // files. Note that there is a slight race condition
                            // in that another process could open a stream at
                            // the same time we are trying to delete it, which will
                            // prevent deletion, but we won't worry about it, since
                            // the worst that will happen is we might have an
                            // occasional 0-byte .lck or .tmp file left around
                            try {
                                lckOut.close();
                            } catch (final IOException ex) {
                            }
                            lckFile.delete();
                            try {
                                tmpOut.close();
                            } catch (final IOException ex) {
                            }
                            tmpFile.delete();
                        } else {
                            try {
                                // Close the file and channel for the *.lck file
                                if (lckOut != null) {
                                    lckOut.close();
                                }
                                // Close the file/channel and release the lock
                                // on the *.tmp file
                                tmpOut.close();
                                tmpLock.release();
                            } catch (final IOException ex) {
                                if (DEBUG) {
                                    ex.printStackTrace();
                                }
                            }
                        }
                    }
                } else {
                    if (DEBUG) {
                        System.err.println("TempFileCache: Skipping: " + tmpDir.getAbsolutePath());
                    }
                }
            }
        }
    }

    /**
     * Remove the specified file or directory. If "path" is a directory, then
     * recursively remove all entries, then remove the directory itself.
     */
    private static void removeAll(final File path) {
        if (DEBUG) {
            System.err.println("TempFileCache: removeAll(" + path + ")");
        }

        if (path.isDirectory()) {
            // Recursively remove all files/directories in this directory
            final File[] list = path.listFiles();
            if (list != null) {
                for (int i = 0; i < list.length; i++) {
                    removeAll(list[i]);
                }
            }
        }

        path.delete();
    }

    public LauncherTempFileCache () {
        if (DEBUG) {
            System.err.println("TempFileCache: new TempFileCache() --------------------- (static ok: "+(!staticInitError)+")");
            System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(LauncherTempFileCache.class.getClassLoader().hashCode())+", this 0x"+Integer.toHexString(hashCode()));
        }
        if(!staticInitError) {
            try {
                createTmpDir();
            } catch (final Exception ex) {
                ex.printStackTrace();
                initError = true;
            }
        }
        if (DEBUG) {
            System.err.println("tempDir: "+individualTmpDir+" (ok: "+(!initError)+")");
            System.err.println("----------------------------------------------------------");
        }
    }

    /**
     * @return true is static and object initialization was successful
     */
    public boolean isValid() { return !staticInitError && !initError; }

    /**
     * Base temp directory used by TempFileCache.
     * Lifecycle: For all JVMs, ClassLoader and times.
     *
     * This is set to:
     *
     * ${java.io.tmpdir}/<tmpDirPrefix>
     *
     *
     * @return
     */
    public File getBaseDir() { return tmpBaseDir; }

    /**
     * Root temp directory for this JVM instance. Used to store individual
     * directories.
     *
     * Lifecycle: For all JVMs and ClassLoader
     *
     * <tmpBaseDir>/<tmpRootPropValue>
     *
     * Use Case: Per ClassLoader files, eg. native libraries.
     *
     * Old temp directories are cleaned up the next time a JVM is launched that
     * uses TempFileCache.
     *
     *
     * @return
     */
    public File getRootDir() { return tmpRootDir; }

    /**
     * Temporary directory for individual files (eg. native libraries of one ClassLoader instance).
     * The directory name is:
     *
     * Lifecycle: Within each JVM .. use case dependent, ie. per ClassLoader
     *
     * <tmpRootDir>/jlnMMMMM
     *
     * where jlnMMMMM is the unique filename created by File.createTempFile()
     * without the ".tmp" extension.
     *
     *
     * @return
     */
    public File getTempDir() { return individualTmpDir; }


    /**
     * Create the temp directory in tmpRootDir. To do this, we create a temp
     * file with a ".tmp" extension, and then create a directory of the
     * same name but without the ".tmp". The temp file, directory, and all
     * files in the directory will be reaped the next time this is started.
     * We avoid deleteOnExit, because it doesn't work reliably.
     */
    private void createTmpDir() throws IOException {
        final File tmpFile = File.createTempFile("jln", ".tmp", tmpRootDir);
        final String tmpFileName = tmpFile.getAbsolutePath();
        final String tmpDirName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
        individualTmpDir = new File(tmpDirName);
        if (!individualTmpDir.mkdir()) {
            throw new IOException("Cannot create " + individualTmpDir);
        }
    }
}