jaulib v1.3.0
Jau Support Library (C++, Java, ..)
TempFileCache.java
Go to the documentation of this file.
1/**
2 * Author: Sven Gothel <sgothel@jausoft.com>
3 * Copyright (c) 2021 Gothel Software e.K.
4 * Copyright (c) 2011 Gothel Software e.K.
5 * Copyright (c) 2011 JogAmp Community.
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining
8 * a copy of this software and associated documentation files (the
9 * "Software"), to deal in the Software without restriction, including
10 * without limitation the rights to use, copy, modify, merge, publish,
11 * distribute, sublicense, and/or sell copies of the Software, and to
12 * permit persons to whom the Software is furnished to do so, subject to
13 * the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be
16 * included in all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 */
26package org.jau.pkg.cache;
27
28import java.io.File;
29import java.io.FileOutputStream;
30import java.io.FilenameFilter;
31import java.io.IOException;
32import java.nio.channels.FileChannel;
33import java.nio.channels.FileLock;
34
35import org.jau.io.IOUtil;
36import org.jau.lang.InterruptSource;
37import org.jau.sys.Debug;
38
39public class TempFileCache {
40 private static final boolean DEBUG = Debug.debug("TempFileCache");
41
42 // Flag indicating that we got a fatal error in the static initializer.
43 private static boolean staticInitError = false;
44
45 // Flag indicating that the temp root folder can be used for executable files
46 private static boolean staticTempIsExecutable = true;
47
48 private static final String tmpDirPrefix = "file_cache";
49
50 // Lifecycle: For one user's JVMs, ClassLoader and time.
51 private static final File tmpBaseDir;
52
53 // Get the value of the tmproot system property
54 // Lifecycle: For one user's concurrently running JVMs and ClassLoader
55 /* package */ static final String tmpRootPropName = "jnlp.jogamp.tmp.cache.root";
56
57 // String representing the name of the temp root directory relative to the
58 // tmpBaseDir. Its value is "jlnNNNNN", which is the unique filename created
59 // by File.createTempFile() without the ".tmp" extension.
60 //
61 // Lifecycle: For one user's concurrently running JVMs and ClassLoader
62 //
63 private static String tmpRootPropValue;
64
65 // Lifecycle: For one user's concurrently running JVMs and ClassLoader
66 private static File tmpRootDir;
67
68 // Flag indicating that we got a fatal error in the initializer.
69 private boolean initError = false;
70
71 private File individualTmpDir;
72
73 static {
74 // Global Lock !
75 synchronized (System.out) {
76 // Create / initialize the temp root directory, starting the Reaper
77 // thread to reclaim old installations if necessary. If we get an
78 // exception, set an error code.
79 File _tmpBaseDir = null;
80 try {
81 _tmpBaseDir = new File(IOUtil.getTempDir(true /* executable */), tmpDirPrefix);
82 _tmpBaseDir = IOUtil.testDir(_tmpBaseDir, true /* create */, false /* executable */); // executable already checked
83 staticTempIsExecutable = true;
84 } catch (final Exception ex) {
85 System.err.println("Warning: Caught Exception while retrieving executable temp base directory:");
86 ex.printStackTrace();
87 staticTempIsExecutable = false;
88 try {
89 _tmpBaseDir = new File(IOUtil.getTempDir(false /* executable */), tmpDirPrefix);
90 _tmpBaseDir = IOUtil.testDir(_tmpBaseDir, true /* create */, false /* executable */);
91 } catch (final Exception ex2) {
92 System.err.println("Warning: Caught Exception while retrieving non-executable temp base directory:");
93 ex2.printStackTrace();
94 staticInitError = true;
95 }
96 }
97 tmpBaseDir = _tmpBaseDir;
98
99 if (DEBUG) {
100 final String tmpBaseDirAbsPath = null != tmpBaseDir ? tmpBaseDir.getAbsolutePath() : null;
101 System.err.println("TempFileCache: Static Initialization ---------------------------------------------- OK: "+(!staticInitError));
102 System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+
103 ", CL 0x"+Integer.toHexString(TempFileCache.class.getClassLoader().hashCode())+
104 ", tempBaseDir "+tmpBaseDirAbsPath+", executable "+staticTempIsExecutable);
105 }
106
107 if(!staticInitError) {
108 try {
109 initTmpRoot();
110 } catch (final Exception ex) {
111 System.err.println("Warning: Caught Exception due to initializing TmpRoot:");
112 ex.printStackTrace();
113 staticInitError = true;
114 staticTempIsExecutable = false;
115 }
116 }
117 if (DEBUG) {
118 System.err.println("------------------------------------------------------------------ OK: "+(!staticInitError));
119 }
120 }
121 }
122
123 /**
124 * Documented way to kick off static initialization
125 * @return true is static initialization was successful
126 */
127 public static boolean initSingleton() {
128 return !staticInitError;
129 }
130
131 /**
132 * This method is called by the static initializer to create / initialize
133 * the temp root directory that will hold the temp directories for this
134 * instance of the JVM. This is done as follows:
135 *
136 * 1. Synchronize on a global lock. Note that for this purpose we will
137 * use System.out in the absence of a true global lock facility.
138 * We are careful not to hold this lock too long.
139 * The global lock is claimed in the static initializer block, calling this method!
140 *
141 * 2. Check for the existence of the "jnlp.jogamp.tmp.cache.root"
142 * system property.
143 *
144 * a. If set, then some other thread in a different ClassLoader has
145 * already created the tmpRootDir, so we just need to
146 * use it. The remaining steps are skipped.
147 * However, we check the existence of the tmpRootDir
148 * and if non existent, we assume a new launch and continue.
149 *
150 * b. If not set, then we are the first thread in this JVM to run,
151 * and we need to create the the tmpRootDir.
152 *
153 * 3. Create the tmpRootDir, along with the appropriate locks.
154 * Note that we perform the operations in the following order,
155 * prior to creating tmpRootDir itself, to work around the fact that
156 * the file creation and file lock steps are not atomic, and we need
157 * to ensure that a newly-created tmpRootDir isn't reaped by a
158 * concurrently running JVM.
159 *
160 * create jlnNNNN.tmp using File.createTempFile()
161 * lock jlnNNNN.tmp
162 * create jlnNNNN.lck while holding the lock on the .tmp file
163 * lock jlnNNNN.lck
164 *
165 * Since the Reaper thread will enumerate the list of *.lck files
166 * before starting, we can guarantee that if there exists a *.lck file
167 * for an active process, then the corresponding *.tmp file is locked
168 * by that active process. This guarantee lets us avoid reaping an
169 * active process' files.
170 *
171 * 4. Set the "jnlp.jogamp.tmp.cache.root" system property.
172 *
173 * 5. Add a shutdown hook to cleanup jlnNNNN.lck and jlnNNNN.tmp. We
174 * don't actually expect that this shutdown hook will ever be called,
175 * but the act of doing this, ensures that the locks never get
176 * garbage-collected, which is necessary for correct behavior when
177 * the first ClassLoader is later unloaded, while subsequent Applets
178 * are still running.
179 *
180 * 6. Start the Reaper thread to cleanup old installations.
181 */
182 private static void initTmpRoot() throws IOException {
183 tmpRootPropValue = System.getProperty(tmpRootPropName);
184
185 if (tmpRootPropValue != null) {
186 // Make sure that the property is not set to an illegal value
187 if (tmpRootPropValue.indexOf('/') >= 0 ||
188 tmpRootPropValue.indexOf(File.separatorChar) >= 0) {
189 throw new IOException("Illegal value of: " + tmpRootPropName);
190 }
191
192 // Set tmpRootDir = ${tmpbase}/${jnlp.applet.launcher.tmproot}
193 if (DEBUG) {
194 System.err.println("TempFileCache: Trying existing value of: " +
195 tmpRootPropName + "=" + tmpRootPropValue);
196 }
197 tmpRootDir = new File(tmpBaseDir, tmpRootPropValue);
198 if (DEBUG) {
199 System.err.println("TempFileCache: Trying tmpRootDir = " + tmpRootDir.getAbsolutePath());
200 }
201 if (tmpRootDir.isDirectory()) {
202 if (!tmpRootDir.canWrite()) {
203 throw new IOException("Temp root directory is not writable: " + tmpRootDir.getAbsolutePath());
204 }
205 } else {
206 // It is possible to move to a new GlueGen version within the same JVM
207 // In case tmpBaseDir has changed, we should assume a new tmpRootDir.
208 System.err.println("TempFileCache: None existing tmpRootDir = " + tmpRootDir.getAbsolutePath()+", assuming new path due to update");
209 tmpRootPropValue = null;
210 tmpRootDir = null;
211 System.clearProperty(tmpRootPropName);
212 }
213 }
214
215 if (tmpRootPropValue == null) {
216 // Create ${tmpbase}/jlnNNNN.tmp then lock the file
217 final File tmpFile = File.createTempFile("jln", ".tmp", tmpBaseDir);
218 if (DEBUG) {
219 System.err.println("TempFileCache: tmpFile = " + tmpFile.getAbsolutePath());
220 }
221 final FileOutputStream tmpOut = new FileOutputStream(tmpFile);
222 final FileChannel tmpChannel = tmpOut.getChannel();
223 final FileLock tmpLock = tmpChannel.lock();
224
225 // Strip off the ".tmp" to get the name of the tmprootdir
226 final String tmpFileName = tmpFile.getAbsolutePath();
227 final String tmpRootName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
228
229 // create ${tmpbase}/jlnNNNN.lck then lock the file
230 final String lckFileName = tmpRootName + ".lck";
231 final File lckFile = new File(lckFileName);
232 if (DEBUG) {
233 System.err.println("TempFileCache: lckFile = " + lckFile.getAbsolutePath());
234 }
235 lckFile.createNewFile();
236 final FileOutputStream lckOut = new FileOutputStream(lckFile);
237 final FileChannel lckChannel = lckOut.getChannel();
238 final FileLock lckLock = lckChannel.lock();
239
240 // Create tmprootdir
241 tmpRootDir = new File(tmpRootName);
242 if (DEBUG) {
243 System.err.println("TempFileCache: tmpRootDir = " + tmpRootDir.getAbsolutePath());
244 }
245 if (!tmpRootDir.mkdir()) {
246 throw new IOException("Cannot create " + tmpRootDir);
247 }
248
249 // Add shutdown hook to cleanup the OutputStream, FileChannel,
250 // and FileLock for the jlnNNNN.lck and jlnNNNN.lck files.
251 // We do this so that the locks never get garbage-collected.
252 Runtime.getRuntime().addShutdownHook(new InterruptSource.Thread() {
253 /* @Override */
254 @Override
255 public void run() {
256 // NOTE: we don't really expect that this code will ever
257 // be called. If it does, we will close the output
258 // stream, which will in turn close the channel.
259 // We will then release the lock.
260 try {
261 tmpOut.close();
262 tmpLock.release();
263 lckOut.close();
264 lckLock.release();
265 } catch (final IOException ex) {
266 // Do nothing
267 }
268 }
269 });
270
271 // Set the system property...
272 tmpRootPropValue = tmpRootName.substring(tmpRootName.lastIndexOf(File.separator) + 1);
273 System.setProperty(tmpRootPropName, tmpRootPropValue);
274 if (DEBUG) {
275 System.err.println("TempFileCache: Setting " + tmpRootPropName + "=" + tmpRootPropValue);
276 }
277
278 // Start a new Reaper thread to do stuff...
279 final Thread reaperThread = new InterruptSource.Thread() {
280 /* @Override */
281 @Override
282 public void run() {
283 deleteOldTempDirs();
284 }
285 };
286 reaperThread.setName("TempFileCache-Reaper");
287 reaperThread.start();
288 }
289 }
290
291 /**
292 * Called by the Reaper thread to delete old temp directories
293 * Only one of these threads will run per JVM invocation.
294 */
295 private static void deleteOldTempDirs() {
296 if (DEBUG) {
297 System.err.println("TempFileCache: *** Reaper: deleteOldTempDirs in " +
298 tmpBaseDir.getAbsolutePath());
299 }
300
301 // enumerate list of jnl*.lck files, ignore our own jlnNNNN file
302 final String ourLockFile = tmpRootPropValue + ".lck";
303 final FilenameFilter lckFilter = new FilenameFilter() {
304 /* @Override */
305 @Override
306 public boolean accept(final File dir, final String name) {
307 return name.endsWith(".lck") && !name.equals(ourLockFile);
308 }
309 };
310
311 // For each file <file>.lck in the list we will first try to lock
312 // <file>.tmp if that succeeds then we will try to lock <file>.lck
313 // (which should always succeed unless there is a problem). If we can
314 // get the lock on both files, then it must be an old installation, and
315 // we will delete it.
316 final String[] fileNames = tmpBaseDir.list(lckFilter);
317 if (fileNames != null) {
318 for (int i = 0; i < fileNames.length; i++) {
319 final String lckFileName = fileNames[i];
320 final String tmpDirName = lckFileName.substring(0, lckFileName.lastIndexOf(".lck"));
321 final String tmpFileName = tmpDirName + ".tmp";
322
323 final File lckFile = new File(tmpBaseDir, lckFileName);
324 final File tmpFile = new File(tmpBaseDir, tmpFileName);
325 final File tmpDir = new File(tmpBaseDir, tmpDirName);
326
327 if (lckFile.exists() && tmpFile.exists() && tmpDir.isDirectory()) {
328 FileOutputStream tmpOut = null;
329 FileChannel tmpChannel = null;
330 FileLock tmpLock = null;
331
332 try {
333 tmpOut = new FileOutputStream(tmpFile);
334 tmpChannel = tmpOut.getChannel();
335 tmpLock = tmpChannel.tryLock();
336 } catch (final Exception ex) {
337 // Ignore exceptions
338 if (DEBUG) {
339 ex.printStackTrace();
340 }
341 }
342
343 if (tmpLock != null) {
344 FileOutputStream lckOut = null;
345 FileChannel lckChannel = null;
346 FileLock lckLock = null;
347
348 try {
349 lckOut = new FileOutputStream(lckFile);
350 lckChannel = lckOut.getChannel();
351 lckLock = lckChannel.tryLock();
352 } catch (final Exception ex) {
353 if (DEBUG) {
354 ex.printStackTrace();
355 }
356 }
357
358 if (lckLock != null) {
359 // Recursively remove the old tmpDir and all of
360 // its contents
361 removeAll(tmpDir);
362
363 // Close the streams and delete the .lck and .tmp
364 // files. Note that there is a slight race condition
365 // in that another process could open a stream at
366 // the same time we are trying to delete it, which will
367 // prevent deletion, but we won't worry about it, since
368 // the worst that will happen is we might have an
369 // occasional 0-byte .lck or .tmp file left around
370 try {
371 lckOut.close();
372 } catch (final IOException ex) {
373 }
374 lckFile.delete();
375 try {
376 tmpOut.close();
377 } catch (final IOException ex) {
378 }
379 tmpFile.delete();
380 } else {
381 try {
382 // Close the file and channel for the *.lck file
383 if (lckOut != null) {
384 lckOut.close();
385 }
386 // Close the file/channel and release the lock
387 // on the *.tmp file
388 tmpOut.close();
389 tmpLock.release();
390 } catch (final IOException ex) {
391 if (DEBUG) {
392 ex.printStackTrace();
393 }
394 }
395 }
396 }
397 } else {
398 if (DEBUG) {
399 System.err.println("TempFileCache: Skipping: " + tmpDir.getAbsolutePath());
400 }
401 }
402 }
403 }
404 }
405
406 /**
407 * Remove the specified file or directory. If "path" is a directory, then
408 * recursively remove all entries, then remove the directory itself.
409 */
410 private static void removeAll(final File path) {
411 if (DEBUG) {
412 System.err.println("TempFileCache: removeAll(" + path + ")");
413 }
414
415 if (path.isDirectory()) {
416 // Recursively remove all files/directories in this directory
417 final File[] list = path.listFiles();
418 if (list != null) {
419 for (int i = 0; i < list.length; i++) {
420 removeAll(list[i]);
421 }
422 }
423 }
424 path.delete();
425 }
426
427 /** Create the {@link #getTempDir()} */
428 public TempFileCache () {
429 if (DEBUG) {
430 System.err.println("TempFileCache: new TempFileCache() --------------------- (static ok: "+(!staticInitError)+")");
431 System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(TempFileCache.class.getClassLoader().hashCode())+", this 0x"+Integer.toHexString(hashCode()));
432 }
433 if(!staticInitError) {
434 try {
435 createTmpDir();
436 } catch (final Exception ex) {
437 ex.printStackTrace();
438 initError = true;
439 }
440 }
441 if (DEBUG) {
442 System.err.println("TempFileCache: tempDir "+individualTmpDir+" (ok: "+(!initError)+")");
443 System.err.println("----------------------------------------------------------");
444 }
445 }
446
447 /** Delete the {@link #getTempDir()} recursively and remove it's reference. */
448 public void destroy() {
449 if (DEBUG) {
450 System.err.println("TempFileCache: destroy() --------------------- (static ok: "+(!staticInitError)+")");
451 System.err.println("TempFileCache: Thread: "+Thread.currentThread().getName()+", CL 0x"+Integer.toHexString(TempFileCache.class.getClassLoader().hashCode())+", this 0x"+Integer.toHexString(hashCode()));
452 }
453 if(!staticInitError) {
454 try {
455 removeAll(individualTmpDir);
456 } catch (final Exception ex) {
457 ex.printStackTrace();
458 }
459 }
460 individualTmpDir = null;
461 if (DEBUG) {
462 System.err.println("TempFileCache: destroy() END");
463 }
464 }
465
466 /**
467 * @param forExecutables if {@code true}, method also tests whether the underlying {@link #getBaseDir()} is suitable to load native libraries or launch executables
468 * @return true if static and object initialization was successful
469 * @see #isTempExecutable()
470 * @see #isValid()
471 */
472 public boolean isValid(final boolean forExecutables) {
473 return !staticInitError && !initError && ( !forExecutables || staticTempIsExecutable );
474 }
475
476 /**
477 * Base temp directory used by {@link TempFileCache}.
478 *
479 * <p>
480 * Lifecycle: For one user's concurrently running JVMs and ClassLoader
481 * </p>
482 *
483 * This is set to:
484 * <pre>
485 * ${java.io.tmpdir}/tmpDirPrefix
486 * </pre>
487 *
488 * @return
489 */
490 public static File getBaseDir() { return tmpBaseDir; }
491
492 /**
493 * Root temp directory for this JVM instance. Used to store individual
494 * directories.
495 * <p>
496 * This directory is a sub-folder to {@link #getBaseDir()}.
497 * </p>
498 *
499 * <p>
500 * Lifecycle: For one user's concurrently running JVMs and ClassLoader
501 * </p>
502 *
503 * <pre>
504 * tmpBaseDir/tmpRootPropValue
505 * </pre>
506 *
507 * <p>
508 * Use Case: Per ClassLoader files, eg. native libraries.
509 * </p>
510 *
511 * <p>
512 * Old temp directories are cleaned up the next time a JVM is launched that
513 * uses TempFileCache.
514 * </p>
515 *
516 *
517 * @return
518 */
519 public static File getRootDir() { return tmpRootDir; }
520
521 /**
522 * Temporary directory for individual files (eg. native libraries of one ClassLoader instance).
523 * <p>
524 * This directory is a sub-folder to {@link #getRootDir()}.
525 * </p>
526 *
527 * <p>
528 * Lifecycle: Within each JVM .. use case dependent, ie. per ClassLoader <b>and</b> per {@link TempFileCache} instance!
529 * </p>
530 * <p>
531 * The directory name is:
532 *
533 * <pre>
534 * tmpRootDir/jlnMMMMM
535 * </pre>
536 *
537 * where jlnMMMMM is the unique filename created by File.createTempFile()
538 * without the ".tmp" extension.
539 * </p>
540 *
541 * @return
542 */
543 public File getTempDir() { return individualTmpDir; }
544
545
546 /**
547 * Create the temp directory in tmpRootDir. To do this, we create a temp
548 * file with a ".tmp" extension, and then create a directory of the
549 * same name but without the ".tmp". The temp file, directory, and all
550 * files in the directory will be reaped the next time this is started.
551 * We avoid deleteOnExit, because it doesn't work reliably.
552 */
553 private void createTmpDir() throws IOException {
554 final File tmpFile = File.createTempFile("jln", ".tmp", tmpRootDir);
555 final String tmpFileName = tmpFile.getAbsolutePath();
556 final String tmpDirName = tmpFileName.substring(0, tmpFileName.lastIndexOf(".tmp"));
557 individualTmpDir = new File(tmpDirName);
558 if (!individualTmpDir.mkdir()) {
559 throw new IOException("Cannot create " + individualTmpDir);
560 }
561 }
562}
static File testDir(final File dir, final boolean create, final boolean executable)
Returns the directory dir, which is processed and tested as described below.
Definition: IOUtil.java:1010
static File getTempDir(final boolean executable)
Returns a platform independent writable directory for temporary files consisting of the platform's te...
Definition: IOUtil.java:1089
void destroy()
Delete the getTempDir() recursively and remove it's reference.
File getTempDir()
Temporary directory for individual files (eg.
static File getRootDir()
Root temp directory for this JVM instance.
TempFileCache()
Create the getTempDir().
boolean isValid(final boolean forExecutables)
static File getBaseDir()
Base temp directory used by TempFileCache.
static boolean initSingleton()
Documented way to kick off static initialization.
Helper routines for logging and debugging.
Definition: Debug.java:35
static final boolean debug(final String subcomponent)
Definition: Debug.java:63