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
1095 @Deprecated
1096 public void setLastModified(File f, long time) throws IOException {
1097 FileUtils.setLastModified(f, time);
1098 }
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112 public void setLastModified(Path p, Instant time) throws IOException {
1113 FileUtils.setLastModified(p, time);
1114 }
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126 public long length(File path) throws IOException {
1127 return FileUtils.getLength(path);
1128 }
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139 public void delete(File f) throws IOException {
1140 FileUtils.delete(f);
1141 }
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161 public File resolve(File dir, String name) {
1162 final File abspn = new File(name);
1163 if (abspn.isAbsolute())
1164 return abspn;
1165 return new File(dir, name);
1166 }
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179 public File userHome() {
1180 Holder<File> p = userHome;
1181 if (p == null) {
1182 p = new Holder<>(safeUserHomeImpl());
1183 userHome = p;
1184 }
1185 return p.value;
1186 }
1187
1188 private File safeUserHomeImpl() {
1189 File home;
1190 try {
1191 home = userHomeImpl();
1192 if (home != null) {
1193 home.toPath();
1194 return home;
1195 }
1196 } catch (RuntimeException e) {
1197 LOG.error(JGitText.get().exceptionWhileFindingUserHome, e);
1198 }
1199 home = defaultUserHomeImpl();
1200 if (home != null) {
1201 try {
1202 home.toPath();
1203 return home;
1204 } catch (InvalidPathException e) {
1205 LOG.error(MessageFormat
1206 .format(JGitText.get().invalidHomeDirectory, home), e);
1207 }
1208 }
1209 return null;
1210 }
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220 public FS setUserHome(File path) {
1221 userHome = new Holder<>(path);
1222 return this;
1223 }
1224
1225
1226
1227
1228
1229
1230 public abstract boolean retryFailedLockFileCommit();
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241 public BasicFileAttributes fileAttributes(File file) throws IOException {
1242 return FileUtils.fileAttributes(file);
1243 }
1244
1245
1246
1247
1248
1249
1250 protected File userHomeImpl() {
1251 return defaultUserHomeImpl();
1252 }
1253
1254 private File defaultUserHomeImpl() {
1255 final String home = AccessController.doPrivileged(
1256 (PrivilegedAction<String>) () -> System.getProperty("user.home")
1257 );
1258 if (home == null || home.length() == 0)
1259 return null;
1260 return new File(home).getAbsoluteFile();
1261 }
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274 protected static File searchPath(String path, String... lookFor) {
1275 if (path == null)
1276 return null;
1277
1278 for (String p : path.split(File.pathSeparator)) {
1279 for (String command : lookFor) {
1280 final File file = new File(p, command);
1281 try {
1282 if (file.isFile()) {
1283 return file.getAbsoluteFile();
1284 }
1285 } catch (SecurityException e) {
1286 LOG.warn(MessageFormat.format(
1287 JGitText.get().skipNotAccessiblePath,
1288 file.getPath()));
1289 }
1290 }
1291 }
1292 return null;
1293 }
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309 @Nullable
1310 protected static String readPipe(File dir, String[] command,
1311 String encoding) throws CommandFailedException {
1312 return readPipe(dir, command, encoding, null);
1313 }
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333 @Nullable
1334 protected static String readPipe(File dir, String[] command,
1335 String encoding, Map<String, String> env)
1336 throws CommandFailedException {
1337 final boolean debug = LOG.isDebugEnabled();
1338 try {
1339 if (debug) {
1340 LOG.debug("readpipe " + Arrays.asList(command) + ","
1341 + dir);
1342 }
1343 ProcessBuilder pb = new ProcessBuilder(command);
1344 pb.directory(dir);
1345 if (env != null) {
1346 pb.environment().putAll(env);
1347 }
1348 Process p;
1349 try {
1350 p = pb.start();
1351 } catch (IOException e) {
1352
1353 throw new CommandFailedException(-1, e.getMessage(), e);
1354 }
1355 p.getOutputStream().close();
1356 GobblerThread gobbler = new GobblerThread(p, command, dir);
1357 gobbler.start();
1358 String r = null;
1359 try (BufferedReader lineRead = new BufferedReader(
1360 new InputStreamReader(p.getInputStream(), encoding))) {
1361 r = lineRead.readLine();
1362 if (debug) {
1363 LOG.debug("readpipe may return '" + r + "'");
1364 LOG.debug("remaining output:\n");
1365 String l;
1366 while ((l = lineRead.readLine()) != null) {
1367 LOG.debug(l);
1368 }
1369 }
1370 }
1371
1372 for (;;) {
1373 try {
1374 int rc = p.waitFor();
1375 gobbler.join();
1376 if (rc == 0 && !gobbler.fail.get()) {
1377 return r;
1378 }
1379 if (debug) {
1380 LOG.debug("readpipe rc=" + rc);
1381 }
1382 throw new CommandFailedException(rc,
1383 gobbler.errorMessage.get(),
1384 gobbler.exception.get());
1385 } catch (InterruptedException ie) {
1386
1387 }
1388 }
1389 } catch (IOException e) {
1390 LOG.error("Caught exception in FS.readPipe()", e);
1391 } catch (AccessControlException e) {
1392 LOG.warn(MessageFormat.format(
1393 JGitText.get().readPipeIsNotAllowedRequiredPermission,
1394 command, dir, e.getPermission()));
1395 } catch (SecurityException e) {
1396 LOG.warn(MessageFormat.format(JGitText.get().readPipeIsNotAllowed,
1397 command, dir));
1398 }
1399 if (debug) {
1400 LOG.debug("readpipe returns null");
1401 }
1402 return null;
1403 }
1404
1405 private static class GobblerThread extends Thread {
1406
1407
1408 private static final int PROCESS_EXIT_TIMEOUT = 5;
1409
1410 private final Process p;
1411 private final String desc;
1412 private final String dir;
1413 final AtomicBoolean fail = new AtomicBoolean();
1414 final AtomicReference<String> errorMessage = new AtomicReference<>();
1415 final AtomicReference<Throwable> exception = new AtomicReference<>();
1416
1417 GobblerThread(Process p, String[] command, File dir) {
1418 this.p = p;
1419 this.desc = Arrays.toString(command);
1420 this.dir = Objects.toString(dir);
1421 }
1422
1423 @Override
1424 public void run() {
1425 StringBuilder err = new StringBuilder();
1426 try (InputStream is = p.getErrorStream()) {
1427 int ch;
1428 while ((ch = is.read()) != -1) {
1429 err.append((char) ch);
1430 }
1431 } catch (IOException e) {
1432 if (waitForProcessCompletion(e) && p.exitValue() != 0) {
1433 setError(e, e.getMessage(), p.exitValue());
1434 fail.set(true);
1435 } else {
1436
1437
1438 }
1439 } finally {
1440 if (waitForProcessCompletion(null) && err.length() > 0) {
1441 setError(null, err.toString(), p.exitValue());
1442 if (p.exitValue() != 0) {
1443 fail.set(true);
1444 }
1445 }
1446 }
1447 }
1448
1449 @SuppressWarnings("boxing")
1450 private boolean waitForProcessCompletion(IOException originalError) {
1451 try {
1452 if (!p.waitFor(PROCESS_EXIT_TIMEOUT, TimeUnit.SECONDS)) {
1453 setError(originalError, MessageFormat.format(
1454 JGitText.get().commandClosedStderrButDidntExit,
1455 desc, PROCESS_EXIT_TIMEOUT), -1);
1456 fail.set(true);
1457 return false;
1458 }
1459 } catch (InterruptedException e) {
1460 setError(originalError, MessageFormat.format(
1461 JGitText.get().threadInterruptedWhileRunning, desc), -1);
1462 fail.set(true);
1463 return false;
1464 }
1465 return true;
1466 }
1467
1468 private void setError(IOException e, String message, int exitCode) {
1469 exception.set(e);
1470 errorMessage.set(MessageFormat.format(
1471 JGitText.get().exceptionCaughtDuringExecutionOfCommand,
1472 desc, dir, Integer.valueOf(exitCode), message));
1473 }
1474 }
1475
1476
1477
1478
1479
1480
1481
1482
1483 protected abstract File discoverGitExe();
1484
1485
1486
1487
1488
1489
1490
1491
1492 protected File discoverGitSystemConfig() {
1493 File gitExe = discoverGitExe();
1494 if (gitExe == null) {
1495 return null;
1496 }
1497
1498
1499 String v;
1500 try {
1501 v = readPipe(gitExe.getParentFile(),
1502 new String[] { gitExe.getPath(), "--version" },
1503 Charset.defaultCharset().name());
1504 } catch (CommandFailedException e) {
1505 LOG.warn(e.getMessage());
1506 return null;
1507 }
1508 if (StringUtils.isEmptyOrNull(v)
1509 || (v != null && v.startsWith("jgit"))) {
1510 return null;
1511 }
1512
1513
1514
1515 Map<String, String> env = new HashMap<>();
1516 env.put("GIT_EDITOR", "echo");
1517
1518 String w;
1519 try {
1520 w = readPipe(gitExe.getParentFile(),
1521 new String[] { gitExe.getPath(), "config", "--system",
1522 "--edit" },
1523 Charset.defaultCharset().name(), env);
1524 } catch (CommandFailedException e) {
1525 LOG.warn(e.getMessage());
1526 return null;
1527 }
1528 if (StringUtils.isEmptyOrNull(w)) {
1529 return null;
1530 }
1531
1532 return new File(w);
1533 }
1534
1535
1536
1537
1538
1539
1540
1541
1542 public File getGitSystemConfig() {
1543 if (gitSystemConfig == null) {
1544 gitSystemConfig = new Holder<>(discoverGitSystemConfig());
1545 }
1546 return gitSystemConfig.value;
1547 }
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557 public FS setGitSystemConfig(File configFile) {
1558 gitSystemConfig = new Holder<>(configFile);
1559 return this;
1560 }
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571 protected static File resolveGrandparentFile(File grandchild) {
1572 if (grandchild != null) {
1573 File parent = grandchild.getParentFile();
1574 if (parent != null)
1575 return parent.getParentFile();
1576 }
1577 return null;
1578 }
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589 public String readSymLink(File path) throws IOException {
1590 return FileUtils.readSymLink(path);
1591 }
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602 public boolean isSymLink(File path) throws IOException {
1603 return FileUtils.isSymlink(path);
1604 }
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615 public boolean exists(File path) {
1616 return FileUtils.exists(path);
1617 }
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628 public boolean isDirectory(File path) {
1629 return FileUtils.isDirectory(path);
1630 }
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641 public boolean isFile(File path) {
1642 return FileUtils.isFile(path);
1643 }
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656 public boolean isHidden(File path) throws IOException {
1657 return FileUtils.isHidden(path);
1658 }
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670 public void setHidden(File path, boolean hidden) throws IOException {
1671 FileUtils.setHidden(path, hidden);
1672 }
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684 public void createSymLink(File path, String target) throws IOException {
1685 FileUtils.createSymLink(path, target);
1686 }
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701 @Deprecated
1702 public boolean createNewFile(File path) throws IOException {
1703 return path.createNewFile();
1704 }
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715 public static class LockToken implements Closeable {
1716 private boolean isCreated;
1717
1718 private Optional<Path> link;
1719
1720 LockToken(boolean isCreated, Optional<Path> link) {
1721 this.isCreated = isCreated;
1722 this.link = link;
1723 }
1724
1725
1726
1727
1728 public boolean isCreated() {
1729 return isCreated;
1730 }
1731
1732 @Override
1733 public void close() {
1734 if (!link.isPresent()) {
1735 return;
1736 }
1737 Path p = link.get();
1738 if (!Files.exists(p)) {
1739 return;
1740 }
1741 try {
1742 Files.delete(p);
1743 } catch (IOException e) {
1744 LOG.error(MessageFormat
1745 .format(JGitText.get().closeLockTokenFailed, this), e);
1746 }
1747 }
1748
1749 @Override
1750 public String toString() {
1751 return "LockToken [lockCreated=" + isCreated +
1752 ", link="
1753 + (link.isPresent() ? link.get().getFileName() + "]"
1754 : "<null>]");
1755 }
1756 }
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770 public LockToken createNewFileAtomic(File path) throws IOException {
1771 return new LockToken(path.createNewFile(), Optional.empty());
1772 }
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788 public String relativize(String base, String other) {
1789 return FileUtils.relativizePath(base, other, File.separator, this.isCaseSensitive());
1790 }
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803 public Entry[] list(File directory, FileModeStrategy fileModeStrategy) {
1804 final File[] all = directory.listFiles();
1805 if (all == null) {
1806 return NO_ENTRIES;
1807 }
1808 final Entry[] result = new Entry[all.length];
1809 for (int i = 0; i < result.length; i++) {
1810 result[i] = new FileEntry(all[i], this, fileModeStrategy);
1811 }
1812 return result;
1813 }
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837 public ProcessResult runHookIfPresent(Repository repository,
1838 final String hookName,
1839 String[] args) throws JGitInternalException {
1840 return runHookIfPresent(repository, hookName, args, System.out, System.err,
1841 null);
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
1871
1872 public ProcessResult runHookIfPresent(Repository repository,
1873 final String hookName,
1874 String[] args, PrintStream outRedirect, PrintStream errRedirect,
1875 String stdinArgs) throws JGitInternalException {
1876 return new ProcessResult(Status.NOT_SUPPORTED);
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
1907
1908 protected ProcessResult internalRunHookIfPresent(Repository repository,
1909 final String hookName, String[] args, PrintStream outRedirect,
1910 PrintStream errRedirect, String stdinArgs)
1911 throws JGitInternalException {
1912 File hookFile = findHook(repository, hookName);
1913 if (hookFile == null || hookName == null) {
1914 return new ProcessResult(Status.NOT_PRESENT);
1915 }
1916
1917 File runDirectory = getRunDirectory(repository, hookName);
1918 if (runDirectory == null) {
1919 return new ProcessResult(Status.NOT_PRESENT);
1920 }
1921 String cmd = hookFile.getAbsolutePath();
1922 ProcessBuilder hookProcess = runInShell(shellQuote(cmd), args);
1923 hookProcess.directory(runDirectory.getAbsoluteFile());
1924 Map<String, String> environment = hookProcess.environment();
1925 environment.put(Constants.GIT_DIR_KEY,
1926 repository.getDirectory().getAbsolutePath());
1927 if (!repository.isBare()) {
1928 environment.put(Constants.GIT_WORK_TREE_KEY,
1929 repository.getWorkTree().getAbsolutePath());
1930 }
1931 try {
1932 return new ProcessResult(runProcess(hookProcess, outRedirect,
1933 errRedirect, stdinArgs), Status.OK);
1934 } catch (IOException e) {
1935 throw new JGitInternalException(MessageFormat.format(
1936 JGitText.get().exceptionCaughtDuringExecutionOfHook,
1937 hookName), e);
1938 } catch (InterruptedException e) {
1939 throw new JGitInternalException(MessageFormat.format(
1940 JGitText.get().exceptionHookExecutionInterrupted,
1941 hookName), e);
1942 }
1943 }
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957 String shellQuote(String cmd) {
1958 return cmd;
1959 }
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972 public File findHook(Repository repository, String hookName) {
1973 if (hookName == null) {
1974 return null;
1975 }
1976 File hookDir = getHooksDirectory(repository);
1977 if (hookDir == null) {
1978 return null;
1979 }
1980 File hookFile = new File(hookDir, hookName);
1981 if (hookFile.isAbsolute()) {
1982 if (!hookFile.exists() || (FS.DETECTED.supportsExecute()
1983 && !FS.DETECTED.canExecute(hookFile))) {
1984 return null;
1985 }
1986 } else {
1987 try {
1988 File runDirectory = getRunDirectory(repository, hookName);
1989 if (runDirectory == null) {
1990 return null;
1991 }
1992 Path hookPath = runDirectory.getAbsoluteFile().toPath()
1993 .resolve(hookFile.toPath());
1994 FS fs = repository.getFS();
1995 if (fs == null) {
1996 fs = FS.DETECTED;
1997 }
1998 if (!Files.exists(hookPath) || (fs.supportsExecute()
1999 && !fs.canExecute(hookPath.toFile()))) {
2000 return null;
2001 }
2002 hookFile = hookPath.toFile();
2003 } catch (InvalidPathException e) {
2004 LOG.warn(MessageFormat.format(JGitText.get().invalidHooksPath,
2005 hookFile));
2006 return null;
2007 }
2008 }
2009 return hookFile;
2010 }
2011
2012 private File getRunDirectory(Repository repository,
2013 @NonNull String hookName) {
2014 if (repository.isBare()) {
2015 return repository.getDirectory();
2016 }
2017 switch (hookName) {
2018 case "pre-receive":
2019 case "update":
2020 case "post-receive":
2021 case "post-update":
2022 case "push-to-checkout":
2023 return repository.getDirectory();
2024 default:
2025 return repository.getWorkTree();
2026 }
2027 }
2028
2029 private File getHooksDirectory(Repository repository) {
2030 Config config = repository.getConfig();
2031 String hooksDir = config.getString(ConfigConstants.CONFIG_CORE_SECTION,
2032 null, ConfigConstants.CONFIG_KEY_HOOKS_PATH);
2033 if (hooksDir != null) {
2034 return new File(hooksDir);
2035 }
2036 File dir = repository.getDirectory();
2037 return dir == null ? null : new File(dir, Constants.HOOKS);
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
2064
2065 public int runProcess(ProcessBuilder processBuilder,
2066 OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
2067 throws IOException, InterruptedException {
2068 InputStream in = (stdinArgs == null) ? null : new ByteArrayInputStream(
2069 stdinArgs.getBytes(UTF_8));
2070 return runProcess(processBuilder, outRedirect, errRedirect, in);
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
2100
2101 public int runProcess(ProcessBuilder processBuilder,
2102 OutputStream outRedirect, OutputStream errRedirect,
2103 InputStream inRedirect) throws IOException,
2104 InterruptedException {
2105 final ExecutorService executor = Executors.newFixedThreadPool(2);
2106 Process process = null;
2107
2108
2109 IOException ioException = null;
2110 try {
2111 process = processBuilder.start();
2112 executor.execute(
2113 new StreamGobbler(process.getErrorStream(), errRedirect));
2114 executor.execute(
2115 new StreamGobbler(process.getInputStream(), outRedirect));
2116 @SuppressWarnings("resource")
2117 OutputStream outputStream = process.getOutputStream();
2118 try {
2119 if (inRedirect != null) {
2120 new StreamGobbler(inRedirect, outputStream).copy();
2121 }
2122 } finally {
2123 try {
2124 outputStream.close();
2125 } catch (IOException e) {
2126
2127
2128
2129
2130
2131
2132 }
2133 }
2134 return process.waitFor();
2135 } catch (IOException e) {
2136 ioException = e;
2137 } finally {
2138 shutdownAndAwaitTermination(executor);
2139 if (process != null) {
2140 try {
2141 process.waitFor();
2142 } catch (InterruptedException e) {
2143
2144
2145
2146
2147 Thread.interrupted();
2148 }
2149
2150
2151
2152 if (inRedirect != null) {
2153 inRedirect.close();
2154 }
2155 try {
2156 process.getErrorStream().close();
2157 } catch (IOException e) {
2158 ioException = ioException != null ? ioException : e;
2159 }
2160 try {
2161 process.getInputStream().close();
2162 } catch (IOException e) {
2163 ioException = ioException != null ? ioException : e;
2164 }
2165 try {
2166 process.getOutputStream().close();
2167 } catch (IOException e) {
2168 ioException = ioException != null ? ioException : e;
2169 }
2170 process.destroy();
2171 }
2172 }
2173
2174 throw ioException;
2175 }
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190 private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
2191 boolean hasShutdown = true;
2192 pool.shutdown();
2193 try {
2194
2195 if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
2196 pool.shutdownNow();
2197
2198 if (!pool.awaitTermination(60, TimeUnit.SECONDS))
2199 hasShutdown = false;
2200 }
2201 } catch (InterruptedException ie) {
2202
2203 pool.shutdownNow();
2204
2205 Thread.currentThread().interrupt();
2206 hasShutdown = false;
2207 }
2208 return hasShutdown;
2209 }
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223 public abstract ProcessBuilder runInShell(String cmd, String[] args);
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237 public ExecutionResult execute(ProcessBuilder pb, InputStream in)
2238 throws IOException, InterruptedException {
2239 try (TemporaryBuffer stdout = new TemporaryBuffer.LocalFile(null);
2240 TemporaryBuffer stderr = new TemporaryBuffer.Heap(1024,
2241 1024 * 1024)) {
2242 int rc = runProcess(pb, stdout, stderr, in);
2243 return new ExecutionResult(stdout, stderr, rc);
2244 }
2245 }
2246
2247 private static class Holder<V> {
2248 final V value;
2249
2250 Holder(V value) {
2251 this.value = value;
2252 }
2253 }
2254
2255
2256
2257
2258
2259
2260 public static class Attributes {
2261
2262
2263
2264
2265 public boolean isDirectory() {
2266 return isDirectory;
2267 }
2268
2269
2270
2271
2272 public boolean isExecutable() {
2273 return isExecutable;
2274 }
2275
2276
2277
2278
2279 public boolean isSymbolicLink() {
2280 return isSymbolicLink;
2281 }
2282
2283
2284
2285
2286 public boolean isRegularFile() {
2287 return isRegularFile;
2288 }
2289
2290
2291
2292
2293 public long getCreationTime() {
2294 return creationTime;
2295 }
2296
2297
2298
2299
2300
2301
2302 @Deprecated
2303 public long getLastModifiedTime() {
2304 return lastModifiedInstant.toEpochMilli();
2305 }
2306
2307
2308
2309
2310
2311 public Instant getLastModifiedInstant() {
2312 return lastModifiedInstant;
2313 }
2314
2315 private final boolean isDirectory;
2316
2317 private final boolean isSymbolicLink;
2318
2319 private final boolean isRegularFile;
2320
2321 private final long creationTime;
2322
2323 private final Instant lastModifiedInstant;
2324
2325 private final boolean isExecutable;
2326
2327 private final File file;
2328
2329 private final boolean exists;
2330
2331
2332
2333
2334 protected long length = -1;
2335
2336 final FS fs;
2337
2338 Attributes(FS fs, File file, boolean exists, boolean isDirectory,
2339 boolean isExecutable, boolean isSymbolicLink,
2340 boolean isRegularFile, long creationTime,
2341 Instant lastModifiedInstant, long length) {
2342 this.fs = fs;
2343 this.file = file;
2344 this.exists = exists;
2345 this.isDirectory = isDirectory;
2346 this.isExecutable = isExecutable;
2347 this.isSymbolicLink = isSymbolicLink;
2348 this.isRegularFile = isRegularFile;
2349 this.creationTime = creationTime;
2350 this.lastModifiedInstant = lastModifiedInstant;
2351 this.length = length;
2352 }
2353
2354
2355
2356
2357
2358
2359
2360
2361 public Attributes(File path, FS fs) {
2362 this(fs, path, false, false, false, false, false, 0L, EPOCH, 0L);
2363 }
2364
2365
2366
2367
2368 public long getLength() {
2369 if (length == -1)
2370 return length = file.length();
2371 return length;
2372 }
2373
2374
2375
2376
2377 public String getName() {
2378 return file.getName();
2379 }
2380
2381
2382
2383
2384 public File getFile() {
2385 return file;
2386 }
2387
2388 boolean exists() {
2389 return exists;
2390 }
2391 }
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401 public Attributes getAttributes(File path) {
2402 boolean isDirectory = isDirectory(path);
2403 boolean isFile = !isDirectory && path.isFile();
2404 assert path.exists() == isDirectory || isFile;
2405 boolean exists = isDirectory || isFile;
2406 boolean canExecute = exists && !isDirectory && canExecute(path);
2407 boolean isSymlink = false;
2408 Instant lastModified = exists ? lastModifiedInstant(path) : EPOCH;
2409 long createTime = 0L;
2410 return new Attributes(this, path, exists, isDirectory, canExecute,
2411 isSymlink, isFile, createTime, lastModified, -1);
2412 }
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422 public File normalize(File file) {
2423 return file;
2424 }
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434 public String normalize(String name) {
2435 return name;
2436 }
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450 private static class StreamGobbler implements Runnable {
2451 private InputStream in;
2452
2453 private OutputStream out;
2454
2455 public StreamGobbler(InputStream stream, OutputStream output) {
2456 this.in = stream;
2457 this.out = output;
2458 }
2459
2460 @Override
2461 public void run() {
2462 try {
2463 copy();
2464 } catch (IOException e) {
2465
2466 }
2467 }
2468
2469 void copy() throws IOException {
2470 boolean writeFailure = false;
2471 byte[] buffer = new byte[4096];
2472 int readBytes;
2473 while ((readBytes = in.read(buffer)) != -1) {
2474
2475
2476
2477 if (!writeFailure && out != null) {
2478 try {
2479 out.write(buffer, 0, readBytes);
2480 out.flush();
2481 } catch (IOException e) {
2482 writeFailure = true;
2483 }
2484 }
2485 }
2486 }
2487 }
2488 }