/
JarFileManager.java
362 lines (314 loc) · 12.3 KB
/
JarFileManager.java
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
/*
* Copyright (c) 2023 Eclipse Foundation and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.web.loader;
import com.sun.enterprise.util.io.FileUtils;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.System.Logger;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.TRACE;
import static java.lang.System.Logger.Level.WARNING;
import static java.util.concurrent.Executors.newScheduledThreadPool;
import static org.glassfish.web.loader.LogFacade.getString;
/**
* @author David Matejcek
*/
class JarFileManager implements Closeable {
private static final int SECONDS_TO_CLOSE_UNUSED_JARS = Integer
.getInteger("org.glassfish.web.loader.unusedJars.secondsToClose", 60);
private static final int SECONDS_TO_CHECK_UNUSED_JARS = Integer
.getInteger("org.glassfish.web.loader.unusedJars.secondsToRunCheck", 15);
private static final Logger LOG = System.getLogger(JarFileManager.class.getName());
/** The list of JARs, in the order they should be searched for locally loaded classes or resources. */
private final List<JarResource> files = new ArrayList<>();
private final ScheduledExecutorService scheduler = newScheduledThreadPool(1, new JarFileManagerThreadFactory());
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private volatile long lastJarFileAccess;
private ScheduledFuture<?> unusedJarsCheck;
private volatile boolean resourcesExtracted;
void addJarFile(File file) {
Lock writeLock = lock.writeLock();
try {
writeLock.lock();
files.add(new JarResource(file));
} finally {
writeLock.unlock();
}
}
/**
* @return array of {@link JarFile}s. Note that they can be closed at any time. Can be null.
*/
JarFile[] getJarFiles() {
if (!isJarsOpen() && !openJARs()) {
return null;
}
Lock readLock = lock.readLock();
try {
readLock.lock();
lastJarFileAccess = System.currentTimeMillis();
return files.stream().map(r -> r.jarFile).toArray(JarFile[]::new);
} finally {
readLock.unlock();
}
}
File[] getJarRealFiles() {
Lock readLock = lock.readLock();
try {
readLock.lock();
return files.stream().map(r -> r.file).toArray(File[]::new);
} finally {
readLock.unlock();
}
}
/**
* Attempts to load the requested resource from this classloader's JAR files.
*
* @return The requested resource, or null if not found
*/
ResourceEntry findResource(String name, String path, File loaderDir, boolean antiJARLocking) {
LOG.log(TRACE, "findResource(name={0}, path={1}, loaderDir={2}, antiJARLocking={3})",
name, path, loaderDir, antiJARLocking);
if (!isJarsOpen() && !openJARs()) {
return null;
}
final Lock readLock = lock.readLock();
try {
readLock.lock();
lastJarFileAccess = System.currentTimeMillis();
for (JarResource jarResource : files) {
final JarFile jarFile = jarResource.jarFile;
final JarEntry jarEntry = jarFile.getJarEntry(path);
if (jarEntry == null) {
continue;
}
final ResourceEntry entry = createResourceEntry(name, jarResource.file, jarFile, jarEntry, path);
if (entry == null) {
// We have found the entry, but we cannot load it.
return null;
}
// Extract resources contained in JAR to the workdir
if (antiJARLocking && !path.endsWith(".class")) {
final File resourceFile = new File(loaderDir, jarEntry.getName());
if (!resourcesExtracted && !resourceFile.exists()) {
extractResources(loaderDir, path);
}
}
return entry;
}
} finally {
readLock.unlock();
}
return null;
}
void extractResources(File loaderDir, String canonicalLoaderDir) {
LOG.log(DEBUG, "extractResources(loaderDir={0}, canonicalLoaderDir={1})", loaderDir, canonicalLoaderDir);
if (resourcesExtracted) {
return;
}
Lock readLock = lock.readLock();
try {
readLock.lock();
for (JarResource jarResource : files) {
extractResource(jarResource.jarFile, loaderDir, canonicalLoaderDir);
}
} finally {
readLock.unlock();
}
resourcesExtracted = true;
}
/**
* Closes jar files. Can be executed multiple times.
*/
void closeJarFiles() {
LOG.log(DEBUG, "closeJarFiles()");
Lock writeLock = lock.writeLock();
try {
writeLock.lock();
lastJarFileAccess = 0L;
closeJarFiles(files);
} finally {
// No need to interrupt, just cancel next executions
this.unusedJarsCheck.cancel(false);
writeLock.unlock();
}
}
@Override
public void close() throws IOException {
closeJarFiles();
scheduler.shutdown();
}
/**
* @return true if opening succeeded
*/
private boolean openJARs() {
LOG.log(DEBUG, "openJARs()");
Lock writeLock = lock.writeLock();
try {
writeLock.lock();
if (isJarsOpen()) {
return true;
}
lastJarFileAccess = System.currentTimeMillis();
for (JarResource jarResource : files) {
if (jarResource.jarFile != null) {
continue;
}
try {
jarResource.jarFile = new JarFile(jarResource.file);
} catch (IOException e) {
LOG.log(DEBUG, "Failed to open JAR", e);
lastJarFileAccess = 0L;
closeJarFiles(files);
return false;
}
}
LOG.log(DEBUG, "JAR files are open. If unused, will be closed after {0} s", SECONDS_TO_CLOSE_UNUSED_JARS);
this.unusedJarsCheck = scheduler.scheduleAtFixedRate(this::closeJarFilesIfNotUsed, SECONDS_TO_CHECK_UNUSED_JARS,
SECONDS_TO_CHECK_UNUSED_JARS, TimeUnit.SECONDS);
return true;
} finally {
writeLock.unlock();
}
}
private boolean isJarsOpen() {
return lastJarFileAccess > 0L;
}
private ResourceEntry createResourceEntry(String name, File file, JarFile jarFile, JarEntry jarEntry,
String entryPath) {
final URL codeBase;
try {
codeBase = file.getCanonicalFile().toURI().toURL();
} catch (IOException e) {
LOG.log(DEBUG, "Invalid file: " + file, e);
return null;
}
final URL source;
try {
source = new URL("jar:" + codeBase + "!/" + entryPath);
} catch (MalformedURLException e) {
LOG.log(DEBUG, "Cannot create valid URL of file " + file + " and entry path " + entryPath, e);
return null;
}
final ResourceEntry entry = new ResourceEntry(codeBase, source);
try {
entry.manifest = jarFile.getManifest();
} catch (IOException e) {
LOG.log(DEBUG, "Failed to get manifest from " + jarFile.getName(), e);
return null;
}
entry.lastModified = file.lastModified();
final int contentLength = (int) jarEntry.getSize();
try (InputStream binaryStream = jarFile.getInputStream(jarEntry)) {
if (binaryStream != null) {
entry.readEntryData(name, binaryStream, contentLength, jarEntry);
}
} catch (IOException e) {
LOG.log(DEBUG, "Failed to read entry data for " + name, e);
return null;
}
return entry;
}
private static void extractResource(JarFile jarFile, File loaderDir, String pathPrefix) {
LOG.log(DEBUG, "extractResource(jarFile={0}, loaderDir={1}, pathPrefix={2})", jarFile, loaderDir, pathPrefix);
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if (!jarEntry.isDirectory() && !jarEntry.getName().endsWith(".class")) {
File resourceFile = new File(loaderDir, jarEntry.getName());
try {
if (!resourceFile.getCanonicalPath().startsWith(pathPrefix)) {
throw new IllegalArgumentException(getString(LogFacade.ILLEGAL_JAR_PATH, jarEntry.getName()));
}
} catch (IOException ioe) {
throw new IllegalArgumentException(
getString(LogFacade.VALIDATION_ERROR_JAR_PATH, jarEntry.getName()), ioe);
}
if (!FileUtils.mkdirsMaybe(resourceFile.getParentFile())) {
LOG.log(WARNING, LogFacade.UNABLE_TO_CREATE, resourceFile.getParentFile());
}
try (InputStream is = jarFile.getInputStream(jarEntry);
FileOutputStream os = new FileOutputStream(resourceFile)) {
FileUtils.copy(is, os, Long.MAX_VALUE);
} catch (IOException e) {
LOG.log(DEBUG, "Failed to copy entry " + jarEntry, e);
}
}
}
}
private void closeJarFilesIfNotUsed() {
if (!isJarsOpen()) {
return;
}
final long unusedFor = (System.currentTimeMillis() - lastJarFileAccess) / 1000L;
if (unusedFor <= SECONDS_TO_CLOSE_UNUSED_JARS) {
return;
}
LOG.log(DEBUG, "Closing jar files, because they were not used for {0} s.", unusedFor);
closeJarFiles();
}
private static void closeJarFiles(List<JarResource> files) {
for (JarResource jarResource : files) {
if (jarResource.jarFile == null) {
continue;
}
final JarFile toClose = jarResource.jarFile;
jarResource.jarFile = null;
closeJarFile(toClose);
}
LOG.log(DEBUG, "JAR files were closed.");
}
private static void closeJarFile(final JarFile jarFile) {
try {
jarFile.close();
} catch (IOException e) {
LOG.log(WARNING, "Could not close the jarFile " + jarFile, e);
}
}
private static class JarResource {
final File file;
JarFile jarFile;
JarResource(File file) {
this.file = file;
}
}
private static class JarFileManagerThreadFactory implements ThreadFactory {
private int counter = 1;
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "JarFileManager-" + counter++);
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
}
}
}