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