| /* | |
| * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others | |
| * | |
| * This program and the accompanying materials are made available under the | |
| * terms of the Eclipse Distribution License v. 1.0 which is available at | |
| * https://www.eclipse.org/org/documents/edl-v10.php. | |
| * | |
| * SPDX-License-Identifier: BSD-3-Clause | |
| */ | |
| package org.eclipse.jgit.util; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.time.Instant.EPOCH; | |
| import java.io.BufferedReader; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.Closeable; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.OutputStream; | |
| import java.io.OutputStreamWriter; | |
| import java.io.Writer; | |
| import java.nio.charset.Charset; | |
| import java.nio.file.AccessDeniedException; | |
| import java.nio.file.FileStore; | |
| import java.nio.file.Files; | |
| import java.nio.file.InvalidPathException; | |
| import java.nio.file.Path; | |
| import java.nio.file.attribute.BasicFileAttributes; | |
| import java.nio.file.attribute.FileTime; | |
| import java.text.MessageFormat; | |
| import java.time.Duration; | |
| import java.time.Instant; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Optional; | |
| import java.util.UUID; | |
| import java.util.concurrent.CancellationException; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.LinkedBlockingQueue; | |
| import java.util.concurrent.ThreadPoolExecutor; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.TimeoutException; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.concurrent.locks.Lock; | |
| import java.util.concurrent.locks.ReentrantLock; | |
| import java.util.regex.Matcher; | |
| import java.util.regex.Pattern; | |
| import org.eclipse.jgit.annotations.NonNull; | |
| import org.eclipse.jgit.annotations.Nullable; | |
| import org.eclipse.jgit.api.errors.JGitInternalException; | |
| import org.eclipse.jgit.errors.CommandFailedException; | |
| import org.eclipse.jgit.errors.ConfigInvalidException; | |
| import org.eclipse.jgit.errors.LockFailedException; | |
| import org.eclipse.jgit.internal.JGitText; | |
| import org.eclipse.jgit.internal.storage.file.FileSnapshot; | |
| import org.eclipse.jgit.internal.util.ShutdownHook; | |
| import org.eclipse.jgit.lib.Config; | |
| import org.eclipse.jgit.lib.ConfigConstants; | |
| import org.eclipse.jgit.lib.Constants; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.lib.StoredConfig; | |
| import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry; | |
| import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; | |
| import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry; | |
| import org.eclipse.jgit.util.ProcessResult.Status; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| /** | |
| * Abstraction to support various file system operations not in Java. | |
| */ | |
| public abstract class FS { | |
| private static final Logger LOG = LoggerFactory.getLogger(FS.class); | |
| /** | |
| * An empty array of entries, suitable as a return value for | |
| * {@link #list(File, FileModeStrategy)}. | |
| * | |
| * @since 5.0 | |
| */ | |
| protected static final Entry[] NO_ENTRIES = {}; | |
| private static final Pattern VERSION = Pattern | |
| .compile("\\s(\\d+)\\.(\\d+)\\.(\\d+)"); //$NON-NLS-1$ | |
| private static final Pattern EMPTY_PATH = Pattern | |
| .compile("^[\\p{javaWhitespace}" + File.pathSeparator + "]*$"); //$NON-NLS-1$ //$NON-NLS-2$ | |
| private volatile Boolean supportSymlinks; | |
| /** | |
| * This class creates FS instances. It will be overridden by a Java7 variant | |
| * if such can be detected in {@link #detect(Boolean)}. | |
| * | |
| * @since 3.0 | |
| */ | |
| public static class FSFactory { | |
| /** | |
| * Constructor | |
| */ | |
| protected FSFactory() { | |
| // empty | |
| } | |
| /** | |
| * Detect the file system | |
| * | |
| * @param cygwinUsed | |
| * whether cygwin is used | |
| * @return FS instance | |
| */ | |
| public FS detect(Boolean cygwinUsed) { | |
| if (SystemReader.getInstance().isWindows()) { | |
| if (cygwinUsed == null) { | |
| cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin()); | |
| } | |
| if (cygwinUsed.booleanValue()) { | |
| return new FS_Win32_Cygwin(); | |
| } | |
| return new FS_Win32(); | |
| } | |
| return new FS_POSIX(); | |
| } | |
| } | |
| /** | |
| * Result of an executed process. The caller is responsible to close the | |
| * contained {@link TemporaryBuffer}s | |
| * | |
| * @since 4.2 | |
| */ | |
| public static class ExecutionResult { | |
| private TemporaryBuffer stdout; | |
| private TemporaryBuffer stderr; | |
| private int rc; | |
| /** | |
| * @param stdout | |
| * stdout stream | |
| * @param stderr | |
| * stderr stream | |
| * @param rc | |
| * return code | |
| */ | |
| public ExecutionResult(TemporaryBuffer stdout, TemporaryBuffer stderr, | |
| int rc) { | |
| this.stdout = stdout; | |
| this.stderr = stderr; | |
| this.rc = rc; | |
| } | |
| /** | |
| * Get buffered standard output stream | |
| * | |
| * @return buffered standard output stream | |
| */ | |
| public TemporaryBuffer getStdout() { | |
| return stdout; | |
| } | |
| /** | |
| * Get buffered standard error stream | |
| * | |
| * @return buffered standard error stream | |
| */ | |
| public TemporaryBuffer getStderr() { | |
| return stderr; | |
| } | |
| /** | |
| * Get the return code of the process | |
| * | |
| * @return the return code of the process | |
| */ | |
| public int getRc() { | |
| return rc; | |
| } | |
| } | |
| /** | |
| * Attributes of FileStores on this system | |
| * | |
| * @since 5.1.9 | |
| */ | |
| public static final class FileStoreAttributes { | |
| /** | |
| * Marker to detect undefined values when reading from the config file. | |
| */ | |
| private static final Duration UNDEFINED_DURATION = Duration | |
| .ofNanos(Long.MAX_VALUE); | |
| /** | |
| * Fallback filesystem timestamp resolution. The worst case timestamp | |
| * resolution on FAT filesystems is 2 seconds. | |
| * <p> | |
| * Must be at least 1 second. | |
| * </p> | |
| */ | |
| public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration | |
| .ofSeconds(2); | |
| /** | |
| * Fallback FileStore attributes used when we can't measure the | |
| * filesystem timestamp resolution. The last modified time granularity | |
| * of FAT filesystems is 2 seconds. | |
| */ | |
| public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes( | |
| FALLBACK_TIMESTAMP_RESOLUTION); | |
| private static final long ONE_MICROSECOND = TimeUnit.MICROSECONDS | |
| .toNanos(1); | |
| private static final long ONE_MILLISECOND = TimeUnit.MILLISECONDS | |
| .toNanos(1); | |
| private static final long ONE_SECOND = TimeUnit.SECONDS.toNanos(1); | |
| /** | |
| * Minimum file system timestamp resolution granularity to check, in | |
| * nanoseconds. Should be a positive power of ten smaller than | |
| * {@link #ONE_SECOND}. Must be strictly greater than zero, i.e., | |
| * minimum value is 1 nanosecond. | |
| * <p> | |
| * Currently set to 1 microsecond, but could also be lower still. | |
| * </p> | |
| */ | |
| private static final long MINIMUM_RESOLUTION_NANOS = ONE_MICROSECOND; | |
| private static final String JAVA_VERSION_PREFIX = System | |
| .getProperty("java.vendor") + '|' //$NON-NLS-1$ | |
| + System.getProperty("java.version") + '|'; //$NON-NLS-1$ | |
| private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration | |
| .ofMillis(10); | |
| private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>(); | |
| private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>( | |
| 100, 0.2f); | |
| private static final AtomicBoolean background = new AtomicBoolean(); | |
| private static final Map<FileStore, Lock> locks = new ConcurrentHashMap<>(); | |
| private static final AtomicInteger threadNumber = new AtomicInteger(1); | |
| /** | |
| * Use a separate executor with at most one thread to synchronize | |
| * writing to the config. We write asynchronously since the config | |
| * itself might be on a different file system, which might otherwise | |
| * lead to locking problems. | |
| * <p> | |
| * Writing the config must not use a daemon thread, otherwise we may | |
| * leave an inconsistent state on disk when the JVM shuts down. Use a | |
| * small keep-alive time to avoid delays on shut-down. | |
| * </p> | |
| */ | |
| private static final ExecutorService SAVE_RUNNER = new ThreadPoolExecutor( | |
| 0, 1, 1L, TimeUnit.MILLISECONDS, | |
| new LinkedBlockingQueue<>(), | |
| runnable -> { | |
| Thread t = new Thread(runnable, | |
| "JGit-FileStoreAttributeWriter-" //$NON-NLS-1$ | |
| + threadNumber.getAndIncrement()); | |
| // Make sure these threads do finish | |
| t.setDaemon(false); | |
| return t; | |
| }); | |
| static { | |
| // Shut down the SAVE_RUNNER on System.exit() | |
| ShutdownHook.INSTANCE | |
| .register(FileStoreAttributes::shutdownSafeRunner); | |
| } | |
| private static void shutdownSafeRunner() { | |
| try { | |
| SAVE_RUNNER.shutdownNow(); | |
| SAVE_RUNNER.awaitTermination(100, TimeUnit.MILLISECONDS); | |
| } catch (Exception e) { | |
| // Ignore; we're shutting down | |
| } | |
| } | |
| /** | |
| * Whether FileStore attributes should be determined asynchronously | |
| * | |
| * @param async | |
| * whether FileStore attributes should be determined | |
| * asynchronously. If false access to cached attributes may | |
| * block for some seconds for the first call per FileStore | |
| * @since 5.6.2 | |
| */ | |
| public static void setBackground(boolean async) { | |
| background.set(async); | |
| } | |
| /** | |
| * Configures size and purge factor of the path-based cache for file | |
| * system attributes. Caching of file system attributes avoids recurring | |
| * lookup of @{code FileStore} of files which may be expensive on some | |
| * platforms. | |
| * | |
| * @param maxSize | |
| * maximum size of the cache, default is 100 | |
| * @param purgeFactor | |
| * when the size of the map reaches maxSize the oldest | |
| * entries will be purged to free up some space for new | |
| * entries, {@code purgeFactor} is the fraction of | |
| * {@code maxSize} to purge when this happens | |
| * @since 5.1.9 | |
| */ | |
| public static void configureAttributesPathCache(int maxSize, | |
| float purgeFactor) { | |
| FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor); | |
| } | |
| /** | |
| * Get the FileStoreAttributes for the given FileStore | |
| * | |
| * @param path | |
| * file residing in the FileStore to get attributes for | |
| * @return FileStoreAttributes for the given path. | |
| */ | |
| public static FileStoreAttributes get(Path path) { | |
| try { | |
| path = path.toAbsolutePath(); | |
| Path dir = Files.isDirectory(path) ? path : path.getParent(); | |
| if (dir == null) { | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| FileStoreAttributes cached = attrCacheByPath.get(dir); | |
| if (cached != null) { | |
| return cached; | |
| } | |
| FileStoreAttributes attrs = getFileStoreAttributes(dir); | |
| if (attrs == null) { | |
| // Don't cache, result might be late | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| attrCacheByPath.put(dir, attrs); | |
| return attrs; | |
| } catch (SecurityException e) { | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| } | |
| private static FileStoreAttributes getFileStoreAttributes(Path dir) { | |
| FileStore s; | |
| CompletableFuture<Optional<FileStoreAttributes>> f = null; | |
| try { | |
| if (Files.exists(dir)) { | |
| s = Files.getFileStore(dir); | |
| FileStoreAttributes c = attributeCache.get(s); | |
| if (c != null) { | |
| return c; | |
| } | |
| if (!Files.isWritable(dir)) { | |
| // cannot measure resolution in a read-only directory | |
| LOG.debug( | |
| "{}: cannot measure timestamp resolution in read-only directory {}", //$NON-NLS-1$ | |
| Thread.currentThread(), dir); | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| } else { | |
| // cannot determine FileStore of an unborn directory | |
| LOG.debug( | |
| "{}: cannot measure timestamp resolution of unborn directory {}", //$NON-NLS-1$ | |
| Thread.currentThread(), dir); | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| f = CompletableFuture | |
| .supplyAsync(() -> { | |
| Lock lock = locks.computeIfAbsent(s, | |
| l -> new ReentrantLock()); | |
| if (!lock.tryLock()) { | |
| LOG.debug( | |
| "{}: couldn't get lock to measure timestamp resolution in {}", //$NON-NLS-1$ | |
| Thread.currentThread(), dir); | |
| return Optional.empty(); | |
| } | |
| Optional<FileStoreAttributes> attributes = Optional | |
| .empty(); | |
| try { | |
| // Some earlier future might have set the value | |
| // and removed itself since we checked for the | |
| // value above. Hence check cache again. | |
| FileStoreAttributes c = attributeCache.get(s); | |
| if (c != null) { | |
| return Optional.of(c); | |
| } | |
| attributes = readFromConfig(s); | |
| if (attributes.isPresent()) { | |
| attributeCache.put(s, attributes.get()); | |
| return attributes; | |
| } | |
| Optional<Duration> resolution = measureFsTimestampResolution( | |
| s, dir); | |
| if (resolution.isPresent()) { | |
| c = new FileStoreAttributes( | |
| resolution.get()); | |
| attributeCache.put(s, c); | |
| // for high timestamp resolution measure | |
| // minimal racy interval | |
| if (c.fsTimestampResolution | |
| .toNanos() < 100_000_000L) { | |
| c.minimalRacyInterval = measureMinimalRacyInterval( | |
| dir); | |
| } | |
| if (LOG.isDebugEnabled()) { | |
| LOG.debug(c.toString()); | |
| } | |
| FileStoreAttributes newAttrs = c; | |
| SAVE_RUNNER.execute( | |
| () -> saveToConfig(s, newAttrs)); | |
| } | |
| attributes = Optional.of(c); | |
| } finally { | |
| lock.unlock(); | |
| locks.remove(s); | |
| } | |
| return attributes; | |
| }); | |
| f = f.exceptionally(e -> { | |
| LOG.error(e.getLocalizedMessage(), e); | |
| return Optional.empty(); | |
| }); | |
| // even if measuring in background wait a little - if the result | |
| // arrives, it's better than returning the large fallback | |
| boolean runInBackground = background.get(); | |
| Optional<FileStoreAttributes> d = runInBackground ? f.get( | |
| 100, TimeUnit.MILLISECONDS) : f.get(); | |
| if (d.isPresent()) { | |
| return d.get(); | |
| } else if (runInBackground) { | |
| // return null until measurement is finished | |
| return null; | |
| } | |
| // fall through and return fallback | |
| } catch (IOException | ExecutionException | CancellationException e) { | |
| cancel(f); | |
| LOG.error(e.getMessage(), e); | |
| } catch (TimeoutException | SecurityException e) { | |
| cancel(f); | |
| // use fallback | |
| } catch (InterruptedException e) { | |
| cancel(f); | |
| LOG.error(e.getMessage(), e); | |
| Thread.currentThread().interrupt(); | |
| } | |
| LOG.debug("{}: use fallback timestamp resolution for directory {}", //$NON-NLS-1$ | |
| Thread.currentThread(), dir); | |
| return FALLBACK_FILESTORE_ATTRIBUTES; | |
| } | |
| private static void cancel( | |
| CompletableFuture<Optional<FileStoreAttributes>> f) { | |
| if (f != null) { | |
| f.cancel(true); | |
| } | |
| } | |
| @SuppressWarnings("boxing") | |
| private static Duration measureMinimalRacyInterval(Path dir) { | |
| LOG.debug("{}: start measure minimal racy interval in {}", //$NON-NLS-1$ | |
| Thread.currentThread(), dir); | |
| int n = 0; | |
| int failures = 0; | |
| long racyNanos = 0; | |
| ArrayList<Long> deltas = new ArrayList<>(); | |
| Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ | |
| Instant end = Instant.now().plusSeconds(3); | |
| try { | |
| probe.toFile().deleteOnExit(); | |
| Files.createFile(probe); | |
| do { | |
| n++; | |
| write(probe, "a"); //$NON-NLS-1$ | |
| FileSnapshot snapshot = FileSnapshot.save(probe.toFile()); | |
| read(probe); | |
| write(probe, "b"); //$NON-NLS-1$ | |
| if (!snapshot.isModified(probe.toFile())) { | |
| deltas.add(Long.valueOf(snapshot.lastDelta())); | |
| racyNanos = snapshot.lastRacyThreshold(); | |
| failures++; | |
| } | |
| } while (Instant.now().compareTo(end) < 0); | |
| } catch (IOException e) { | |
| LOG.error(e.getMessage(), e); | |
| return FALLBACK_MIN_RACY_INTERVAL; | |
| } finally { | |
| deleteProbe(probe); | |
| } | |
| if (failures > 0) { | |
| Stats stats = new Stats(); | |
| for (Long d : deltas) { | |
| stats.add(d); | |
| } | |
| LOG.debug( | |
| "delta [ns] since modification FileSnapshot failed to detect\n" //$NON-NLS-1$ | |
| + "count, failures, racy limit [ns], delta min [ns]," //$NON-NLS-1$ | |
| + " delta max [ns], delta avg [ns]," //$NON-NLS-1$ | |
| + " delta stddev [ns]\n" //$NON-NLS-1$ | |
| + "{}, {}, {}, {}, {}, {}, {}", //$NON-NLS-1$ | |
| n, failures, racyNanos, stats.min(), stats.max(), | |
| stats.avg(), stats.stddev()); | |
| return Duration | |
| .ofNanos(Double.valueOf(stats.max()).longValue()); | |
| } | |
| // since no failures occurred using the measured filesystem | |
| // timestamp resolution there is no need for minimal racy interval | |
| LOG.debug("{}: no failures when measuring minimal racy interval", //$NON-NLS-1$ | |
| Thread.currentThread()); | |
| return Duration.ZERO; | |
| } | |
| private static void write(Path p, String body) throws IOException { | |
| Path parent = p.getParent(); | |
| if (parent != null) { | |
| FileUtils.mkdirs(parent.toFile(), true); | |
| } | |
| try (Writer w = new OutputStreamWriter(Files.newOutputStream(p), | |
| UTF_8)) { | |
| w.write(body); | |
| } | |
| } | |
| private static String read(Path p) throws IOException { | |
| byte[] body = IO.readFully(p.toFile()); | |
| return new String(body, 0, body.length, UTF_8); | |
| } | |
| private static Optional<Duration> measureFsTimestampResolution( | |
| FileStore s, Path dir) { | |
| if (LOG.isDebugEnabled()) { | |
| LOG.debug("{}: start measure timestamp resolution {} in {}", //$NON-NLS-1$ | |
| Thread.currentThread(), s, dir); | |
| } | |
| Path probe = dir.resolve(".probe-" + UUID.randomUUID()); //$NON-NLS-1$ | |
| try { | |
| probe.toFile().deleteOnExit(); | |
| Files.createFile(probe); | |
| Duration fsResolution = getFsResolution(s, dir, probe); | |
| Duration clockResolution = measureClockResolution(); | |
| fsResolution = fsResolution.plus(clockResolution); | |
| if (LOG.isDebugEnabled()) { | |
| LOG.debug( | |
| "{}: end measure timestamp resolution {} in {}; got {}", //$NON-NLS-1$ | |
| Thread.currentThread(), s, dir, fsResolution); | |
| } | |
| return Optional.of(fsResolution); | |
| } catch (SecurityException e) { | |
| // Log it here; most likely deleteProbe() below will also run | |
| // into a SecurityException, and then this one will be lost | |
| // without trace. | |
| LOG.warn(e.getLocalizedMessage(), e); | |
| } catch (AccessDeniedException e) { | |
| LOG.warn(e.getLocalizedMessage(), e); // see bug 548648 | |
| } catch (IOException e) { | |
| LOG.error(e.getLocalizedMessage(), e); | |
| } finally { | |
| deleteProbe(probe); | |
| } | |
| return Optional.empty(); | |
| } | |
| private static Duration getFsResolution(FileStore s, Path dir, | |
| Path probe) throws IOException { | |
| File probeFile = probe.toFile(); | |
| FileTime t1 = Files.getLastModifiedTime(probe); | |
| Instant t1i = t1.toInstant(); | |
| FileTime t2; | |
| Duration last = FALLBACK_TIMESTAMP_RESOLUTION; | |
| long minScale = MINIMUM_RESOLUTION_NANOS; | |
| long scale = ONE_SECOND; | |
| long high = TimeUnit.MILLISECONDS.toSeconds(last.toMillis()); | |
| long low = 0; | |
| // Try up-front at microsecond and millisecond | |
| long[] tries = { ONE_MICROSECOND, ONE_MILLISECOND }; | |
| for (long interval : tries) { | |
| if (interval >= ONE_MILLISECOND) { | |
| probeFile.setLastModified( | |
| t1i.plusNanos(interval).toEpochMilli()); | |
| } else { | |
| Files.setLastModifiedTime(probe, | |
| FileTime.from(t1i.plusNanos(interval))); | |
| } | |
| t2 = Files.getLastModifiedTime(probe); | |
| if (t2.compareTo(t1) > 0) { | |
| Duration diff = Duration.between(t1i, t2.toInstant()); | |
| if (!diff.isZero() && !diff.isNegative() | |
| && diff.compareTo(last) < 0) { | |
| scale = interval; | |
| high = 1; | |
| last = diff; | |
| break; | |
| } | |
| } else { | |
| // Makes no sense going below | |
| minScale = Math.max(minScale, interval); | |
| } | |
| } | |
| // Binary search loop | |
| while (high > low) { | |
| long mid = (high + low) / 2; | |
| if (mid == 0) { | |
| // Smaller than current scale. Adjust scale. | |
| long newScale = scale / 10; | |
| if (newScale < minScale) { | |
| break; | |
| } | |
| high *= scale / newScale; | |
| low *= scale / newScale; | |
| scale = newScale; | |
| mid = (high + low) / 2; | |
| } | |
| long delta = mid * scale; | |
| if (scale >= ONE_MILLISECOND) { | |
| probeFile.setLastModified( | |
| t1i.plusNanos(delta).toEpochMilli()); | |
| } else { | |
| Files.setLastModifiedTime(probe, | |
| FileTime.from(t1i.plusNanos(delta))); | |
| } | |
| t2 = Files.getLastModifiedTime(probe); | |
| int cmp = t2.compareTo(t1); | |
| if (cmp > 0) { | |
| high = mid; | |
| Duration diff = Duration.between(t1i, t2.toInstant()); | |
| if (diff.isZero() || diff.isNegative()) { | |
| LOG.warn(JGitText.get().logInconsistentFiletimeDiff, | |
| Thread.currentThread(), s, dir, t2, t1, diff, | |
| last); | |
| break; | |
| } else if (diff.compareTo(last) > 0) { | |
| LOG.warn(JGitText.get().logLargerFiletimeDiff, | |
| Thread.currentThread(), s, dir, diff, last); | |
| break; | |
| } | |
| last = diff; | |
| } else if (cmp < 0) { | |
| LOG.warn(JGitText.get().logSmallerFiletime, | |
| Thread.currentThread(), s, dir, t2, t1, last); | |
| break; | |
| } else { | |
| // No discernible difference | |
| low = mid + 1; | |
| } | |
| } | |
| return last; | |
| } | |
| private static Duration measureClockResolution() { | |
| Duration clockResolution = Duration.ZERO; | |
| for (int i = 0; i < 10; i++) { | |
| Instant t1 = Instant.now(); | |
| Instant t2 = t1; | |
| while (t2.compareTo(t1) <= 0) { | |
| t2 = Instant.now(); | |
| } | |
| Duration r = Duration.between(t1, t2); | |
| if (r.compareTo(clockResolution) > 0) { | |
| clockResolution = r; | |
| } | |
| } | |
| return clockResolution; | |
| } | |
| private static void deleteProbe(Path probe) { | |
| try { | |
| FileUtils.delete(probe.toFile(), | |
| FileUtils.SKIP_MISSING | FileUtils.RETRY); | |
| } catch (IOException e) { | |
| LOG.error(e.getMessage(), e); | |
| } | |
| } | |
| private static Optional<FileStoreAttributes> readFromConfig( | |
| FileStore s) { | |
| StoredConfig userConfig; | |
| try { | |
| userConfig = SystemReader.getInstance().getUserConfig(); | |
| } catch (IOException | ConfigInvalidException e) { | |
| LOG.error(JGitText.get().readFileStoreAttributesFailed, e); | |
| return Optional.empty(); | |
| } | |
| String key = getConfigKey(s); | |
| Duration resolution = Duration.ofNanos(userConfig.getTimeUnit( | |
| ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, | |
| ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, | |
| UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS)); | |
| if (UNDEFINED_DURATION.equals(resolution)) { | |
| return Optional.empty(); | |
| } | |
| Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit( | |
| ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, | |
| ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD, | |
| UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS)); | |
| FileStoreAttributes c = new FileStoreAttributes(resolution); | |
| if (!UNDEFINED_DURATION.equals(minRacyThreshold)) { | |
| c.minimalRacyInterval = minRacyThreshold; | |
| } | |
| return Optional.of(c); | |
| } | |
| private static void saveToConfig(FileStore s, | |
| FileStoreAttributes c) { | |
| StoredConfig jgitConfig; | |
| try { | |
| jgitConfig = SystemReader.getInstance().getJGitConfig(); | |
| } catch (IOException | ConfigInvalidException e) { | |
| LOG.error(JGitText.get().saveFileStoreAttributesFailed, e); | |
| return; | |
| } | |
| long resolution = c.getFsTimestampResolution().toNanos(); | |
| TimeUnit resolutionUnit = getUnit(resolution); | |
| long resolutionValue = resolutionUnit.convert(resolution, | |
| TimeUnit.NANOSECONDS); | |
| long minRacyThreshold = c.getMinimalRacyInterval().toNanos(); | |
| TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold); | |
| long minRacyThresholdValue = minRacyThresholdUnit | |
| .convert(minRacyThreshold, TimeUnit.NANOSECONDS); | |
| final int max_retries = 5; | |
| int retries = 0; | |
| boolean succeeded = false; | |
| String key = getConfigKey(s); | |
| while (!succeeded && retries < max_retries) { | |
| try { | |
| jgitConfig.setString( | |
| ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, | |
| ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION, | |
| String.format("%d %s", //$NON-NLS-1$ | |
| Long.valueOf(resolutionValue), | |
| resolutionUnit.name().toLowerCase())); | |
| jgitConfig.setString( | |
| ConfigConstants.CONFIG_FILESYSTEM_SECTION, key, | |
| ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD, | |
| String.format("%d %s", //$NON-NLS-1$ | |
| Long.valueOf(minRacyThresholdValue), | |
| minRacyThresholdUnit.name().toLowerCase())); | |
| jgitConfig.save(); | |
| succeeded = true; | |
| } catch (LockFailedException e) { | |
| // race with another thread, wait a bit and try again | |
| try { | |
| retries++; | |
| if (retries < max_retries) { | |
| Thread.sleep(100); | |
| LOG.debug("locking {} failed, retries {}/{}", //$NON-NLS-1$ | |
| jgitConfig, Integer.valueOf(retries), | |
| Integer.valueOf(max_retries)); | |
| } else { | |
| LOG.warn(MessageFormat.format( | |
| JGitText.get().lockFailedRetry, jgitConfig, | |
| Integer.valueOf(retries))); | |
| } | |
| } catch (InterruptedException e1) { | |
| Thread.currentThread().interrupt(); | |
| break; | |
| } | |
| } catch (IOException e) { | |
| LOG.error(MessageFormat.format( | |
| JGitText.get().cannotSaveConfig, jgitConfig), e); | |
| break; | |
| } | |
| } | |
| } | |
| private static String getConfigKey(FileStore s) { | |
| String storeKey; | |
| if (SystemReader.getInstance().isWindows()) { | |
| Object attribute = null; | |
| try { | |
| attribute = s.getAttribute("volume:vsn"); //$NON-NLS-1$ | |
| } catch (IOException ignored) { | |
| // ignore | |
| } | |
| if (attribute instanceof Integer) { | |
| storeKey = attribute.toString(); | |
| } else { | |
| storeKey = s.name(); | |
| } | |
| } else { | |
| storeKey = s.name(); | |
| } | |
| return JAVA_VERSION_PREFIX + storeKey; | |
| } | |
| private static TimeUnit getUnit(long nanos) { | |
| TimeUnit unit; | |
| if (nanos < 200_000L) { | |
| unit = TimeUnit.NANOSECONDS; | |
| } else if (nanos < 200_000_000L) { | |
| unit = TimeUnit.MICROSECONDS; | |
| } else { | |
| unit = TimeUnit.MILLISECONDS; | |
| } | |
| return unit; | |
| } | |
| private final @NonNull Duration fsTimestampResolution; | |
| private Duration minimalRacyInterval; | |
| /** | |
| * Get the minimal racy interval | |
| * | |
| * @return the measured minimal interval after a file has been modified | |
| * in which we cannot rely on lastModified to detect | |
| * modifications | |
| */ | |
| public Duration getMinimalRacyInterval() { | |
| return minimalRacyInterval; | |
| } | |
| /** | |
| * Get the measured filesystem timestamp resolution | |
| * | |
| * @return the measured filesystem timestamp resolution | |
| */ | |
| @NonNull | |
| public Duration getFsTimestampResolution() { | |
| return fsTimestampResolution; | |
| } | |
| /** | |
| * Construct a FileStoreAttributeCache entry for the given filesystem | |
| * timestamp resolution | |
| * | |
| * @param fsTimestampResolution | |
| * resolution of filesystem timestamps | |
| */ | |
| public FileStoreAttributes( | |
| @NonNull Duration fsTimestampResolution) { | |
| this.fsTimestampResolution = fsTimestampResolution; | |
| this.minimalRacyInterval = Duration.ZERO; | |
| } | |
| @SuppressWarnings({ "nls", "boxing" }) | |
| @Override | |
| public String toString() { | |
| return String.format( | |
| "FileStoreAttributes[fsTimestampResolution=%,d µs, " | |
| + "minimalRacyInterval=%,d µs]", | |
| fsTimestampResolution.toNanos() / 1000, | |
| minimalRacyInterval.toNanos() / 1000); | |
| } | |
| } | |
| /** The auto-detected implementation selected for this operating system and JRE. */ | |
| public static final FS DETECTED = detect(); | |
| private static volatile FSFactory factory; | |
| /** | |
| * Auto-detect the appropriate file system abstraction. | |
| * | |
| * @return detected file system abstraction | |
| */ | |
| public static FS detect() { | |
| return detect(null); | |
| } | |
| /** | |
| * Auto-detect the appropriate file system abstraction, taking into account | |
| * the presence of a Cygwin installation on the system. Using jgit in | |
| * combination with Cygwin requires a more elaborate (and possibly slower) | |
| * resolution of file system paths. | |
| * | |
| * @param cygwinUsed | |
| * <ul> | |
| * <li><code>Boolean.TRUE</code> to assume that Cygwin is used in | |
| * combination with jgit</li> | |
| * <li><code>Boolean.FALSE</code> to assume that Cygwin is | |
| * <b>not</b> used with jgit</li> | |
| * <li><code>null</code> to auto-detect whether a Cygwin | |
| * installation is present on the system and in this case assume | |
| * that Cygwin is used</li> | |
| * </ul> | |
| * | |
| * Note: this parameter is only relevant on Windows. | |
| * @return detected file system abstraction | |
| */ | |
| public static FS detect(Boolean cygwinUsed) { | |
| if (factory == null) { | |
| factory = new FS.FSFactory(); | |
| } | |
| return factory.detect(cygwinUsed); | |
| } | |
| /** | |
| * Get cached FileStore attributes, if not yet available measure them using | |
| * a probe file under the given directory. | |
| * | |
| * @param dir | |
| * the directory under which the probe file will be created to | |
| * measure the timer resolution. | |
| * @return measured filesystem timestamp resolution | |
| * @since 5.1.9 | |
| */ | |
| public static FileStoreAttributes getFileStoreAttributes( | |
| @NonNull Path dir) { | |
| return FileStoreAttributes.get(dir); | |
| } | |
| private volatile Holder<File> userHome; | |
| private volatile Holder<File> gitSystemConfig; | |
| /** | |
| * Constructs a file system abstraction. | |
| */ | |
| protected FS() { | |
| // Do nothing by default. | |
| } | |
| /** | |
| * Initialize this FS using another's current settings. | |
| * | |
| * @param src | |
| * the source FS to copy from. | |
| */ | |
| protected FS(FS src) { | |
| userHome = src.userHome; | |
| gitSystemConfig = src.gitSystemConfig; | |
| } | |
| /** | |
| * Create a new instance of the same type of FS. | |
| * | |
| * @return a new instance of the same type of FS. | |
| */ | |
| public abstract FS newInstance(); | |
| /** | |
| * Does this operating system and JRE support the execute flag on files? | |
| * | |
| * @return true if this implementation can provide reasonably accurate | |
| * executable bit information; false otherwise. | |
| */ | |
| public abstract boolean supportsExecute(); | |
| /** | |
| * Does this file system support atomic file creation via | |
| * java.io.File#createNewFile()? In certain environments (e.g. on NFS) it is | |
| * not guaranteed that when two file system clients run createNewFile() in | |
| * parallel only one will succeed. In such cases both clients may think they | |
| * created a new file. | |
| * | |
| * @return true if this implementation support atomic creation of new Files | |
| * by {@link java.io.File#createNewFile()} | |
| * @since 4.5 | |
| */ | |
| public boolean supportsAtomicCreateNewFile() { | |
| return true; | |
| } | |
| /** | |
| * Does this operating system and JRE supports symbolic links. The | |
| * capability to handle symbolic links is detected at runtime. | |
| * | |
| * @return true if symbolic links may be used | |
| * @since 3.0 | |
| */ | |
| public boolean supportsSymlinks() { | |
| if (supportSymlinks == null) { | |
| detectSymlinkSupport(); | |
| } | |
| return Boolean.TRUE.equals(supportSymlinks); | |
| } | |
| private void detectSymlinkSupport() { | |
| File tempFile = null; | |
| try { | |
| tempFile = File.createTempFile("tempsymlinktarget", ""); //$NON-NLS-1$ //$NON-NLS-2$ | |
| File linkName = new File(tempFile.getPath() + "-tempsymlink"); //$NON-NLS-1$ | |
| createSymLink(linkName, tempFile.getPath()); | |
| supportSymlinks = Boolean.TRUE; | |
| linkName.delete(); | |
| } catch (IOException | UnsupportedOperationException | SecurityException | |
| | InternalError e) { | |
| supportSymlinks = Boolean.FALSE; | |
| } finally { | |
| if (tempFile != null) { | |
| try { | |
| FileUtils.delete(tempFile); | |
| } catch (IOException e) { | |
| LOG.error(JGitText.get().cannotDeleteFile, tempFile); | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Is this file system case sensitive | |
| * | |
| * @return true if this implementation is case sensitive | |
| */ | |
| public abstract boolean isCaseSensitive(); | |
| /** | |
| * Determine if the file is executable (or not). | |
| * <p> | |
| * Not all platforms and JREs support executable flags on files. If the | |
| * feature is unsupported this method will always return false. | |
| * <p> | |
| * <em>If the platform supports symbolic links and <code>f</code> is a symbolic link | |
| * this method returns false, rather than the state of the executable flags | |
| * on the target file.</em> | |
| * | |
| * @param f | |
| * abstract path to test. | |
| * @return true if the file is believed to be executable by the user. | |
| */ | |
| public abstract boolean canExecute(File f); | |
| /** | |
| * Set a file to be executable by the user. | |
| * <p> | |
| * Not all platforms and JREs support executable flags on files. If the | |
| * feature is unsupported this method will always return false and no | |
| * changes will be made to the file specified. | |
| * | |
| * @param f | |
| * path to modify the executable status of. | |
| * @param canExec | |
| * true to enable execution; false to disable it. | |
| * @return true if the change succeeded; false otherwise. | |
| */ | |
| public abstract boolean setExecute(File f, boolean canExec); | |
| /** | |
| * Get the last modified time of a file system object. If the OS/JRE support | |
| * symbolic links, the modification time of the link is returned, rather | |
| * than that of the link target. | |
| * | |
| * @param p | |
| * a {@link Path} object. | |
| * @return last modified time of p | |
| * @since 5.1.9 | |
| */ | |
| public Instant lastModifiedInstant(Path p) { | |
| return FileUtils.lastModifiedInstant(p); | |
| } | |
| /** | |
| * Get the last modified time of a file system object. If the OS/JRE support | |
| * symbolic links, the modification time of the link is returned, rather | |
| * than that of the link target. | |
| * | |
| * @param f | |
| * a {@link File} object. | |
| * @return last modified time of p | |
| * @since 5.1.9 | |
| */ | |
| public Instant lastModifiedInstant(File f) { | |
| return FileUtils.lastModifiedInstant(f.toPath()); | |
| } | |
| /** | |
| * Set the last modified time of a file system object. | |
| * <p> | |
| * For symlinks it sets the modified time of the link target. | |
| * | |
| * @param p | |
| * a {@link Path} object. | |
| * @param time | |
| * last modified time | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 5.1.9 | |
| */ | |
| public void setLastModified(Path p, Instant time) throws IOException { | |
| FileUtils.setLastModified(p, time); | |
| } | |
| /** | |
| * Get the length of a file or link, If the OS/JRE supports symbolic links | |
| * it's the length of the link, else the length of the target. | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return length of a file | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public long length(File path) throws IOException { | |
| return FileUtils.getLength(path); | |
| } | |
| /** | |
| * Delete a file. Throws an exception if delete fails. | |
| * | |
| * @param f | |
| * a {@link java.io.File} object. | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.3 | |
| */ | |
| public void delete(File f) throws IOException { | |
| FileUtils.delete(f); | |
| } | |
| /** | |
| * Resolve this file to its actual path name that the JRE can use. | |
| * <p> | |
| * This method can be relatively expensive. Computing a translation may | |
| * require forking an external process per path name translated. Callers | |
| * should try to minimize the number of translations necessary by caching | |
| * the results. | |
| * <p> | |
| * Not all platforms and JREs require path name translation. Currently only | |
| * Cygwin on Win32 require translation for Cygwin based paths. | |
| * | |
| * @param dir | |
| * directory relative to which the path name is. | |
| * @param name | |
| * path name to translate. | |
| * @return the translated path. <code>new File(dir,name)</code> if this | |
| * platform does not require path name translation. | |
| */ | |
| public File resolve(File dir, String name) { | |
| File abspn = new File(name); | |
| if (abspn.isAbsolute()) | |
| return abspn; | |
| return new File(dir, name); | |
| } | |
| /** | |
| * Determine the user's home directory (location where preferences are). | |
| * <p> | |
| * This method can be expensive on the first invocation if path name | |
| * translation is required. Subsequent invocations return a cached result. | |
| * <p> | |
| * Not all platforms and JREs require path name translation. Currently only | |
| * Cygwin on Win32 requires translation of the Cygwin HOME directory. | |
| * | |
| * @return the user's home directory; null if the user does not have one. | |
| */ | |
| public File userHome() { | |
| Holder<File> p = userHome; | |
| if (p == null) { | |
| p = new Holder<>(safeUserHomeImpl()); | |
| userHome = p; | |
| } | |
| return p.value; | |
| } | |
| private File safeUserHomeImpl() { | |
| File home; | |
| try { | |
| home = userHomeImpl(); | |
| if (home != null) { | |
| home.toPath(); | |
| return home; | |
| } | |
| } catch (RuntimeException e) { | |
| LOG.error(JGitText.get().exceptionWhileFindingUserHome, e); | |
| } | |
| home = defaultUserHomeImpl(); | |
| if (home != null) { | |
| try { | |
| home.toPath(); | |
| return home; | |
| } catch (InvalidPathException e) { | |
| LOG.error(MessageFormat | |
| .format(JGitText.get().invalidHomeDirectory, home), e); | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Set the user's home directory location. | |
| * | |
| * @param path | |
| * the location of the user's preferences; null if there is no | |
| * home directory for the current user. | |
| * @return {@code this}. | |
| */ | |
| public FS setUserHome(File path) { | |
| userHome = new Holder<>(path); | |
| return this; | |
| } | |
| /** | |
| * Does this file system have problems with atomic renames? | |
| * | |
| * @return true if the caller should retry a failed rename of a lock file. | |
| */ | |
| public abstract boolean retryFailedLockFileCommit(); | |
| /** | |
| * Return all the attributes of a file, without following symbolic links. | |
| * | |
| * @param file | |
| * the file | |
| * @return {@link BasicFileAttributes} of the file | |
| * @throws IOException | |
| * in case of any I/O errors accessing the file | |
| * | |
| * @since 4.5.6 | |
| */ | |
| public BasicFileAttributes fileAttributes(File file) throws IOException { | |
| return FileUtils.fileAttributes(file); | |
| } | |
| /** | |
| * Determine the user's home directory (location where preferences are). | |
| * | |
| * @return the user's home directory; null if the user does not have one. | |
| */ | |
| protected File userHomeImpl() { | |
| return defaultUserHomeImpl(); | |
| } | |
| private File defaultUserHomeImpl() { | |
| String home = SystemReader.getInstance().getProperty("user.home"); //$NON-NLS-1$ | |
| if (StringUtils.isEmptyOrNull(home)) { | |
| return null; | |
| } | |
| return new File(home).getAbsoluteFile(); | |
| } | |
| /** | |
| * Searches the given path to see if it contains one of the given files. | |
| * Returns the first it finds which is executable. Returns null if not found | |
| * or if path is null. | |
| * | |
| * @param path | |
| * List of paths to search separated by File.pathSeparator | |
| * @param lookFor | |
| * Files to search for in the given path | |
| * @return the first match found, or null | |
| * @since 3.0 | |
| */ | |
| @SuppressWarnings("StringSplitter") | |
| protected static File searchPath(String path, String... lookFor) { | |
| if (StringUtils.isEmptyOrNull(path) | |
| || EMPTY_PATH.matcher(path).find()) { | |
| return null; | |
| } | |
| for (String p : path.split(File.pathSeparator)) { | |
| for (String command : lookFor) { | |
| File file = new File(p, command); | |
| try { | |
| if (file.isFile() && file.canExecute()) { | |
| return file.getAbsoluteFile(); | |
| } | |
| } catch (SecurityException e) { | |
| LOG.warn(MessageFormat.format( | |
| JGitText.get().skipNotAccessiblePath, | |
| file.getPath())); | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| /** | |
| * Execute a command and return a single line of output as a String | |
| * | |
| * @param dir | |
| * Working directory for the command | |
| * @param command | |
| * as component array | |
| * @param encoding | |
| * to be used to parse the command's output | |
| * @return the one-line output of the command or {@code null} if there is | |
| * none | |
| * @throws org.eclipse.jgit.errors.CommandFailedException | |
| * thrown when the command failed (return code was non-zero) | |
| */ | |
| @Nullable | |
| protected static String readPipe(File dir, String[] command, | |
| String encoding) throws CommandFailedException { | |
| return readPipe(dir, command, encoding, null); | |
| } | |
| /** | |
| * Execute a command and return a single line of output as a String | |
| * | |
| * @param dir | |
| * Working directory for the command | |
| * @param command | |
| * as component array | |
| * @param encoding | |
| * to be used to parse the command's output | |
| * @param env | |
| * Map of environment variables to be merged with those of the | |
| * current process | |
| * @return the one-line output of the command or {@code null} if there is | |
| * none | |
| * @throws org.eclipse.jgit.errors.CommandFailedException | |
| * thrown when the command failed (return code was non-zero) | |
| * @since 4.0 | |
| */ | |
| @Nullable | |
| protected static String readPipe(File dir, String[] command, | |
| String encoding, Map<String, String> env) | |
| throws CommandFailedException { | |
| boolean debug = LOG.isDebugEnabled(); | |
| try { | |
| if (debug) { | |
| LOG.debug("readpipe {},{}", Arrays.asList(command), dir); //$NON-NLS-1$ | |
| } | |
| ProcessBuilder pb = new ProcessBuilder(command); | |
| pb.directory(dir); | |
| if (env != null) { | |
| pb.environment().putAll(env); | |
| } | |
| Process p; | |
| try { | |
| p = pb.start(); | |
| } catch (IOException e) { | |
| // Process failed to start | |
| throw new CommandFailedException(-1, e.getMessage(), e); | |
| } | |
| p.getOutputStream().close(); | |
| GobblerThread gobbler = new GobblerThread(p, command, dir); | |
| gobbler.start(); | |
| String r = null; | |
| Charset charset; | |
| try { | |
| charset = Charset.forName(encoding); | |
| } catch (IllegalArgumentException e) { | |
| throw new IOException("Unsupported charset: " + encoding, e); //$NON-NLS-1$ | |
| } | |
| try (BufferedReader lineRead = new BufferedReader( | |
| new InputStreamReader(p.getInputStream(), charset))) { | |
| r = lineRead.readLine(); | |
| if (debug) { | |
| LOG.debug("readpipe may return '{}'", //$NON-NLS-1$ | |
| StringUtils.escapeControls(r)); | |
| LOG.debug("remaining output:\n"); //$NON-NLS-1$ | |
| String l; | |
| while ((l = lineRead.readLine()) != null) { | |
| LOG.debug("{}", StringUtils.escapeControls(l)); //$NON-NLS-1$ | |
| } | |
| } | |
| } | |
| for (;;) { | |
| try { | |
| int rc = p.waitFor(); | |
| gobbler.join(); | |
| if (rc == 0 && !gobbler.fail.get()) { | |
| return r; | |
| } | |
| if (debug) { | |
| LOG.debug("readpipe rc=" + rc); //$NON-NLS-1$ | |
| } | |
| throw new CommandFailedException(rc, | |
| gobbler.errorMessage.get(), | |
| gobbler.exception.get()); | |
| } catch (InterruptedException ie) { | |
| // Stop bothering me, I have a zombie to reap. | |
| } | |
| } | |
| } catch (IOException e) { | |
| LOG.error("Caught exception in FS.readPipe()", e); //$NON-NLS-1$ | |
| } | |
| if (debug) { | |
| LOG.debug("readpipe returns null"); //$NON-NLS-1$ | |
| } | |
| return null; | |
| } | |
| private static class GobblerThread extends Thread { | |
| /* The process has 5 seconds to exit after closing stderr */ | |
| private static final int PROCESS_EXIT_TIMEOUT = 5; | |
| private final Process p; | |
| private final String desc; | |
| private final String dir; | |
| final AtomicBoolean fail = new AtomicBoolean(); | |
| final AtomicReference<String> errorMessage = new AtomicReference<>(); | |
| final AtomicReference<Throwable> exception = new AtomicReference<>(); | |
| GobblerThread(Process p, String[] command, File dir) { | |
| this.p = p; | |
| this.desc = Arrays.toString(command); | |
| this.dir = Objects.toString(dir); | |
| } | |
| @Override | |
| public void run() { | |
| StringBuilder err = new StringBuilder(); | |
| try (InputStream is = p.getErrorStream()) { | |
| int ch; | |
| while ((ch = is.read()) != -1) { | |
| err.append((char) ch); | |
| } | |
| } catch (IOException e) { | |
| if (waitForProcessCompletion(e) && p.exitValue() != 0) { | |
| setError(e, e.getMessage(), p.exitValue()); | |
| fail.set(true); | |
| } else { | |
| // ignore. command terminated faster and stream was just closed | |
| // or the process didn't terminate within timeout | |
| } | |
| } finally { | |
| if (waitForProcessCompletion(null) && err.length() > 0) { | |
| setError(null, err.toString(), p.exitValue()); | |
| if (p.exitValue() != 0) { | |
| fail.set(true); | |
| } | |
| } | |
| } | |
| } | |
| @SuppressWarnings("boxing") | |
| private boolean waitForProcessCompletion(IOException originalError) { | |
| try { | |
| if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) { | |
| setError(originalError, MessageFormat.format( | |
| JGitText.get().commandClosedStderrButDidntExit, | |
| desc, PROCESS_EXIT_TIMEOUT), -1); | |
| fail.set(true); | |
| return false; | |
| } | |
| } catch (InterruptedException e) { | |
| setError(originalError, MessageFormat.format( | |
| JGitText.get().threadInterruptedWhileRunning, desc), -1); | |
| fail.set(true); | |
| return false; | |
| } | |
| return true; | |
| } | |
| private void setError(IOException e, String message, int exitCode) { | |
| exception.set(e); | |
| errorMessage.set(MessageFormat.format( | |
| JGitText.get().exceptionCaughtDuringExecutionOfCommand, | |
| desc, dir, Integer.valueOf(exitCode), message)); | |
| } | |
| } | |
| /** | |
| * Discover the path to the Git executable. | |
| * | |
| * @return the path to the Git executable or {@code null} if it cannot be | |
| * determined. | |
| * @since 4.0 | |
| */ | |
| protected abstract File discoverGitExe(); | |
| /** | |
| * Discover the path to the system-wide Git configuration file | |
| * | |
| * @return the path to the system-wide Git configuration file or | |
| * {@code null} if it cannot be determined. | |
| * @since 4.0 | |
| */ | |
| protected File discoverGitSystemConfig() { | |
| File gitExe = discoverGitExe(); | |
| if (gitExe == null) { | |
| return null; | |
| } | |
| // Bug 480782: Check if the discovered git executable is JGit CLI | |
| String v; | |
| try { | |
| v = readPipe(gitExe.getParentFile(), | |
| new String[] { gitExe.getPath(), "--version" }, //$NON-NLS-1$ | |
| SystemReader.getInstance().getDefaultCharset().name()); | |
| } catch (CommandFailedException e) { | |
| LOG.warn(e.getMessage()); | |
| return null; | |
| } | |
| if (StringUtils.isEmptyOrNull(v) | |
| || (v != null && v.startsWith("jgit"))) { //$NON-NLS-1$ | |
| return null; | |
| } | |
| if (parseVersion(v) < makeVersion(2, 8, 0)) { | |
| // --show-origin was introduced in git 2.8.0. For older git: trick | |
| // it into printing the path to the config file by using "echo" as | |
| // the editor. | |
| Map<String, String> env = new HashMap<>(); | |
| env.put("GIT_EDITOR", "echo"); //$NON-NLS-1$ //$NON-NLS-2$ | |
| String w; | |
| try { | |
| // This command prints the path even if it doesn't exist | |
| w = readPipe(gitExe.getParentFile(), | |
| new String[] { gitExe.getPath(), "config", "--system", //$NON-NLS-1$ //$NON-NLS-2$ | |
| "--edit" }, //$NON-NLS-1$ | |
| SystemReader.getInstance().getDefaultCharset().name(), | |
| env); | |
| } catch (CommandFailedException e) { | |
| LOG.warn(e.getMessage()); | |
| return null; | |
| } | |
| if (StringUtils.isEmptyOrNull(w)) { | |
| return null; | |
| } | |
| return new File(w); | |
| } | |
| String w; | |
| try { | |
| w = readPipe(gitExe.getParentFile(), | |
| new String[] { gitExe.getPath(), "config", "--system", //$NON-NLS-1$ //$NON-NLS-2$ | |
| "--show-origin", "--list", "-z" }, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ | |
| SystemReader.getInstance().getDefaultCharset().name()); | |
| } catch (CommandFailedException e) { | |
| // This command fails if the system config doesn't exist | |
| if (LOG.isDebugEnabled()) { | |
| LOG.debug(e.getMessage()); | |
| } | |
| return null; | |
| } | |
| if (w == null) { | |
| return null; | |
| } | |
| // We get NUL-terminated items; the first one will be a file name, | |
| // prefixed by "file:". (Using -z is crucial, otherwise git quotes file | |
| // names with special characters.) | |
| int nul = w.indexOf(0); | |
| if (nul <= 0) { | |
| return null; | |
| } | |
| w = w.substring(0, nul); | |
| int colon = w.indexOf(':'); | |
| if (colon < 0) { | |
| return null; | |
| } | |
| w = w.substring(colon + 1); | |
| return w.isEmpty() ? null : new File(w); | |
| } | |
| private long parseVersion(String version) { | |
| Matcher m = VERSION.matcher(version); | |
| if (m.find()) { | |
| try { | |
| return makeVersion( | |
| Integer.parseInt(m.group(1)), | |
| Integer.parseInt(m.group(2)), | |
| Integer.parseInt(m.group(3))); | |
| } catch (NumberFormatException e) { | |
| // Ignore | |
| } | |
| } | |
| return -1; | |
| } | |
| private long makeVersion(int major, int minor, int patch) { | |
| return ((major * 10_000L) + minor) * 10_000L + patch; | |
| } | |
| /** | |
| * Get the currently used path to the system-wide Git configuration file. | |
| * | |
| * @return the currently used path to the system-wide Git configuration file | |
| * or {@code null} if none has been set. | |
| * @since 4.0 | |
| */ | |
| public File getGitSystemConfig() { | |
| if (gitSystemConfig == null) { | |
| gitSystemConfig = new Holder<>(discoverGitSystemConfig()); | |
| } | |
| return gitSystemConfig.value; | |
| } | |
| /** | |
| * Set the path to the system-wide Git configuration file to use. | |
| * | |
| * @param configFile | |
| * the path to the config file. | |
| * @return {@code this} | |
| * @since 4.0 | |
| */ | |
| public FS setGitSystemConfig(File configFile) { | |
| gitSystemConfig = new Holder<>(configFile); | |
| return this; | |
| } | |
| /** | |
| * Get the parent directory of this file's parent directory | |
| * | |
| * @param grandchild | |
| * a {@link java.io.File} object. | |
| * @return the parent directory of this file's parent directory or | |
| * {@code null} in case there's no grandparent directory | |
| * @since 4.0 | |
| */ | |
| protected static File resolveGrandparentFile(File grandchild) { | |
| if (grandchild != null) { | |
| File parent = grandchild.getParentFile(); | |
| if (parent != null) | |
| return parent.getParentFile(); | |
| } | |
| return null; | |
| } | |
| /** | |
| * Check if a file is a symbolic link and read it | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return target of link or null | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public String readSymLink(File path) throws IOException { | |
| return FileUtils.readSymLink(path); | |
| } | |
| /** | |
| * Whether the path is a symbolic link (and we support these). | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return true if the path is a symbolic link (and we support these) | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public boolean isSymLink(File path) throws IOException { | |
| return FileUtils.isSymlink(path); | |
| } | |
| /** | |
| * Tests if the path exists, in case of a symbolic link, true even if the | |
| * target does not exist | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return true if path exists | |
| * @since 3.0 | |
| */ | |
| public boolean exists(File path) { | |
| return FileUtils.exists(path); | |
| } | |
| /** | |
| * Check if path is a directory. If the OS/JRE supports symbolic links and | |
| * path is a symbolic link to a directory, this method returns false. | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return true if file is a directory, | |
| * @since 3.0 | |
| */ | |
| public boolean isDirectory(File path) { | |
| return FileUtils.isDirectory(path); | |
| } | |
| /** | |
| * Examine if path represents a regular file. If the OS/JRE supports | |
| * symbolic links the test returns false if path represents a symbolic link. | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return true if path represents a regular file | |
| * @since 3.0 | |
| */ | |
| public boolean isFile(File path) { | |
| return FileUtils.isFile(path); | |
| } | |
| /** | |
| * Whether path is hidden, either starts with . on unix or has the hidden | |
| * attribute in windows | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return true if path is hidden, either starts with . on unix or has the | |
| * hidden attribute in windows | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public boolean isHidden(File path) throws IOException { | |
| return FileUtils.isHidden(path); | |
| } | |
| /** | |
| * Set the hidden attribute for file whose name starts with a period. | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @param hidden | |
| * whether to set the file hidden | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public void setHidden(File path, boolean hidden) throws IOException { | |
| FileUtils.setHidden(path, hidden); | |
| } | |
| /** | |
| * Create a symbolic link | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @param target | |
| * target path of the symlink | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 3.0 | |
| */ | |
| public void createSymLink(File path, String target) throws IOException { | |
| FileUtils.createSymLink(path, target); | |
| } | |
| /** | |
| * A token representing a file created by | |
| * {@link #createNewFileAtomic(File)}. The token must be retained until the | |
| * file has been deleted in order to guarantee that the unique file was | |
| * created atomically. As soon as the file is no longer needed the lock | |
| * token must be closed. | |
| * | |
| * @since 4.7 | |
| */ | |
| public static class LockToken implements Closeable { | |
| private boolean isCreated; | |
| private Optional<Path> link; | |
| LockToken(boolean isCreated, Optional<Path> link) { | |
| this.isCreated = isCreated; | |
| this.link = link; | |
| } | |
| /** | |
| * Whether the file was created successfully | |
| * | |
| * @return {@code true} if the file was created successfully | |
| */ | |
| public boolean isCreated() { | |
| return isCreated; | |
| } | |
| @Override | |
| public void close() { | |
| if (!link.isPresent()) { | |
| return; | |
| } | |
| Path p = link.get(); | |
| if (!Files.exists(p)) { | |
| return; | |
| } | |
| try { | |
| Files.delete(p); | |
| } catch (IOException e) { | |
| LOG.error(MessageFormat | |
| .format(JGitText.get().closeLockTokenFailed, this), e); | |
| } | |
| } | |
| @Override | |
| public String toString() { | |
| return "LockToken [lockCreated=" + isCreated + //$NON-NLS-1$ | |
| ", link=" //$NON-NLS-1$ | |
| + (link.isPresent() ? link.get().getFileName() + "]" //$NON-NLS-1$ | |
| : "<null>]"); //$NON-NLS-1$ | |
| } | |
| } | |
| /** | |
| * Create a new file. See {@link java.io.File#createNewFile()}. Subclasses | |
| * of this class may take care to provide a safe implementation for this | |
| * even if {@link #supportsAtomicCreateNewFile()} is <code>false</code> | |
| * | |
| * @param path | |
| * the file to be created | |
| * @return LockToken this token must be closed after the created file was | |
| * deleted | |
| * @throws IOException | |
| * if an IO error occurred | |
| * @since 4.7 | |
| */ | |
| public LockToken createNewFileAtomic(File path) throws IOException { | |
| return new LockToken(path.createNewFile(), Optional.empty()); | |
| } | |
| /** | |
| * See | |
| * {@link org.eclipse.jgit.util.FileUtils#relativizePath(String, String, String, boolean)}. | |
| * | |
| * @param base | |
| * The path against which <code>other</code> should be | |
| * relativized. | |
| * @param other | |
| * The path that will be made relative to <code>base</code>. | |
| * @return A relative path that, when resolved against <code>base</code>, | |
| * will yield the original <code>other</code>. | |
| * @see FileUtils#relativizePath(String, String, String, boolean) | |
| * @since 3.7 | |
| */ | |
| public String relativize(String base, String other) { | |
| return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive()); | |
| } | |
| /** | |
| * Enumerates children of a directory. | |
| * | |
| * @param directory | |
| * to get the children of | |
| * @param fileModeStrategy | |
| * to use to calculate the git mode of a child | |
| * @return an array of entries for the children | |
| * | |
| * @since 5.0 | |
| */ | |
| public Entry[] list(File directory, FileModeStrategy fileModeStrategy) { | |
| File[] all = directory.listFiles(); | |
| if (all == null) { | |
| return NO_ENTRIES; | |
| } | |
| Entry[] result = new Entry[all.length]; | |
| for (int i = 0; i < result.length; i++) { | |
| result[i] = new FileEntry(all[i], this, fileModeStrategy); | |
| } | |
| return result; | |
| } | |
| /** | |
| * Checks whether the given hook is defined for the given repository, then | |
| * runs it with the given arguments. | |
| * <p> | |
| * The hook's standard output and error streams will be redirected to | |
| * <code>System.out</code> and <code>System.err</code> respectively. The | |
| * hook will have no stdin. | |
| * </p> | |
| * | |
| * @param repository | |
| * The repository for which a hook should be run. | |
| * @param hookName | |
| * The name of the hook to be executed. | |
| * @param args | |
| * Arguments to pass to this hook. Cannot be <code>null</code>, | |
| * but can be an empty array. | |
| * @return The ProcessResult describing this hook's execution. | |
| * @throws org.eclipse.jgit.api.errors.JGitInternalException | |
| * if we fail to run the hook somehow. Causes may include an | |
| * interrupted process or I/O errors. | |
| * @since 4.0 | |
| */ | |
| public ProcessResult runHookIfPresent(Repository repository, | |
| String hookName, String[] args) throws JGitInternalException { | |
| return runHookIfPresent(repository, hookName, args, System.out, | |
| System.err, null); | |
| } | |
| /** | |
| * Checks whether the given hook is defined for the given repository, then | |
| * runs it with the given arguments. | |
| * | |
| * @param repository | |
| * The repository for which a hook should be run. | |
| * @param hookName | |
| * The name of the hook to be executed. | |
| * @param args | |
| * Arguments to pass to this hook. Cannot be <code>null</code>, | |
| * but can be an empty array. | |
| * @param outRedirect | |
| * A print stream on which to redirect the hook's stdout. Can be | |
| * <code>null</code>, in which case the hook's standard output | |
| * will be lost. | |
| * @param errRedirect | |
| * A print stream on which to redirect the hook's stderr. Can be | |
| * <code>null</code>, in which case the hook's standard error | |
| * will be lost. | |
| * @param stdinArgs | |
| * A string to pass on to the standard input of the hook. May be | |
| * <code>null</code>. | |
| * @return The ProcessResult describing this hook's execution. | |
| * @throws org.eclipse.jgit.api.errors.JGitInternalException | |
| * if we fail to run the hook somehow. Causes may include an | |
| * interrupted process or I/O errors. | |
| * @since 5.11 | |
| */ | |
| public ProcessResult runHookIfPresent(Repository repository, | |
| String hookName, String[] args, OutputStream outRedirect, | |
| OutputStream errRedirect, String stdinArgs) | |
| throws JGitInternalException { | |
| return new ProcessResult(Status.NOT_SUPPORTED); | |
| } | |
| /** | |
| * See | |
| * {@link #runHookIfPresent(Repository, String, String[], OutputStream, OutputStream, String)} | |
| * . Should only be called by FS supporting shell scripts execution. | |
| * | |
| * @param repository | |
| * The repository for which a hook should be run. | |
| * @param hookName | |
| * The name of the hook to be executed. | |
| * @param args | |
| * Arguments to pass to this hook. Cannot be <code>null</code>, | |
| * but can be an empty array. | |
| * @param outRedirect | |
| * A print stream on which to redirect the hook's stdout. Can be | |
| * <code>null</code>, in which case the hook's standard output | |
| * will be lost. | |
| * @param errRedirect | |
| * A print stream on which to redirect the hook's stderr. Can be | |
| * <code>null</code>, in which case the hook's standard error | |
| * will be lost. | |
| * @param stdinArgs | |
| * A string to pass on to the standard input of the hook. May be | |
| * <code>null</code>. | |
| * @return The ProcessResult describing this hook's execution. | |
| * @throws org.eclipse.jgit.api.errors.JGitInternalException | |
| * if we fail to run the hook somehow. Causes may include an | |
| * interrupted process or I/O errors. | |
| * @since 5.11 | |
| */ | |
| protected ProcessResult internalRunHookIfPresent(Repository repository, | |
| String hookName, String[] args, OutputStream outRedirect, | |
| OutputStream errRedirect, String stdinArgs) | |
| throws JGitInternalException { | |
| File hookFile = findHook(repository, hookName); | |
| if (hookFile == null || hookName == null) { | |
| return new ProcessResult(Status.NOT_PRESENT); | |
| } | |
| File runDirectory = getRunDirectory(repository, hookName); | |
| if (runDirectory == null) { | |
| return new ProcessResult(Status.NOT_PRESENT); | |
| } | |
| String cmd = hookFile.getAbsolutePath(); | |
| ProcessBuilder hookProcess = runInShell(shellQuote(cmd), args); | |
| hookProcess.directory(runDirectory.getAbsoluteFile()); | |
| Map<String, String> environment = hookProcess.environment(); | |
| environment.put(Constants.GIT_DIR_KEY, | |
| repository.getDirectory().getAbsolutePath()); | |
| if (!repository.isBare()) { | |
| environment.put(Constants.GIT_COMMON_DIR_KEY, | |
| repository.getCommonDirectory().getAbsolutePath()); | |
| environment.put(Constants.GIT_WORK_TREE_KEY, | |
| repository.getWorkTree().getAbsolutePath()); | |
| } | |
| try { | |
| return new ProcessResult(runProcess(hookProcess, outRedirect, | |
| errRedirect, stdinArgs), Status.OK); | |
| } catch (IOException e) { | |
| throw new JGitInternalException(MessageFormat.format( | |
| JGitText.get().exceptionCaughtDuringExecutionOfHook, | |
| hookName), e); | |
| } catch (InterruptedException e) { | |
| throw new JGitInternalException(MessageFormat.format( | |
| JGitText.get().exceptionHookExecutionInterrupted, | |
| hookName), e); | |
| } | |
| } | |
| /** | |
| * Quote a string (such as a file system path obtained from a Java | |
| * {@link File} or {@link Path} object) such that it can be passed as first | |
| * argument to {@link #runInShell(String, String[])}. | |
| * <p> | |
| * This default implementation returns the string unchanged. | |
| * </p> | |
| * | |
| * @param cmd | |
| * the String to quote | |
| * @return the quoted string | |
| */ | |
| String shellQuote(String cmd) { | |
| return cmd; | |
| } | |
| /** | |
| * Tries to find a hook matching the given one in the given repository. | |
| * | |
| * @param repository | |
| * The repository within which to find a hook. | |
| * @param hookName | |
| * The name of the hook we're trying to find. | |
| * @return The {@link java.io.File} containing this particular hook if it | |
| * exists in the given repository, <code>null</code> otherwise. | |
| * @since 4.0 | |
| */ | |
| public File findHook(Repository repository, String hookName) { | |
| if (hookName == null) { | |
| return null; | |
| } | |
| File hookDir = getHooksDirectory(repository); | |
| if (hookDir == null) { | |
| return null; | |
| } | |
| File hookFile = new File(hookDir, hookName); | |
| if (hookFile.isAbsolute()) { | |
| if (!hookFile.exists() || (FS.DETECTED.supportsExecute() | |
| && !FS.DETECTED.canExecute(hookFile))) { | |
| return null; | |
| } | |
| } else { | |
| try { | |
| File runDirectory = getRunDirectory(repository, hookName); | |
| if (runDirectory == null) { | |
| return null; | |
| } | |
| Path hookPath = runDirectory.getAbsoluteFile().toPath() | |
| .resolve(hookFile.toPath()); | |
| FS fs = repository.getFS(); | |
| if (fs == null) { | |
| fs = FS.DETECTED; | |
| } | |
| if (!Files.exists(hookPath) || (fs.supportsExecute() | |
| && !fs.canExecute(hookPath.toFile()))) { | |
| return null; | |
| } | |
| hookFile = hookPath.toFile(); | |
| } catch (InvalidPathException e) { | |
| LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath, | |
| hookFile)); | |
| return null; | |
| } | |
| } | |
| return hookFile; | |
| } | |
| private File getRunDirectory(Repository repository, | |
| @NonNull String hookName) { | |
| if (repository.isBare()) { | |
| return repository.getDirectory(); | |
| } | |
| switch (hookName) { | |
| case "pre-receive": //$NON-NLS-1$ | |
| case "update": //$NON-NLS-1$ | |
| case "post-receive": //$NON-NLS-1$ | |
| case "post-update": //$NON-NLS-1$ | |
| case "push-to-checkout": //$NON-NLS-1$ | |
| return repository.getCommonDirectory(); | |
| default: | |
| return repository.getWorkTree(); | |
| } | |
| } | |
| private File getHooksDirectory(Repository repository) { | |
| Config config = repository.getConfig(); | |
| String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION, | |
| null, ConfigConstants.CONFIG_KEY_HOOKS_PATH); | |
| if (hooksDir != null) { | |
| return new File(hooksDir); | |
| } | |
| File dir = repository.getCommonDirectory(); | |
| return dir == null ? null : new File(dir, Constants.HOOKS); | |
| } | |
| /** | |
| * Runs the given process until termination, clearing its stdout and stderr | |
| * streams on-the-fly. | |
| * | |
| * @param processBuilder | |
| * The process builder configured for this process. | |
| * @param outRedirect | |
| * A OutputStream on which to redirect the processes stdout. Can | |
| * be <code>null</code>, in which case the processes standard | |
| * output will be lost. | |
| * @param errRedirect | |
| * A OutputStream on which to redirect the processes stderr. Can | |
| * be <code>null</code>, in which case the processes standard | |
| * error will be lost. | |
| * @param stdinArgs | |
| * A string to pass on to the standard input of the hook. Can be | |
| * <code>null</code>. | |
| * @return the exit value of this process. | |
| * @throws java.io.IOException | |
| * if an I/O error occurs while executing this process. | |
| * @throws java.lang.InterruptedException | |
| * if the current thread is interrupted while waiting for the | |
| * process to end. | |
| * @since 4.2 | |
| */ | |
| public int runProcess(ProcessBuilder processBuilder, | |
| OutputStream outRedirect, OutputStream errRedirect, String stdinArgs) | |
| throws IOException, InterruptedException { | |
| InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream( | |
| stdinArgs.getBytes(UTF_8)); | |
| return runProcess(processBuilder, outRedirect, errRedirect, in); | |
| } | |
| /** | |
| * Runs the given process until termination, clearing its stdout and stderr | |
| * streams on-the-fly. | |
| * | |
| * @param processBuilder | |
| * The process builder configured for this process. | |
| * @param outRedirect | |
| * An OutputStream on which to redirect the processes stdout. Can | |
| * be <code>null</code>, in which case the processes standard | |
| * output will be lost. | |
| * @param errRedirect | |
| * An OutputStream on which to redirect the processes stderr. Can | |
| * be <code>null</code>, in which case the processes standard | |
| * error will be lost. | |
| * @param inRedirect | |
| * An InputStream from which to redirect the processes stdin. Can | |
| * be <code>null</code>, in which case the process doesn't get | |
| * any data over stdin. It is assumed that the whole InputStream | |
| * will be consumed by the process. The method will close the | |
| * inputstream after all bytes are read. | |
| * @return the return code of this process. | |
| * @throws java.io.IOException | |
| * if an I/O error occurs while executing this process. | |
| * @throws java.lang.InterruptedException | |
| * if the current thread is interrupted while waiting for the | |
| * process to end. | |
| * @since 4.2 | |
| */ | |
| public int runProcess(ProcessBuilder processBuilder, | |
| OutputStream outRedirect, OutputStream errRedirect, | |
| InputStream inRedirect) throws IOException, | |
| InterruptedException { | |
| ExecutorService executor = Executors.newFixedThreadPool(2); | |
| Process process = null; | |
| // We'll record the first I/O exception that occurs, but keep on trying | |
| // to dispose of our open streams and file handles | |
| IOException ioException = null; | |
| try { | |
| process = processBuilder.start(); | |
| executor.execute( | |
| new StreamGobbler(process.getErrorStream(), errRedirect)); | |
| executor.execute( | |
| new StreamGobbler(process.getInputStream(), outRedirect)); | |
| @SuppressWarnings("resource") // Closed in the finally block | |
| OutputStream outputStream = process.getOutputStream(); | |
| try { | |
| if (inRedirect != null) { | |
| new StreamGobbler(inRedirect, outputStream).copy(); | |
| } | |
| } finally { | |
| try { | |
| outputStream.close(); | |
| } catch (IOException e) { | |
| // When the process exits before consuming the input, the OutputStream | |
| // is replaced with the null output stream. This null output stream | |
| // throws IOException for all write calls. When StreamGobbler fails to | |
| // flush the buffer because of this, this close call tries to flush it | |
| // again. This causes another IOException. Since we ignore the | |
| // IOException in StreamGobbler, we also ignore the exception here. | |
| } | |
| } | |
| return process.waitFor(); | |
| } catch (IOException e) { | |
| ioException = e; | |
| } finally { | |
| shutdownAndAwaitTermination(executor); | |
| if (process != null) { | |
| try { | |
| process.waitFor(); | |
| } catch (InterruptedException e) { | |
| // Thrown by the outer try. | |
| // Swallow this one to carry on our cleanup, and clear the | |
| // interrupted flag (processes throw the exception without | |
| // clearing the flag). | |
| Thread.interrupted(); | |
| } | |
| // A process doesn't clean its own resources even when destroyed | |
| // Explicitly try and close all three streams, preserving the | |
| // outer I/O exception if any. | |
| if (inRedirect != null) { | |
| inRedirect.close(); | |
| } | |
| try { | |
| process.getErrorStream().close(); | |
| } catch (IOException e) { | |
| ioException = ioException != null ? ioException : e; | |
| } | |
| try { | |
| process.getInputStream().close(); | |
| } catch (IOException e) { | |
| ioException = ioException != null ? ioException : e; | |
| } | |
| try { | |
| process.getOutputStream().close(); | |
| } catch (IOException e) { | |
| ioException = ioException != null ? ioException : e; | |
| } | |
| process.destroy(); | |
| } | |
| } | |
| // We can only be here if the outer try threw an IOException. | |
| throw ioException; | |
| } | |
| /** | |
| * Shuts down an {@link ExecutorService} in two phases, first by calling | |
| * {@link ExecutorService#shutdown() shutdown} to reject incoming tasks, and | |
| * then calling {@link ExecutorService#shutdownNow() shutdownNow}, if | |
| * necessary, to cancel any lingering tasks. Returns true if the pool has | |
| * been properly shutdown, false otherwise. | |
| * <p> | |
| * | |
| * @param pool | |
| * the pool to shutdown | |
| * @return <code>true</code> if the pool has been properly shutdown, | |
| * <code>false</code> otherwise. | |
| */ | |
| private static boolean shutdownAndAwaitTermination(ExecutorService pool) { | |
| boolean hasShutdown = true; | |
| pool.shutdown(); // Disable new tasks from being submitted | |
| try { | |
| // Wait a while for existing tasks to terminate | |
| if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { | |
| pool.shutdownNow(); // Cancel currently executing tasks | |
| // Wait a while for tasks to respond to being canceled | |
| if (!pool.awaitTermination(60, TimeUnit.SECONDS)) | |
| hasShutdown = false; | |
| } | |
| } catch (InterruptedException ie) { | |
| // (Re-)Cancel if current thread also interrupted | |
| pool.shutdownNow(); | |
| // Preserve interrupt status | |
| Thread.currentThread().interrupt(); | |
| hasShutdown = false; | |
| } | |
| return hasShutdown; | |
| } | |
| /** | |
| * Initialize a ProcessBuilder to run a command using the system shell. | |
| * | |
| * @param cmd | |
| * command to execute. This string should originate from the | |
| * end-user, and thus is platform specific. | |
| * @param args | |
| * arguments to pass to command. These should be protected from | |
| * shell evaluation. | |
| * @return a partially completed process builder. Caller should finish | |
| * populating directory, environment, and then start the process. | |
| */ | |
| public abstract ProcessBuilder runInShell(String cmd, String[] args); | |
| /** | |
| * Execute a command defined by a {@link java.lang.ProcessBuilder}. | |
| * | |
| * @param pb | |
| * The command to be executed | |
| * @param in | |
| * The standard input stream passed to the process | |
| * @return The result of the executed command | |
| * @throws java.lang.InterruptedException | |
| * if thread was interrupted | |
| * @throws java.io.IOException | |
| * if an IO error occurred | |
| * @since 4.2 | |
| */ | |
| public ExecutionResult execute(ProcessBuilder pb, InputStream in) | |
| throws IOException, InterruptedException { | |
| try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null); | |
| TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024, | |
| 1024 * 1024)) { | |
| int rc = runProcess(pb, stdout, stderr, in); | |
| return new ExecutionResult(stdout, stderr, rc); | |
| } | |
| } | |
| private static class Holder<V> { | |
| final V value; | |
| Holder(V value) { | |
| this.value = value; | |
| } | |
| } | |
| /** | |
| * File attributes we typically care for. | |
| * | |
| * @since 3.3 | |
| */ | |
| public static class Attributes { | |
| /** | |
| * Whether this are attributes of a directory | |
| * | |
| * @return true if this are the attributes of a directory | |
| */ | |
| public boolean isDirectory() { | |
| return isDirectory; | |
| } | |
| /** | |
| * Whether this are attributes of an executable file | |
| * | |
| * @return true if this are the attributes of an executable file | |
| */ | |
| public boolean isExecutable() { | |
| return isExecutable; | |
| } | |
| /** | |
| * Whether this are the attributes of a symbolic link | |
| * | |
| * @return true if this are the attributes of a symbolic link | |
| */ | |
| public boolean isSymbolicLink() { | |
| return isSymbolicLink; | |
| } | |
| /** | |
| * Whether this are the attributes of a regular file | |
| * | |
| * @return true if this are the attributes of a regular file | |
| */ | |
| public boolean isRegularFile() { | |
| return isRegularFile; | |
| } | |
| /** | |
| * Get the file creation time | |
| * | |
| * @return the time when the file was created | |
| */ | |
| public long getCreationTime() { | |
| return creationTime; | |
| } | |
| /** | |
| * Get the time when this object was last modified | |
| * | |
| * @return the time when this object was last modified | |
| * @since 5.1.9 | |
| */ | |
| public Instant getLastModifiedInstant() { | |
| return lastModifiedInstant; | |
| } | |
| private final boolean isDirectory; | |
| private final boolean isSymbolicLink; | |
| private final boolean isRegularFile; | |
| private final long creationTime; | |
| private final Instant lastModifiedInstant; | |
| private final boolean isExecutable; | |
| private final File file; | |
| private final boolean exists; | |
| /** | |
| * file length | |
| */ | |
| protected long length = -1; | |
| final FS fs; | |
| Attributes(FS fs, File file, boolean exists, boolean isDirectory, | |
| boolean isExecutable, boolean isSymbolicLink, | |
| boolean isRegularFile, long creationTime, | |
| Instant lastModifiedInstant, long length) { | |
| this.fs = fs; | |
| this.file = file; | |
| this.exists = exists; | |
| this.isDirectory = isDirectory; | |
| this.isExecutable = isExecutable; | |
| this.isSymbolicLink = isSymbolicLink; | |
| this.isRegularFile = isRegularFile; | |
| this.creationTime = creationTime; | |
| this.lastModifiedInstant = lastModifiedInstant; | |
| this.length = length; | |
| } | |
| /** | |
| * Constructor when there are issues with reading. All attributes except | |
| * given will be set to the default values. | |
| * | |
| * @param path | |
| * file path | |
| * @param fs | |
| * FS to use | |
| */ | |
| public Attributes(File path, FS fs) { | |
| this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L); | |
| } | |
| /** | |
| * Get the length of this file | |
| * | |
| * @return length of this file object | |
| */ | |
| public long getLength() { | |
| if (length == -1) | |
| return length = file.length(); | |
| return length; | |
| } | |
| /** | |
| * Get the filename | |
| * | |
| * @return the filename | |
| */ | |
| public String getName() { | |
| return file.getName(); | |
| } | |
| /** | |
| * Get the file the attributes apply to | |
| * | |
| * @return the file the attributes apply to | |
| */ | |
| public File getFile() { | |
| return file; | |
| } | |
| boolean exists() { | |
| return exists; | |
| } | |
| } | |
| /** | |
| * Get the file attributes we care for. | |
| * | |
| * @param path | |
| * a {@link java.io.File} object. | |
| * @return the file attributes we care for. | |
| * @since 3.3 | |
| */ | |
| public Attributes getAttributes(File path) { | |
| boolean isDirectory = isDirectory(path); | |
| boolean isFile = !isDirectory && path.isFile(); | |
| assert path.exists() == isDirectory || isFile; | |
| boolean exists = isDirectory || isFile; | |
| boolean canExecute = exists && !isDirectory && canExecute(path); | |
| boolean isSymlink = false; | |
| Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH; | |
| long createTime = 0L; | |
| return new Attributes(this, path, exists, isDirectory, canExecute, | |
| isSymlink, isFile, createTime, lastModified, -1); | |
| } | |
| /** | |
| * Normalize the unicode path to composed form. | |
| * | |
| * @param file | |
| * a {@link java.io.File} object. | |
| * @return NFC-format File | |
| * @since 3.3 | |
| */ | |
| public File normalize(File file) { | |
| return file; | |
| } | |
| /** | |
| * Normalize the unicode path to composed form. | |
| * | |
| * @param name | |
| * path name | |
| * @return NFC-format string | |
| * @since 3.3 | |
| */ | |
| public String normalize(String name) { | |
| return name; | |
| } | |
| /** | |
| * Get common dir path. | |
| * | |
| * @param dir | |
| * the .git folder | |
| * @return common dir path | |
| * @throws IOException | |
| * if commondir file can't be read | |
| * | |
| * @since 7.0 | |
| */ | |
| public File getCommonDir(File dir) throws IOException { | |
| // first the GIT_COMMON_DIR is same as GIT_DIR | |
| File commonDir = dir; | |
| // now check if commondir file exists (e.g. worktree repository) | |
| File commonDirFile = new File(dir, Constants.COMMONDIR_FILE); | |
| if (commonDirFile.isFile()) { | |
| String commonDirPath = new String(IO.readFully(commonDirFile)) | |
| .trim(); | |
| commonDir = new File(commonDirPath); | |
| if (!commonDir.isAbsolute()) { | |
| commonDir = new File(dir, commonDirPath).getCanonicalFile(); | |
| } | |
| } | |
| return commonDir; | |
| } | |
| /** | |
| * This runnable will consume an input stream's content into an output | |
| * stream as soon as it gets available. | |
| * <p> | |
| * Typically used to empty processes' standard output and error, preventing | |
| * them to choke. | |
| * </p> | |
| * <p> | |
| * <b>Note</b> that a {@link StreamGobbler} will never close either of its | |
| * streams. | |
| * </p> | |
| */ | |
| private static class StreamGobbler implements Runnable { | |
| private InputStream in; | |
| private OutputStream out; | |
| public StreamGobbler(InputStream stream, OutputStream output) { | |
| this.in = stream; | |
| this.out = output; | |
| } | |
| @Override | |
| public void run() { | |
| try { | |
| copy(); | |
| } catch (IOException e) { | |
| // Do nothing on read failure; leave streams open. | |
| } | |
| } | |
| void copy() throws IOException { | |
| boolean writeFailure = false; | |
| byte[] buffer = new byte[4096]; | |
| int readBytes; | |
| while ((readBytes = in.read(buffer)) != -1) { | |
| // Do not try to write again after a failure, but keep | |
| // reading as long as possible to prevent the input stream | |
| // from choking. | |
| if (!writeFailure && out != null) { | |
| try { | |
| out.write(buffer, 0, readBytes); | |
| out.flush(); | |
| } catch (IOException e) { | |
| writeFailure = true; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } |