1
2
3
4
5
6
7
8
9
10
11 package org.eclipse.jgit.util;
12
13 import static java.nio.charset.StandardCharsets.UTF_8;
14 import static java.time.Instant.EPOCH;
15
16 import java.io.BufferedReader;
17 import java.io.ByteArrayInputStream;
18 import java.io.Closeable;
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.io.OutputStream;
24 import java.io.OutputStreamWriter;
25 import java.io.PrintStream;
26 import java.io.Writer;
27 import java.nio.charset.Charset;
28 import java.nio.file.AccessDeniedException;
29 import java.nio.file.FileStore;
30 import java.nio.file.Files;
31 import java.nio.file.InvalidPathException;
32 import java.nio.file.Path;
33 import java.nio.file.attribute.BasicFileAttributes;
34 import java.nio.file.attribute.FileTime;
35 import java.security.AccessControlException;
36 import java.security.AccessController;
37 import java.security.PrivilegedAction;
38 import java.text.MessageFormat;
39 import java.time.Duration;
40 import java.time.Instant;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.HashMap;
44 import java.util.Map;
45 import java.util.Objects;
46 import java.util.Optional;
47 import java.util.UUID;
48 import java.util.concurrent.CancellationException;
49 import java.util.concurrent.CompletableFuture;
50 import java.util.concurrent.ConcurrentHashMap;
51 import java.util.concurrent.ExecutionException;
52 import java.util.concurrent.Executor;
53 import java.util.concurrent.ExecutorService;
54 import java.util.concurrent.Executors;
55 import java.util.concurrent.LinkedBlockingQueue;
56 import java.util.concurrent.ThreadPoolExecutor;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.TimeoutException;
59 import java.util.concurrent.atomic.AtomicBoolean;
60 import java.util.concurrent.atomic.AtomicInteger;
61 import java.util.concurrent.atomic.AtomicReference;
62 import java.util.concurrent.locks.Lock;
63 import java.util.concurrent.locks.ReentrantLock;
64
65 import org.eclipse.jgit.annotations.NonNull;
66 import org.eclipse.jgit.annotations.Nullable;
67 import org.eclipse.jgit.api.errors.JGitInternalException;
68 import org.eclipse.jgit.errors.CommandFailedException;
69 import org.eclipse.jgit.errors.ConfigInvalidException;
70 import org.eclipse.jgit.errors.LockFailedException;
71 import org.eclipse.jgit.internal.JGitText;
72 import org.eclipse.jgit.internal.storage.file.FileSnapshot;
73 import org.eclipse.jgit.lib.Config;
74 import org.eclipse.jgit.lib.ConfigConstants;
75 import org.eclipse.jgit.lib.Constants;
76 import org.eclipse.jgit.lib.Repository;
77 import org.eclipse.jgit.lib.StoredConfig;
78 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
79 import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy;
80 import org.eclipse.jgit.treewalk.WorkingTreeIterator.Entry;
81 import org.eclipse.jgit.util.ProcessResult.Status;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
84
85
86
87
88 public abstract class FS {
89 private static final Logger LOG = LoggerFactory.getLogger(FS.class);
90
91
92
93
94
95
96
97 protected static final Entry[] NO_ENTRIES = {};
98
99 private volatile Boolean supportSymlinks;
100
101
102
103
104
105
106
107 public static class FSFactory {
108
109
110
111 protected FSFactory() {
112
113 }
114
115
116
117
118
119
120
121 public FS detect(Boolean cygwinUsed) {
122 if (SystemReader.getInstance().isWindows()) {
123 if (cygwinUsed == null) {
124 cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin());
125 }
126 if (cygwinUsed.booleanValue()) {
127 return new FS_Win32_Cygwin();
128 }
129 return new FS_Win32();
130 }
131 return new FS_POSIX();
132 }
133 }
134
135
136
137
138
139
140
141 public static class ExecutionResult {
142 private TemporaryBuffer stdout;
143
144 private TemporaryBuffer stderr;
145
146 private int rc;
147
148
149
150
151
152
153 public ExecutionResult(TemporaryBuffer./../org/eclipse/jgit/util/TemporaryBuffer.html#TemporaryBuffer">TemporaryBuffer stdout, TemporaryBuffer stderr,
154 int rc) {
155 this.stdout = stdout;
156 this.stderr = stderr;
157 this.rc = rc;
158 }
159
160
161
162
163 public TemporaryBuffer getStdout() {
164 return stdout;
165 }
166
167
168
169
170 public TemporaryBuffer getStderr() {
171 return stderr;
172 }
173
174
175
176
177 public int getRc() {
178 return rc;
179 }
180 }
181
182
183
184
185
186
187 public static final class FileStoreAttributes {
188
189
190
191
192 private static final Duration UNDEFINED_DURATION = Duration
193 .ofNanos(Long.MAX_VALUE);
194
195
196
197
198
199
200
201
202 public static final Duration FALLBACK_TIMESTAMP_RESOLUTION = Duration
203 .ofMillis(2000);
204
205
206
207
208
209
210 public static final FileStoreAttributes FALLBACK_FILESTORE_ATTRIBUTES = new FileStoreAttributes(
211 FALLBACK_TIMESTAMP_RESOLUTION);
212
213 private static final long ONE_MICROSECOND = TimeUnit.MICROSECONDS
214 .toNanos(1);
215
216 private static final long ONE_MILLISECOND = TimeUnit.MILLISECONDS
217 .toNanos(1);
218
219 private static final long ONE_SECOND = TimeUnit.SECONDS.toNanos(1);
220
221
222
223
224
225
226
227
228
229
230 private static final long MINIMUM_RESOLUTION_NANOS = ONE_MICROSECOND;
231
232 private static final String JAVA_VERSION_PREFIX = System
233 .getProperty("java.vendor") + '|'
234 + System.getProperty("java.version") + '|';
235
236 private static final Duration FALLBACK_MIN_RACY_INTERVAL = Duration
237 .ofMillis(10);
238
239 private static final Map<FileStore, FileStoreAttributes> attributeCache = new ConcurrentHashMap<>();
240
241 private static final SimpleLruCache<Path, FileStoreAttributes> attrCacheByPath = new SimpleLruCache<>(
242 100, 0.2f);
243
244 private static final AtomicBoolean background = new AtomicBoolean();
245
246 private static final Map<FileStore, Lock> locks = new ConcurrentHashMap<>();
247
248 private static final AtomicInteger threadNumber = new AtomicInteger(1);
249
250
251
252
253
254
255
256
257
258
259
260
261
262 private static final Executor FUTURE_RUNNER = new ThreadPoolExecutor(0,
263 5, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
264 runnable -> {
265 Thread t = new Thread(runnable,
266 "JGit-FileStoreAttributeReader-"
267 + threadNumber.getAndIncrement());
268
269
270 t.setDaemon(true);
271 return t;
272 });
273
274
275
276
277
278
279
280
281
282
283
284
285 private static final Executor SAVE_RUNNER = new ThreadPoolExecutor(0, 1,
286 1L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
287 runnable -> {
288 Thread t = new Thread(runnable,
289 "JGit-FileStoreAttributeWriter-"
290 + threadNumber.getAndIncrement());
291
292 t.setDaemon(false);
293 return t;
294 });
295
296
297
298
299
300
301
302
303
304
305 public static void setBackground(boolean async) {
306 background.set(async);
307 }
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324 public static void configureAttributesPathCache(int maxSize,
325 float purgeFactor) {
326 FileStoreAttributes.attrCacheByPath.configure(maxSize, purgeFactor);
327 }
328
329
330
331
332
333
334
335
336 public static FileStoreAttributes get(Path path) {
337 try {
338 path = path.toAbsolutePath();
339 Path dir = Files.isDirectory(path) ? path : path.getParent();
340 FileStoreAttributes cached = attrCacheByPath.get(dir);
341 if (cached != null) {
342 return cached;
343 }
344 FileStoreAttributes attrs = getFileStoreAttributes(dir);
345 if (attrs == null) {
346
347 return FALLBACK_FILESTORE_ATTRIBUTES;
348 }
349 attrCacheByPath.put(dir, attrs);
350 return attrs;
351 } catch (SecurityException e) {
352 return FALLBACK_FILESTORE_ATTRIBUTES;
353 }
354 }
355
356 private static FileStoreAttributes getFileStoreAttributes(Path dir) {
357 FileStore s;
358 try {
359 if (Files.exists(dir)) {
360 s = Files.getFileStore(dir);
361 FileStoreAttributes c = attributeCache.get(s);
362 if (c != null) {
363 return c;
364 }
365 if (!Files.isWritable(dir)) {
366
367 LOG.debug(
368 "{}: cannot measure timestamp resolution in read-only directory {}",
369 Thread.currentThread(), dir);
370 return FALLBACK_FILESTORE_ATTRIBUTES;
371 }
372 } else {
373
374 LOG.debug(
375 "{}: cannot measure timestamp resolution of unborn directory {}",
376 Thread.currentThread(), dir);
377 return FALLBACK_FILESTORE_ATTRIBUTES;
378 }
379
380 CompletableFuture<Optional<FileStoreAttributes>> f = CompletableFuture
381 .supplyAsync(() -> {
382 Lock lock = locks.computeIfAbsent(s,
383 l -> new ReentrantLock());
384 if (!lock.tryLock()) {
385 LOG.debug(
386 "{}: couldn't get lock to measure timestamp resolution in {}",
387 Thread.currentThread(), dir);
388 return Optional.empty();
389 }
390 Optional<FileStoreAttributes> attributes = Optional
391 .empty();
392 try {
393
394
395
396 FileStoreAttributes c = attributeCache.get(s);
397 if (c != null) {
398 return Optional.of(c);
399 }
400 attributes = readFromConfig(s);
401 if (attributes.isPresent()) {
402 attributeCache.put(s, attributes.get());
403 return attributes;
404 }
405
406 Optional<Duration> resolution = measureFsTimestampResolution(
407 s, dir);
408 if (resolution.isPresent()) {
409 c = new FileStoreAttributes(
410 resolution.get());
411 attributeCache.put(s, c);
412
413
414 if (c.fsTimestampResolution
415 .toNanos() < 100_000_000L) {
416 c.minimalRacyInterval = measureMinimalRacyInterval(
417 dir);
418 }
419 if (LOG.isDebugEnabled()) {
420 LOG.debug(c.toString());
421 }
422 FileStoreAttributes newAttrs = c;
423 SAVE_RUNNER.execute(
424 () -> saveToConfig(s, newAttrs));
425 }
426 attributes = Optional.of(c);
427 } finally {
428 lock.unlock();
429 locks.remove(s);
430 }
431 return attributes;
432 }, FUTURE_RUNNER);
433 f = f.exceptionally(e -> {
434 LOG.error(e.getLocalizedMessage(), e);
435 return Optional.empty();
436 });
437
438
439 boolean runInBackground = background.get();
440 Optional<FileStoreAttributes> d = runInBackground ? f.get(
441 100, TimeUnit.MILLISECONDS) : f.get();
442 if (d.isPresent()) {
443 return d.get();
444 } else if (runInBackground) {
445
446 return null;
447 }
448
449 } catch (IOException | InterruptedException
450 | ExecutionException | CancellationException e) {
451 LOG.error(e.getMessage(), e);
452 } catch (TimeoutException | SecurityException e) {
453
454 }
455 LOG.debug("{}: use fallback timestamp resolution for directory {}",
456 Thread.currentThread(), dir);
457 return FALLBACK_FILESTORE_ATTRIBUTES;
458 }
459
460 @SuppressWarnings("boxing")
461 private static Duration measureMinimalRacyInterval(Path dir) {
462 LOG.debug("{}: start measure minimal racy interval in {}",
463 Thread.currentThread(), dir);
464 int n = 0;
465 int failures = 0;
466 long racyNanos = 0;
467 ArrayList<Long> deltas = new ArrayList<>();
468 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
469 Instant end = Instant.now().plusSeconds(3);
470 try {
471 Files.createFile(probe);
472 do {
473 n++;
474 write(probe, "a");
475 FileSnapshot snapshot = FileSnapshot.save(probe.toFile());
476 read(probe);
477 write(probe, "b");
478 if (!snapshot.isModified(probe.toFile())) {
479 deltas.add(Long.valueOf(snapshot.lastDelta()));
480 racyNanos = snapshot.lastRacyThreshold();
481 failures++;
482 }
483 } while (Instant.now().compareTo(end) < 0);
484 } catch (IOException e) {
485 LOG.error(e.getMessage(), e);
486 return FALLBACK_MIN_RACY_INTERVAL;
487 } finally {
488 deleteProbe(probe);
489 }
490 if (failures > 0) {
491 Stats stats = new Stats();
492 for (Long d : deltas) {
493 stats.add(d);
494 }
495 LOG.debug(
496 "delta [ns] since modification FileSnapshot failed to detect\n"
497 + "count, failures, racy limit [ns], delta min [ns],"
498 + " delta max [ns], delta avg [ns],"
499 + " delta stddev [ns]\n"
500 + "{}, {}, {}, {}, {}, {}, {}",
501 n, failures, racyNanos, stats.min(), stats.max(),
502 stats.avg(), stats.stddev());
503 return Duration
504 .ofNanos(Double.valueOf(stats.max()).longValue());
505 }
506
507
508 LOG.debug("{}: no failures when measuring minimal racy interval",
509 Thread.currentThread());
510 return Duration.ZERO;
511 }
512
513 private static void write(Path p, String body) throws IOException {
514 FileUtils.mkdirs(p.getParent().toFile(), true);
515 try (Writer w = new OutputStreamWriter(Files.newOutputStream(p),
516 UTF_8)) {
517 w.write(body);
518 }
519 }
520
521 private static String read(Path p) throws IOException {
522 final byte[] body = IO.readFully(p.toFile());
523 return new String(body, 0, body.length, UTF_8);
524 }
525
526 private static Optional<Duration> measureFsTimestampResolution(
527 FileStore s, Path dir) {
528 if (LOG.isDebugEnabled()) {
529 LOG.debug("{}: start measure timestamp resolution {} in {}",
530 Thread.currentThread(), s, dir);
531 }
532 Path probe = dir.resolve(".probe-" + UUID.randomUUID());
533 try {
534 Files.createFile(probe);
535 Duration fsResolution = getFsResolution(s, dir, probe);
536 Duration clockResolution = measureClockResolution();
537 fsResolution = fsResolution.plus(clockResolution);
538 if (LOG.isDebugEnabled()) {
539 LOG.debug(
540 "{}: end measure timestamp resolution {} in {}; got {}",
541 Thread.currentThread(), s, dir, fsResolution);
542 }
543 return Optional.of(fsResolution);
544 } catch (SecurityException e) {
545
546
547
548 LOG.warn(e.getLocalizedMessage(), e);
549 } catch (AccessDeniedException e) {
550 LOG.warn(e.getLocalizedMessage(), e);
551 } catch (IOException e) {
552 LOG.error(e.getLocalizedMessage(), e);
553 } finally {
554 deleteProbe(probe);
555 }
556 return Optional.empty();
557 }
558
559 private static Duration getFsResolution(FileStore s, Path dir,
560 Path probe) throws IOException {
561 File probeFile = probe.toFile();
562 FileTime t1 = Files.getLastModifiedTime(probe);
563 Instant t1i = t1.toInstant();
564 FileTime t2;
565 Duration last = FALLBACK_TIMESTAMP_RESOLUTION;
566 long minScale = MINIMUM_RESOLUTION_NANOS;
567 long scale = ONE_SECOND;
568 long high = TimeUnit.MILLISECONDS.toSeconds(last.toMillis());
569 long low = 0;
570
571 long[] tries = { ONE_MICROSECOND, ONE_MILLISECOND };
572 for (long interval : tries) {
573 if (interval >= ONE_MILLISECOND) {
574 probeFile.setLastModified(
575 t1i.plusNanos(interval).toEpochMilli());
576 } else {
577 Files.setLastModifiedTime(probe,
578 FileTime.from(t1i.plusNanos(interval)));
579 }
580 t2 = Files.getLastModifiedTime(probe);
581 if (t2.compareTo(t1) > 0) {
582 Duration diff = Duration.between(t1i, t2.toInstant());
583 if (!diff.isZero() && !diff.isNegative()
584 && diff.compareTo(last) < 0) {
585 scale = interval;
586 high = 1;
587 last = diff;
588 break;
589 }
590 } else {
591
592 minScale = Math.max(minScale, interval);
593 }
594 }
595
596 while (high > low) {
597 long mid = (high + low) / 2;
598 if (mid == 0) {
599
600 long newScale = scale / 10;
601 if (newScale < minScale) {
602 break;
603 }
604 high *= scale / newScale;
605 low *= scale / newScale;
606 scale = newScale;
607 mid = (high + low) / 2;
608 }
609 long delta = mid * scale;
610 if (scale >= ONE_MILLISECOND) {
611 probeFile.setLastModified(
612 t1i.plusNanos(delta).toEpochMilli());
613 } else {
614 Files.setLastModifiedTime(probe,
615 FileTime.from(t1i.plusNanos(delta)));
616 }
617 t2 = Files.getLastModifiedTime(probe);
618 int cmp = t2.compareTo(t1);
619 if (cmp > 0) {
620 high = mid;
621 Duration diff = Duration.between(t1i, t2.toInstant());
622 if (diff.isZero() || diff.isNegative()) {
623 LOG.warn(JGitText.get().logInconsistentFiletimeDiff,
624 Thread.currentThread(), s, dir, t2, t1, diff,
625 last);
626 break;
627 } else if (diff.compareTo(last) > 0) {
628 LOG.warn(JGitText.get().logLargerFiletimeDiff,
629 Thread.currentThread(), s, dir, diff, last);
630 break;
631 }
632 last = diff;
633 } else if (cmp < 0) {
634 LOG.warn(JGitText.get().logSmallerFiletime,
635 Thread.currentThread(), s, dir, t2, t1, last);
636 break;
637 } else {
638
639 low = mid + 1;
640 }
641 }
642 return last;
643 }
644
645 private static Duration measureClockResolution() {
646 Duration clockResolution = Duration.ZERO;
647 for (int i = 0; i < 10; i++) {
648 Instant t1 = Instant.now();
649 Instant t2 = t1;
650 while (t2.compareTo(t1) <= 0) {
651 t2 = Instant.now();
652 }
653 Duration r = Duration.between(t1, t2);
654 if (r.compareTo(clockResolution) > 0) {
655 clockResolution = r;
656 }
657 }
658 return clockResolution;
659 }
660
661 private static void deleteProbe(Path probe) {
662 try {
663 FileUtils.delete(probe.toFile(),
664 FileUtils.SKIP_MISSING | FileUtils.RETRY);
665 } catch (IOException e) {
666 LOG.error(e.getMessage(), e);
667 }
668 }
669
670 private static Optional<FileStoreAttributes> readFromConfig(
671 FileStore s) {
672 StoredConfig userConfig;
673 try {
674 userConfig = SystemReader.getInstance().getUserConfig();
675 } catch (IOException | ConfigInvalidException e) {
676 LOG.error(JGitText.get().readFileStoreAttributesFailed, e);
677 return Optional.empty();
678 }
679 String key = getConfigKey(s);
680 Duration resolution = Duration.ofNanos(userConfig.getTimeUnit(
681 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
682 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
683 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
684 if (UNDEFINED_DURATION.equals(resolution)) {
685 return Optional.empty();
686 }
687 Duration minRacyThreshold = Duration.ofNanos(userConfig.getTimeUnit(
688 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
689 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
690 UNDEFINED_DURATION.toNanos(), TimeUnit.NANOSECONDS));
691 FileStoreAttributes c = new FileStoreAttributes(resolution);
692 if (!UNDEFINED_DURATION.equals(minRacyThreshold)) {
693 c.minimalRacyInterval = minRacyThreshold;
694 }
695 return Optional.of(c);
696 }
697
698 private static void saveToConfig(FileStore s,
699 FileStoreAttributes c) {
700 StoredConfig jgitConfig;
701 try {
702 jgitConfig = SystemReader.getInstance().getJGitConfig();
703 } catch (IOException | ConfigInvalidException e) {
704 LOG.error(JGitText.get().saveFileStoreAttributesFailed, e);
705 return;
706 }
707 long resolution = c.getFsTimestampResolution().toNanos();
708 TimeUnit resolutionUnit = getUnit(resolution);
709 long resolutionValue = resolutionUnit.convert(resolution,
710 TimeUnit.NANOSECONDS);
711
712 long minRacyThreshold = c.getMinimalRacyInterval().toNanos();
713 TimeUnit minRacyThresholdUnit = getUnit(minRacyThreshold);
714 long minRacyThresholdValue = minRacyThresholdUnit
715 .convert(minRacyThreshold, TimeUnit.NANOSECONDS);
716
717 final int max_retries = 5;
718 int retries = 0;
719 boolean succeeded = false;
720 String key = getConfigKey(s);
721 while (!succeeded && retries < max_retries) {
722 try {
723 jgitConfig.setString(
724 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
725 ConfigConstants.CONFIG_KEY_TIMESTAMP_RESOLUTION,
726 String.format("%d %s",
727 Long.valueOf(resolutionValue),
728 resolutionUnit.name().toLowerCase()));
729 jgitConfig.setString(
730 ConfigConstants.CONFIG_FILESYSTEM_SECTION, key,
731 ConfigConstants.CONFIG_KEY_MIN_RACY_THRESHOLD,
732 String.format("%d %s",
733 Long.valueOf(minRacyThresholdValue),
734 minRacyThresholdUnit.name().toLowerCase()));
735 jgitConfig.save();
736 succeeded = true;
737 } catch (LockFailedException e) {
738
739 try {
740 retries++;
741 if (retries < max_retries) {
742 Thread.sleep(100);
743 LOG.debug("locking {} failed, retries {}/{}",
744 jgitConfig, Integer.valueOf(retries),
745 Integer.valueOf(max_retries));
746 } else {
747 LOG.warn(MessageFormat.format(
748 JGitText.get().lockFailedRetry, jgitConfig,
749 Integer.valueOf(retries)));
750 }
751 } catch (InterruptedException e1) {
752 Thread.currentThread().interrupt();
753 break;
754 }
755 } catch (IOException e) {
756 LOG.error(MessageFormat.format(
757 JGitText.get().cannotSaveConfig, jgitConfig), e);
758 break;
759 }
760 }
761 }
762
763 private static String getConfigKey(FileStore s) {
764 final String storeKey;
765 if (SystemReader.getInstance().isWindows()) {
766 Object attribute = null;
767 try {
768 attribute = s.getAttribute("volume:vsn");
769 } catch (IOException ignored) {
770
771 }
772 if (attribute instanceof Integer) {
773 storeKey = attribute.toString();
774 } else {
775 storeKey = s.name();
776 }
777 } else {
778 storeKey = s.name();
779 }
780 return JAVA_VERSION_PREFIX + storeKey;
781 }
782
783 private static TimeUnit getUnit(long nanos) {
784 TimeUnit unit;
785 if (nanos < 200_000L) {
786 unit = TimeUnit.NANOSECONDS;
787 } else if (nanos < 200_000_000L) {
788 unit = TimeUnit.MICROSECONDS;
789 } else {
790 unit = TimeUnit.MILLISECONDS;
791 }
792 return unit;
793 }
794
795 private final @NonNull Duration fsTimestampResolution;
796
797 private Duration minimalRacyInterval;
798
799
800
801
802
803
804 public Duration getMinimalRacyInterval() {
805 return minimalRacyInterval;
806 }
807
808
809
810
811 @NonNull
812 public Duration getFsTimestampResolution() {
813 return fsTimestampResolution;
814 }
815
816
817
818
819
820
821
822 public FileStoreAttributes(
823 @NonNull Duration fsTimestampResolution) {
824 this.fsTimestampResolution = fsTimestampResolution;
825 this.minimalRacyInterval = Duration.ZERO;
826 }
827
828 @SuppressWarnings({ "nls", "boxing" })
829 @Override
830 public String toString() {
831 return String.format(
832 "FileStoreAttributes[fsTimestampResolution=%,d µs, "
833 + "minimalRacyInterval=%,d µs]",
834 fsTimestampResolution.toNanos() / 1000,
835 minimalRacyInterval.toNanos() / 1000);
836 }
837
838 }
839
840
841 public static final FS DETECTED = detect();
842
843 private static volatile FSFactory factory;
844
845
846
847
848
849
850 public static FS detect() {
851 return detect(null);
852 }
853
854
855
856
857
858
859
860
861
862
863
864 @Deprecated
865 public static void setAsyncFileStoreAttributes(boolean asynch) {
866 FileStoreAttributes.setBackground(asynch);
867 }
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889 public static FS detect(Boolean cygwinUsed) {
890 if (factory == null) {
891 factory = new FS.FSFactory();
892 }
893 return factory.detect(cygwinUsed);
894 }
895
896
897
898
899
900
901
902
903
904
905
906 public static FileStoreAttributes getFileStoreAttributes(
907 @NonNull Path dir) {
908 return FileStoreAttributes.get(dir);
909 }
910
911 private volatile Holder<File> userHome;
912
913 private volatile Holder<File> gitSystemConfig;
914
915
916
917
918 protected FS() {
919
920 }
921
922
923
924
925
926
927
928 protected FSme="FS" href="../../../../org/eclipse/jgit/util/FS.html#FS">FS(FS src) {
929 userHome = src.userHome;
930 gitSystemConfig = src.gitSystemConfig;
931 }
932
933
934
935
936
937
938 public abstract FS newInstance();
939
940
941
942
943
944
945
946 public abstract boolean supportsExecute();
947
948
949
950
951
952
953
954
955
956
957
958
959 public boolean supportsAtomicCreateNewFile() {
960 return true;
961 }
962
963
964
965
966
967
968
969
970 public boolean supportsSymlinks() {
971 if (supportSymlinks == null) {
972 detectSymlinkSupport();
973 }
974 return Boolean.TRUE.equals(supportSymlinks);
975 }
976
977 private void detectSymlinkSupport() {
978 File tempFile = null;
979 try {
980 tempFile = File.createTempFile("tempsymlinktarget", "");
981 File linkName = new File(tempFile.getParentFile(), "tempsymlink");
982 createSymLink(linkName, tempFile.getPath());
983 supportSymlinks = Boolean.TRUE;
984 linkName.delete();
985 } catch (IOException | UnsupportedOperationException | SecurityException
986 | InternalError e) {
987 supportSymlinks = Boolean.FALSE;
988 } finally {
989 if (tempFile != null) {
990 try {
991 FileUtils.delete(tempFile);
992 } catch (IOException e) {
993 LOG.error(JGitText.get().cannotDeleteFile, tempFile);
994 }
995 }
996 }
997 }
998
999
1000
1001
1002
1003
1004 public abstract boolean isCaseSensitive();
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020 public abstract boolean canExecute(File f);
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035 public abstract boolean setExecute(File f, boolean canExec);
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049 @Deprecated
1050 public long lastModified(File f) throws IOException {
1051 return FileUtils.lastModified(f);
1052 }
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064 public Instant lastModifiedInstant(Path p) {
1065 return FileUtils.lastModifiedInstant(p);
1066 }
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078 public Instant lastModifiedInstant(File f) {
1079 return FileUtils.lastModifiedInstant(f.toPath());
1080 }
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094 @Deprecated
1095 public void setLastModified(File f, long time) throws IOException {
1096 FileUtils.setLastModified(f, time);
1097 }
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110 public void setLastModified(Path p, Instant time) throws IOException {
1111 FileUtils.setLastModified(p, time);
1112 }
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124 public long length(File path) throws IOException {
1125 return FileUtils.getLength(path);
1126 }
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137 public void delete(File f) throws IOException {
1138 FileUtils.delete(f);
1139 }
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159 public File resolve(File dir, String name) {
1160 final File abspn = new File(name);
1161 if (abspn.isAbsolute())
1162 return abspn;
1163 return new File(dir, name);
1164 }
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177 public File userHome() {
1178 Holder<File> p = userHome;
1179 if (p == null) {
1180 p = new Holder<>(safeUserHomeImpl());
1181 userHome = p;
1182 }
1183 return p.value;
1184 }
1185
1186 private File safeUserHomeImpl() {
1187 File home;
1188 try {
1189 home = userHomeImpl();
1190 if (home != null) {
1191 home.toPath();
1192 return home;
1193 }
1194 } catch (RuntimeException e) {
1195 LOG.error(JGitText.get().exceptionWhileFindingUserHome, e);
1196 }
1197 home = defaultUserHomeImpl();
1198 if (home != null) {
1199 try {
1200 home.toPath();
1201 return home;
1202 } catch (InvalidPathException e) {
1203 LOG.error(MessageFormat
1204 .format(JGitText.get().invalidHomeDirectory, home), e);
1205 }
1206 }
1207 return null;
1208 }
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218 public FS setUserHome(File path) {
1219 userHome = new Holder<>(path);
1220 return this;
1221 }
1222
1223
1224
1225
1226
1227
1228 public abstract boolean retryFailedLockFileCommit();
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239 public BasicFileAttributes fileAttributes(File file) throws IOException {
1240 return FileUtils.fileAttributes(file);
1241 }
1242
1243
1244
1245
1246
1247
1248 protected File userHomeImpl() {
1249 return defaultUserHomeImpl();
1250 }
1251
1252 private File defaultUserHomeImpl() {
1253 final String home = AccessController.doPrivileged(
1254 (PrivilegedAction<String>) () -> System.getProperty("user.home")
1255 );
1256 if (home == null || home.length() == 0)
1257 return null;
1258 return new File(home).getAbsoluteFile();
1259 }
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272 protected static File searchPath(String path, String... lookFor) {
1273 if (path == null)
1274 return null;
1275
1276 for (String p : path.split(File.pathSeparator)) {
1277 for (String command : lookFor) {
1278 final File file = new File(p, command);
1279 try {
1280 if (file.isFile()) {
1281 return file.getAbsoluteFile();
1282 }
1283 } catch (SecurityException e) {
1284 LOG.warn(MessageFormat.format(
1285 JGitText.get().skipNotAccessiblePath,
1286 file.getPath()));
1287 }
1288 }
1289 }
1290 return null;
1291 }
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307 @Nullable
1308 protected static String readPipe(File dir, String[] command,
1309 String encoding) throws CommandFailedException {
1310 return readPipe(dir, command, encoding, null);
1311 }
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331 @Nullable
1332 protected static String readPipe(File dir, String[] command,
1333 String encoding, Map<String, String> env)
1334 throws CommandFailedException {
1335 final boolean debug = LOG.isDebugEnabled();
1336 try {
1337 if (debug) {
1338 LOG.debug("readpipe " + Arrays.asList(command) + ","
1339 + dir);
1340 }
1341 ProcessBuilder pb = new ProcessBuilder(command);
1342 pb.directory(dir);
1343 if (env != null) {
1344 pb.environment().putAll(env);
1345 }
1346 Process p;
1347 try {
1348 p = pb.start();
1349 } catch (IOException e) {
1350
1351 throw new CommandFailedException(-1, e.getMessage(), e);
1352 }
1353 p.getOutputStream().close();
1354 GobblerThread gobbler = new GobblerThread(p, command, dir);
1355 gobbler.start();
1356 String r = null;
1357 try (BufferedReader lineRead = new BufferedReader(
1358 new InputStreamReader(p.getInputStream(), encoding))) {
1359 r = lineRead.readLine();
1360 if (debug) {
1361 LOG.debug("readpipe may return '" + r + "'");
1362 LOG.debug("remaining output:\n");
1363 String l;
1364 while ((l = lineRead.readLine()) != null) {
1365 LOG.debug(l);
1366 }
1367 }
1368 }
1369
1370 for (;;) {
1371 try {
1372 int rc = p.waitFor();
1373 gobbler.join();
1374 if (rc == 0 && !gobbler.fail.get()) {
1375 return r;
1376 }
1377 if (debug) {
1378 LOG.debug("readpipe rc=" + rc);
1379 }
1380 throw new CommandFailedException(rc,
1381 gobbler.errorMessage.get(),
1382 gobbler.exception.get());
1383 } catch (InterruptedException ie) {
1384
1385 }
1386 }
1387 } catch (IOException e) {
1388 LOG.error("Caught exception in FS.readPipe()", e);
1389 } catch (AccessControlException e) {
1390 LOG.warn(MessageFormat.format(
1391 JGitText.get().readPipeIsNotAllowedRequiredPermission,
1392 command, dir, e.getPermission()));
1393 } catch (SecurityException e) {
1394 LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed,
1395 command, dir));
1396 }
1397 if (debug) {
1398 LOG.debug("readpipe returns null");
1399 }
1400 return null;
1401 }
1402
1403 private static class GobblerThread extends Thread {
1404
1405
1406 private static final int PROCESS_EXIT_TIMEOUT = 5;
1407
1408 private final Process p;
1409 private final String desc;
1410 private final String dir;
1411 final AtomicBoolean fail = new AtomicBoolean();
1412 final AtomicReference<String> errorMessage = new AtomicReference<>();
1413 final AtomicReference<Throwable> exception = new AtomicReference<>();
1414
1415 GobblerThread(Process p, String[] command, File dir) {
1416 this.p = p;
1417 this.desc = Arrays.toString(command);
1418 this.dir = Objects.toString(dir);
1419 }
1420
1421 @Override
1422 public void run() {
1423 StringBuilder err = new StringBuilder();
1424 try (InputStream is = p.getErrorStream()) {
1425 int ch;
1426 while ((ch = is.read()) != -1) {
1427 err.append((char) ch);
1428 }
1429 } catch (IOException e) {
1430 if (waitForProcessCompletion(e) && p.exitValue() != 0) {
1431 setError(e, e.getMessage(), p.exitValue());
1432 fail.set(true);
1433 } else {
1434
1435
1436 }
1437 } finally {
1438 if (waitForProcessCompletion(null) && err.length() > 0) {
1439 setError(null, err.toString(), p.exitValue());
1440 if (p.exitValue() != 0) {
1441 fail.set(true);
1442 }
1443 }
1444 }
1445 }
1446
1447 @SuppressWarnings("boxing")
1448 private boolean waitForProcessCompletion(IOException originalError) {
1449 try {
1450 if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) {
1451 setError(originalError, MessageFormat.format(
1452 JGitText.get().commandClosedStderrButDidntExit,
1453 desc, PROCESS_EXIT_TIMEOUT), -1);
1454 fail.set(true);
1455 return false;
1456 }
1457 } catch (InterruptedException e) {
1458 setError(originalError, MessageFormat.format(
1459 JGitText.get().threadInterruptedWhileRunning, desc), -1);
1460 fail.set(true);
1461 return false;
1462 }
1463 return true;
1464 }
1465
1466 private void setError(IOException e, String message, int exitCode) {
1467 exception.set(e);
1468 errorMessage.set(MessageFormat.format(
1469 JGitText.get().exceptionCaughtDuringExecutionOfCommand,
1470 desc, dir, Integer.valueOf(exitCode), message));
1471 }
1472 }
1473
1474
1475
1476
1477
1478
1479
1480
1481 protected abstract File discoverGitExe();
1482
1483
1484
1485
1486
1487
1488
1489
1490 protected File discoverGitSystemConfig() {
1491 File gitExe = discoverGitExe();
1492 if (gitExe == null) {
1493 return null;
1494 }
1495
1496
1497 String v;
1498 try {
1499 v = readPipe(gitExe.getParentFile(),
1500 new String[] { gitExe.getPath(), "--version" },
1501 Charset.defaultCharset().name());
1502 } catch (CommandFailedException e) {
1503 LOG.warn(e.getMessage());
1504 return null;
1505 }
1506 if (StringUtils.isEmptyOrNull(v)
1507 || (v != null && v.startsWith("jgit"))) {
1508 return null;
1509 }
1510
1511
1512
1513 Map<String, String> env = new HashMap<>();
1514 env.put("GIT_EDITOR", "echo");
1515
1516 String w;
1517 try {
1518 w = readPipe(gitExe.getParentFile(),
1519 new String[] { gitExe.getPath(), "config", "--system",
1520 "--edit" },
1521 Charset.defaultCharset().name(), env);
1522 } catch (CommandFailedException e) {
1523 LOG.warn(e.getMessage());
1524 return null;
1525 }
1526 if (StringUtils.isEmptyOrNull(w)) {
1527 return null;
1528 }
1529
1530 return new File(w);
1531 }
1532
1533
1534
1535
1536
1537
1538
1539
1540 public File getGitSystemConfig() {
1541 if (gitSystemConfig == null) {
1542 gitSystemConfig = new Holder<>(discoverGitSystemConfig());
1543 }
1544 return gitSystemConfig.value;
1545 }
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555 public FS setGitSystemConfig(File configFile) {
1556 gitSystemConfig = new Holder<>(configFile);
1557 return this;
1558 }
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569 protected static File resolveGrandparentFile(File grandchild) {
1570 if (grandchild != null) {
1571 File parent = grandchild.getParentFile();
1572 if (parent != null)
1573 return parent.getParentFile();
1574 }
1575 return null;
1576 }
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587 public String readSymLink(File path) throws IOException {
1588 return FileUtils.readSymLink(path);
1589 }
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600 public boolean isSymLink(File path) throws IOException {
1601 return FileUtils.isSymlink(path);
1602 }
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613 public boolean exists(File path) {
1614 return FileUtils.exists(path);
1615 }
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626 public boolean isDirectory(File path) {
1627 return FileUtils.isDirectory(path);
1628 }
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639 public boolean isFile(File path) {
1640 return FileUtils.isFile(path);
1641 }
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654 public boolean isHidden(File path) throws IOException {
1655 return FileUtils.isHidden(path);
1656 }
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668 public void setHidden(File path, boolean hidden) throws IOException {
1669 FileUtils.setHidden(path, hidden);
1670 }
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682 public void createSymLink(File path, String target) throws IOException {
1683 FileUtils.createSymLink(path, target);
1684 }
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699 @Deprecated
1700 public boolean createNewFile(File path) throws IOException {
1701 return path.createNewFile();
1702 }
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713 public static class LockToken implements Closeable {
1714 private boolean isCreated;
1715
1716 private Optional<Path> link;
1717
1718 LockToken(boolean isCreated, Optional<Path> link) {
1719 this.isCreated = isCreated;
1720 this.link = link;
1721 }
1722
1723
1724
1725
1726 public boolean isCreated() {
1727 return isCreated;
1728 }
1729
1730 @Override
1731 public void close() {
1732 if (!link.isPresent()) {
1733 return;
1734 }
1735 Path p = link.get();
1736 if (!Files.exists(p)) {
1737 return;
1738 }
1739 try {
1740 Files.delete(p);
1741 } catch (IOException e) {
1742 LOG.error(MessageFormat
1743 .format(JGitText.get().closeLockTokenFailed, this), e);
1744 }
1745 }
1746
1747 @Override
1748 public String toString() {
1749 return "LockToken [lockCreated=" + isCreated +
1750 ", link="
1751 + (link.isPresent() ? link.get().getFileName() + "]"
1752 : "<null>]");
1753 }
1754 }
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768 public LockToken createNewFileAtomic(File path) throws IOException {
1769 return new LockToken(path.createNewFile(), Optional.empty());
1770 }
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786 public String relativize(String base, String other) {
1787 return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive());
1788 }
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801 public Entry[] list(File directory, FileModeStrategy fileModeStrategy) {
1802 final File[] all = directory.listFiles();
1803 if (all == null) {
1804 return NO_ENTRIES;
1805 }
1806 final Entry[] result = new Entry[all.length];
1807 for (int i = 0; i < result.length; i++) {
1808 result[i] = new FileEntry(all[i], this, fileModeStrategy);
1809 }
1810 return result;
1811 }
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835 public ProcessResult runHookIfPresent(Repository repository,
1836 final String hookName,
1837 String[] args) throws JGitInternalException {
1838 return runHookIfPresent(repository, hookName, args, System.out, System.err,
1839 null);
1840 }
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870 public ProcessResult runHookIfPresent(Repository repository,
1871 final String hookName,
1872 String[] args, PrintStream outRedirect, PrintStream errRedirect,
1873 String stdinArgs) throws JGitInternalException {
1874 return new ProcessResult(Status.NOT_SUPPORTED);
1875 }
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906 protected ProcessResult internalRunHookIfPresent(Repository repository,
1907 final String hookName, String[] args, PrintStream outRedirect,
1908 PrintStream errRedirect, String stdinArgs)
1909 throws JGitInternalException {
1910 File hookFile = findHook(repository, hookName);
1911 if (hookFile == null || hookName == null) {
1912 return new ProcessResult(Status.NOT_PRESENT);
1913 }
1914
1915 File runDirectory = getRunDirectory(repository, hookName);
1916 if (runDirectory == null) {
1917 return new ProcessResult(Status.NOT_PRESENT);
1918 }
1919 String cmd = hookFile.getAbsolutePath();
1920 ProcessBuilder hookProcess = runInShell(shellQuote(cmd), args);
1921 hookProcess.directory(runDirectory.getAbsoluteFile());
1922 Map<String, String> environment = hookProcess.environment();
1923 environment.put(Constants.GIT_DIR_KEY,
1924 repository.getDirectory().getAbsolutePath());
1925 if (!repository.isBare()) {
1926 environment.put(Constants.GIT_WORK_TREE_KEY,
1927 repository.getWorkTree().getAbsolutePath());
1928 }
1929 try {
1930 return new ProcessResult(runProcess(hookProcess, outRedirect,
1931 errRedirect, stdinArgs), Status.OK);
1932 } catch (IOException e) {
1933 throw new JGitInternalException(MessageFormat.format(
1934 JGitText.get().exceptionCaughtDuringExecutionOfHook,
1935 hookName), e);
1936 } catch (InterruptedException e) {
1937 throw new JGitInternalException(MessageFormat.format(
1938 JGitText.get().exceptionHookExecutionInterrupted,
1939 hookName), e);
1940 }
1941 }
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955 String shellQuote(String cmd) {
1956 return cmd;
1957 }
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970 public File findHook(Repository repository, String hookName) {
1971 if (hookName == null) {
1972 return null;
1973 }
1974 File hookDir = getHooksDirectory(repository);
1975 if (hookDir == null) {
1976 return null;
1977 }
1978 File hookFile = new File(hookDir, hookName);
1979 if (hookFile.isAbsolute()) {
1980 if (!hookFile.exists() || (FS.DETECTED.supportsExecute()
1981 && !FS.DETECTED.canExecute(hookFile))) {
1982 return null;
1983 }
1984 } else {
1985 try {
1986 File runDirectory = getRunDirectory(repository, hookName);
1987 if (runDirectory == null) {
1988 return null;
1989 }
1990 Path hookPath = runDirectory.getAbsoluteFile().toPath()
1991 .resolve(hookFile.toPath());
1992 FS fs = repository.getFS();
1993 if (fs == null) {
1994 fs = FS.DETECTED;
1995 }
1996 if (!Files.exists(hookPath) || (fs.supportsExecute()
1997 && !fs.canExecute(hookPath.toFile()))) {
1998 return null;
1999 }
2000 hookFile = hookPath.toFile();
2001 } catch (InvalidPathException e) {
2002 LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath,
2003 hookFile));
2004 return null;
2005 }
2006 }
2007 return hookFile;
2008 }
2009
2010 private File getRunDirectory(Repository repository,
2011 @NonNull String hookName) {
2012 if (repository.isBare()) {
2013 return repository.getDirectory();
2014 }
2015 switch (hookName) {
2016 case "pre-receive":
2017 case "update":
2018 case "post-receive":
2019 case "post-update":
2020 case "push-to-checkout":
2021 return repository.getDirectory();
2022 default:
2023 return repository.getWorkTree();
2024 }
2025 }
2026
2027 private File getHooksDirectory(Repository repository) {
2028 Config config = repository.getConfig();
2029 String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION,
2030 null, ConfigConstants.CONFIG_KEY_HOOKS_PATH);
2031 if (hooksDir != null) {
2032 return new File(hooksDir);
2033 }
2034 File dir = repository.getDirectory();
2035 return dir == null ? null : new File(dir, Constants.HOOKS);
2036 }
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063 public int runProcess(ProcessBuilder processBuilder,
2064 OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
2065 throws IOException, InterruptedException {
2066 InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(
2067 stdinArgs.getBytes(UTF_8));
2068 return runProcess(processBuilder, outRedirect, errRedirect, in);
2069 }
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099 public int runProcess(ProcessBuilder processBuilder,
2100 OutputStream outRedirect, OutputStream errRedirect,
2101 InputStream inRedirect) throws IOException,
2102 InterruptedException {
2103 final ExecutorService executor = Executors.newFixedThreadPool(2);
2104 Process process = null;
2105
2106
2107 IOException ioException = null;
2108 try {
2109 process = processBuilder.start();
2110 executor.execute(
2111 new StreamGobbler(process.getErrorStream(), errRedirect));
2112 executor.execute(
2113 new StreamGobbler(process.getInputStream(), outRedirect));
2114 @SuppressWarnings("resource")
2115 OutputStream outputStream = process.getOutputStream();
2116 try {
2117 if (inRedirect != null) {
2118 new StreamGobbler(inRedirect, outputStream).copy();
2119 }
2120 } finally {
2121 try {
2122 outputStream.close();
2123 } catch (IOException e) {
2124
2125
2126
2127
2128
2129
2130 }
2131 }
2132 return process.waitFor();
2133 } catch (IOException e) {
2134 ioException = e;
2135 } finally {
2136 shutdownAndAwaitTermination(executor);
2137 if (process != null) {
2138 try {
2139 process.waitFor();
2140 } catch (InterruptedException e) {
2141
2142
2143
2144
2145 Thread.interrupted();
2146 }
2147
2148
2149
2150 if (inRedirect != null) {
2151 inRedirect.close();
2152 }
2153 try {
2154 process.getErrorStream().close();
2155 } catch (IOException e) {
2156 ioException = ioException != null ? ioException : e;
2157 }
2158 try {
2159 process.getInputStream().close();
2160 } catch (IOException e) {
2161 ioException = ioException != null ? ioException : e;
2162 }
2163 try {
2164 process.getOutputStream().close();
2165 } catch (IOException e) {
2166 ioException = ioException != null ? ioException : e;
2167 }
2168 process.destroy();
2169 }
2170 }
2171
2172 throw ioException;
2173 }
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188 private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
2189 boolean hasShutdown = true;
2190 pool.shutdown();
2191 try {
2192
2193 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
2194 pool.shutdownNow();
2195
2196 if (!pool.awaitTermination(60, TimeUnit.SECONDS))
2197 hasShutdown = false;
2198 }
2199 } catch (InterruptedException ie) {
2200
2201 pool.shutdownNow();
2202
2203 Thread.currentThread().interrupt();
2204 hasShutdown = false;
2205 }
2206 return hasShutdown;
2207 }
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221 public abstract ProcessBuilder runInShell(String cmd, String[] args);
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235 public ExecutionResult execute(ProcessBuilder pb, InputStream in)
2236 throws IOException, InterruptedException {
2237 try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null);
2238 TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024,
2239 1024 * 1024)) {
2240 int rc = runProcess(pb, stdout, stderr, in);
2241 return new ExecutionResult(stdout, stderr, rc);
2242 }
2243 }
2244
2245 private static class Holder<V> {
2246 final V value;
2247
2248 Holder(V value) {
2249 this.value = value;
2250 }
2251 }
2252
2253
2254
2255
2256
2257
2258 public static class Attributes {
2259
2260
2261
2262
2263 public boolean isDirectory() {
2264 return isDirectory;
2265 }
2266
2267
2268
2269
2270 public boolean isExecutable() {
2271 return isExecutable;
2272 }
2273
2274
2275
2276
2277 public boolean isSymbolicLink() {
2278 return isSymbolicLink;
2279 }
2280
2281
2282
2283
2284 public boolean isRegularFile() {
2285 return isRegularFile;
2286 }
2287
2288
2289
2290
2291 public long getCreationTime() {
2292 return creationTime;
2293 }
2294
2295
2296
2297
2298
2299
2300 @Deprecated
2301 public long getLastModifiedTime() {
2302 return lastModifiedInstant.toEpochMilli();
2303 }
2304
2305
2306
2307
2308
2309 public Instant getLastModifiedInstant() {
2310 return lastModifiedInstant;
2311 }
2312
2313 private final boolean isDirectory;
2314
2315 private final boolean isSymbolicLink;
2316
2317 private final boolean isRegularFile;
2318
2319 private final long creationTime;
2320
2321 private final Instant lastModifiedInstant;
2322
2323 private final boolean isExecutable;
2324
2325 private final File file;
2326
2327 private final boolean exists;
2328
2329
2330
2331
2332 protected long length = -1;
2333
2334 final FS fs;
2335
2336 Attributes(FS fs, File file, boolean exists, boolean isDirectory,
2337 boolean isExecutable, boolean isSymbolicLink,
2338 boolean isRegularFile, long creationTime,
2339 Instant lastModifiedInstant, long length) {
2340 this.fs = fs;
2341 this.file = file;
2342 this.exists = exists;
2343 this.isDirectory = isDirectory;
2344 this.isExecutable = isExecutable;
2345 this.isSymbolicLink = isSymbolicLink;
2346 this.isRegularFile = isRegularFile;
2347 this.creationTime = creationTime;
2348 this.lastModifiedInstant = lastModifiedInstant;
2349 this.length = length;
2350 }
2351
2352
2353
2354
2355
2356
2357
2358
2359 public Attributes(File path, FS fs) {
2360 this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
2361 }
2362
2363
2364
2365
2366 public long getLength() {
2367 if (length == -1)
2368 return length = file.length();
2369 return length;
2370 }
2371
2372
2373
2374
2375 public String getName() {
2376 return file.getName();
2377 }
2378
2379
2380
2381
2382 public File getFile() {
2383 return file;
2384 }
2385
2386 boolean exists() {
2387 return exists;
2388 }
2389 }
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399 public Attributes getAttributes(File path) {
2400 boolean isDirectory = isDirectory(path);
2401 boolean isFile = !isDirectory && path.isFile();
2402 assert path.exists() == isDirectory || isFile;
2403 boolean exists = isDirectory || isFile;
2404 boolean canExecute = exists && !isDirectory && canExecute(path);
2405 boolean isSymlink = false;
2406 Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
2407 long createTime = 0L;
2408 return new Attributes(this, path, exists, isDirectory, canExecute,
2409 isSymlink, isFile, createTime, lastModified, -1);
2410 }
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420 public File normalize(File file) {
2421 return file;
2422 }
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432 public String normalize(String name) {
2433 return name;
2434 }
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448 private static class StreamGobbler implements Runnable {
2449 private InputStream in;
2450
2451 private OutputStream out;
2452
2453 public StreamGobbler(InputStream stream, OutputStream output) {
2454 this.in = stream;
2455 this.out = output;
2456 }
2457
2458 @Override
2459 public void run() {
2460 try {
2461 copy();
2462 } catch (IOException e) {
2463
2464 }
2465 }
2466
2467 void copy() throws IOException {
2468 boolean writeFailure = false;
2469 byte[] buffer = new byte[4096];
2470 int readBytes;
2471 while ((readBytes = in.read(buffer)) != -1) {
2472
2473
2474
2475 if (!writeFailure && out != null) {
2476 try {
2477 out.write(buffer, 0, readBytes);
2478 out.flush();
2479 } catch (IOException e) {
2480 writeFailure = true;
2481 }
2482 }
2483 }
2484 }
2485 }
2486 }