LockFile.java

  1. /*
  2.  * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
  3.  * Copyright (C) 2006-2021, Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. package org.eclipse.jgit.internal.storage.file;

  12. import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;

  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.FileNotFoundException;
  16. import java.io.FileOutputStream;
  17. import java.io.FilenameFilter;
  18. import java.io.IOException;
  19. import java.io.OutputStream;
  20. import java.nio.ByteBuffer;
  21. import java.nio.channels.Channels;
  22. import java.nio.channels.FileChannel;
  23. import java.nio.file.Files;
  24. import java.nio.file.StandardCopyOption;
  25. import java.nio.file.attribute.FileTime;
  26. import java.text.MessageFormat;
  27. import java.time.Instant;
  28. import java.util.concurrent.TimeUnit;

  29. import org.eclipse.jgit.internal.JGitText;
  30. import org.eclipse.jgit.lib.Constants;
  31. import org.eclipse.jgit.lib.ObjectId;
  32. import org.eclipse.jgit.util.FS;
  33. import org.eclipse.jgit.util.FS.LockToken;
  34. import org.eclipse.jgit.util.FileUtils;
  35. import org.slf4j.Logger;
  36. import org.slf4j.LoggerFactory;

  37. /**
  38.  * Git style file locking and replacement.
  39.  * <p>
  40.  * To modify a ref file Git tries to use an atomic update approach: we write the
  41.  * new data into a brand new file, then rename it in place over the old name.
  42.  * This way we can just delete the temporary file if anything goes wrong, and
  43.  * nothing has been damaged. To coordinate access from multiple processes at
  44.  * once Git tries to atomically create the new temporary file under a well-known
  45.  * name.
  46.  */
  47. public class LockFile {
  48.     private static final Logger LOG = LoggerFactory.getLogger(LockFile.class);

  49.     /**
  50.      * Unlock the given file.
  51.      * <p>
  52.      * This method can be used for recovering from a thrown
  53.      * {@link org.eclipse.jgit.errors.LockFailedException} . This method does
  54.      * not validate that the lock is or is not currently held before attempting
  55.      * to unlock it.
  56.      *
  57.      * @param file
  58.      *            a {@link java.io.File} object.
  59.      * @return true if unlocked, false if unlocking failed
  60.      */
  61.     public static boolean unlock(File file) {
  62.         final File lockFile = getLockFile(file);
  63.         final int flags = FileUtils.RETRY | FileUtils.SKIP_MISSING;
  64.         try {
  65.             FileUtils.delete(lockFile, flags);
  66.         } catch (IOException ignored) {
  67.             // Ignore and return whether lock file still exists
  68.         }
  69.         return !lockFile.exists();
  70.     }

  71.     /**
  72.      * Get the lock file corresponding to the given file.
  73.      *
  74.      * @param file
  75.      * @return lock file
  76.      */
  77.     static File getLockFile(File file) {
  78.         return new File(file.getParentFile(),
  79.                 file.getName() + LOCK_SUFFIX);
  80.     }

  81.     /** Filter to skip over active lock files when listing a directory. */
  82.     static final FilenameFilter FILTER = (File dir,
  83.             String name) -> !name.endsWith(LOCK_SUFFIX);

  84.     private final File ref;

  85.     private final File lck;

  86.     private boolean haveLck;

  87.     private FileOutputStream os;

  88.     private boolean needSnapshot;

  89.     private boolean fsync;

  90.     private boolean isAppend;

  91.     private boolean written;

  92.     private boolean snapshotNoConfig;

  93.     private FileSnapshot commitSnapshot;

  94.     private LockToken token;

  95.     /**
  96.      * Create a new lock for any file.
  97.      *
  98.      * @param f
  99.      *            the file that will be locked.
  100.      */
  101.     public LockFile(File f) {
  102.         ref = f;
  103.         lck = getLockFile(ref);
  104.     }

  105.     /**
  106.      * Try to establish the lock.
  107.      *
  108.      * @return true if the lock is now held by the caller; false if it is held
  109.      *         by someone else.
  110.      * @throws java.io.IOException
  111.      *             the temporary output file could not be created. The caller
  112.      *             does not hold the lock.
  113.      */
  114.     public boolean lock() throws IOException {
  115.         if (haveLck) {
  116.             throw new IllegalStateException(
  117.                     MessageFormat.format(JGitText.get().lockAlreadyHeld, ref));
  118.         }
  119.         FileUtils.mkdirs(lck.getParentFile(), true);
  120.         try {
  121.             token = FS.DETECTED.createNewFileAtomic(lck);
  122.         } catch (IOException e) {
  123.             LOG.error(JGitText.get().failedCreateLockFile, lck, e);
  124.             throw e;
  125.         }
  126.         boolean obtainedLock = token.isCreated();
  127.         if (obtainedLock) {
  128.             haveLck = true;
  129.             isAppend = false;
  130.             written = false;
  131.         } else {
  132.             closeToken();
  133.         }
  134.         return obtainedLock;
  135.     }

  136.     /**
  137.      * Try to establish the lock for appending.
  138.      *
  139.      * @return true if the lock is now held by the caller; false if it is held
  140.      *         by someone else.
  141.      * @throws java.io.IOException
  142.      *             the temporary output file could not be created. The caller
  143.      *             does not hold the lock.
  144.      */
  145.     public boolean lockForAppend() throws IOException {
  146.         if (!lock()) {
  147.             return false;
  148.         }
  149.         copyCurrentContent();
  150.         isAppend = true;
  151.         written = false;
  152.         return true;
  153.     }

  154.     // For tests only
  155.     boolean isLocked() {
  156.         return haveLck;
  157.     }

  158.     private FileOutputStream getStream() throws IOException {
  159.         return new FileOutputStream(lck, isAppend);
  160.     }

  161.     /**
  162.      * Copy the current file content into the temporary file.
  163.      * <p>
  164.      * This method saves the current file content by inserting it into the
  165.      * temporary file, so that the caller can safely append rather than replace
  166.      * the primary file.
  167.      * <p>
  168.      * This method does nothing if the current file does not exist, or exists
  169.      * but is empty.
  170.      *
  171.      * @throws java.io.IOException
  172.      *             the temporary file could not be written, or a read error
  173.      *             occurred while reading from the current file. The lock is
  174.      *             released before throwing the underlying IO exception to the
  175.      *             caller.
  176.      * @throws java.lang.RuntimeException
  177.      *             the temporary file could not be written. The lock is released
  178.      *             before throwing the underlying exception to the caller.
  179.      */
  180.     public void copyCurrentContent() throws IOException {
  181.         requireLock();
  182.         try (FileOutputStream out = getStream()) {
  183.             try (FileInputStream fis = new FileInputStream(ref)) {
  184.                 if (fsync) {
  185.                     FileChannel in = fis.getChannel();
  186.                     long pos = 0;
  187.                     long cnt = in.size();
  188.                     while (0 < cnt) {
  189.                         long r = out.getChannel().transferFrom(in, pos, cnt);
  190.                         pos += r;
  191.                         cnt -= r;
  192.                     }
  193.                 } else {
  194.                     final byte[] buf = new byte[2048];
  195.                     int r;
  196.                     while ((r = fis.read(buf)) >= 0) {
  197.                         out.write(buf, 0, r);
  198.                     }
  199.                 }
  200.             } catch (FileNotFoundException fnfe) {
  201.                 if (ref.exists()) {
  202.                     throw fnfe;
  203.                 }
  204.                 // Don't worry about a file that doesn't exist yet, it
  205.                 // conceptually has no current content to copy.
  206.             }
  207.         } catch (IOException | RuntimeException | Error ioe) {
  208.             unlock();
  209.             throw ioe;
  210.         }
  211.     }

  212.     /**
  213.      * Write an ObjectId and LF to the temporary file.
  214.      *
  215.      * @param id
  216.      *            the id to store in the file. The id will be written in hex,
  217.      *            followed by a sole LF.
  218.      * @throws java.io.IOException
  219.      *             the temporary file could not be written. The lock is released
  220.      *             before throwing the underlying IO exception to the caller.
  221.      * @throws java.lang.RuntimeException
  222.      *             the temporary file could not be written. The lock is released
  223.      *             before throwing the underlying exception to the caller.
  224.      */
  225.     public void write(ObjectId id) throws IOException {
  226.         byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1];
  227.         id.copyTo(buf, 0);
  228.         buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n';
  229.         write(buf);
  230.     }

  231.     /**
  232.      * Write arbitrary data to the temporary file.
  233.      *
  234.      * @param content
  235.      *            the bytes to store in the temporary file. No additional bytes
  236.      *            are added, so if the file must end with an LF it must appear
  237.      *            at the end of the byte array.
  238.      * @throws java.io.IOException
  239.      *             the temporary file could not be written. The lock is released
  240.      *             before throwing the underlying IO exception to the caller.
  241.      * @throws java.lang.RuntimeException
  242.      *             the temporary file could not be written. The lock is released
  243.      *             before throwing the underlying exception to the caller.
  244.      */
  245.     public void write(byte[] content) throws IOException {
  246.         requireLock();
  247.         try (FileOutputStream out = getStream()) {
  248.             if (written) {
  249.                 throw new IOException(MessageFormat
  250.                         .format(JGitText.get().lockStreamClosed, ref));
  251.             }
  252.             if (fsync) {
  253.                 FileChannel fc = out.getChannel();
  254.                 ByteBuffer buf = ByteBuffer.wrap(content);
  255.                 while (0 < buf.remaining()) {
  256.                     fc.write(buf);
  257.                 }
  258.                 fc.force(true);
  259.             } else {
  260.                 out.write(content);
  261.             }
  262.             written = true;
  263.         } catch (IOException | RuntimeException | Error ioe) {
  264.             unlock();
  265.             throw ioe;
  266.         }
  267.     }

  268.     /**
  269.      * Obtain the direct output stream for this lock.
  270.      * <p>
  271.      * The stream may only be accessed once, and only after {@link #lock()} has
  272.      * been successfully invoked and returned true. Callers must close the
  273.      * stream prior to calling {@link #commit()} to commit the change.
  274.      *
  275.      * @return a stream to write to the new file. The stream is unbuffered.
  276.      */
  277.     public OutputStream getOutputStream() {
  278.         requireLock();

  279.         if (written || os != null) {
  280.             throw new IllegalStateException(MessageFormat
  281.                     .format(JGitText.get().lockStreamMultiple, ref));
  282.         }

  283.         return new OutputStream() {

  284.             private OutputStream out;

  285.             private boolean closed;

  286.             private OutputStream get() throws IOException {
  287.                 if (written) {
  288.                     throw new IOException(MessageFormat
  289.                             .format(JGitText.get().lockStreamMultiple, ref));
  290.                 }
  291.                 if (out == null) {
  292.                     os = getStream();
  293.                     if (fsync) {
  294.                         out = Channels.newOutputStream(os.getChannel());
  295.                     } else {
  296.                         out = os;
  297.                     }
  298.                 }
  299.                 return out;
  300.             }

  301.             @Override
  302.             public void write(byte[] b, int o, int n) throws IOException {
  303.                 get().write(b, o, n);
  304.             }

  305.             @Override
  306.             public void write(byte[] b) throws IOException {
  307.                 get().write(b);
  308.             }

  309.             @Override
  310.             public void write(int b) throws IOException {
  311.                 get().write(b);
  312.             }

  313.             @Override
  314.             public void close() throws IOException {
  315.                 if (closed) {
  316.                     return;
  317.                 }
  318.                 closed = true;
  319.                 try {
  320.                     if (written) {
  321.                         throw new IOException(MessageFormat
  322.                                 .format(JGitText.get().lockStreamClosed, ref));
  323.                     }
  324.                     if (out != null) {
  325.                         if (fsync) {
  326.                             os.getChannel().force(true);
  327.                         }
  328.                         out.close();
  329.                         os = null;
  330.                     }
  331.                     written = true;
  332.                 } catch (IOException | RuntimeException | Error ioe) {
  333.                     unlock();
  334.                     throw ioe;
  335.                 }
  336.             }
  337.         };
  338.     }

  339.     void requireLock() {
  340.         if (!haveLck) {
  341.             unlock();
  342.             throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref));
  343.         }
  344.     }

  345.     /**
  346.      * Request that {@link #commit()} remember modification time.
  347.      * <p>
  348.      * This is an alias for {@code setNeedSnapshot(true)}.
  349.      *
  350.      * @param on
  351.      *            true if the commit method must remember the modification time.
  352.      */
  353.     public void setNeedStatInformation(boolean on) {
  354.         setNeedSnapshot(on);
  355.     }

  356.     /**
  357.      * Request that {@link #commit()} remember the
  358.      * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot}.
  359.      *
  360.      * @param on
  361.      *            true if the commit method must remember the FileSnapshot.
  362.      */
  363.     public void setNeedSnapshot(boolean on) {
  364.         needSnapshot = on;
  365.     }

  366.     /**
  367.      * Request that {@link #commit()} remember the
  368.      * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot} without using
  369.      * config file to get filesystem timestamp resolution.
  370.      * This method should be invoked before the file is accessed.
  371.      * It is used by FileBasedConfig to avoid endless recursion.
  372.      *
  373.      * @param on
  374.      *            true if the commit method must remember the FileSnapshot.
  375.      */
  376.     public void setNeedSnapshotNoConfig(boolean on) {
  377.         needSnapshot = on;
  378.         snapshotNoConfig = on;
  379.     }

  380.     /**
  381.      * Request that {@link #commit()} force dirty data to the drive.
  382.      *
  383.      * @param on
  384.      *            true if dirty data should be forced to the drive.
  385.      */
  386.     public void setFSync(boolean on) {
  387.         fsync = on;
  388.     }

  389.     /**
  390.      * Wait until the lock file information differs from the old file.
  391.      * <p>
  392.      * This method tests the last modification date. If both are the same, this
  393.      * method sleeps until it can force the new lock file's modification date to
  394.      * be later than the target file.
  395.      *
  396.      * @throws java.lang.InterruptedException
  397.      *             the thread was interrupted before the last modified date of
  398.      *             the lock file was different from the last modified date of
  399.      *             the target file.
  400.      */
  401.     public void waitForStatChange() throws InterruptedException {
  402.         FileSnapshot o = FileSnapshot.save(ref);
  403.         FileSnapshot n = FileSnapshot.save(lck);
  404.         long fsTimeResolution = FS.getFileStoreAttributes(lck.toPath())
  405.                 .getFsTimestampResolution().toNanos();
  406.         while (o.equals(n)) {
  407.             TimeUnit.NANOSECONDS.sleep(fsTimeResolution);
  408.             try {
  409.                 Files.setLastModifiedTime(lck.toPath(),
  410.                         FileTime.from(Instant.now()));
  411.             } catch (IOException e) {
  412.                 n.waitUntilNotRacy();
  413.             }
  414.             n = FileSnapshot.save(lck);
  415.         }
  416.     }

  417.     /**
  418.      * Commit this change and release the lock.
  419.      * <p>
  420.      * If this method fails (returns false) the lock is still released.
  421.      *
  422.      * @return true if the commit was successful and the file contains the new
  423.      *         data; false if the commit failed and the file remains with the
  424.      *         old data.
  425.      * @throws java.lang.IllegalStateException
  426.      *             the lock is not held.
  427.      */
  428.     public boolean commit() {
  429.         if (os != null) {
  430.             unlock();
  431.             throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref));
  432.         }

  433.         saveStatInformation();
  434.         try {
  435.             FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE);
  436.             haveLck = false;
  437.             isAppend = false;
  438.             written = false;
  439.             closeToken();
  440.             return true;
  441.         } catch (IOException e) {
  442.             unlock();
  443.             return false;
  444.         }
  445.     }

  446.     private void closeToken() {
  447.         if (token != null) {
  448.             token.close();
  449.             token = null;
  450.         }
  451.     }

  452.     private void saveStatInformation() {
  453.         if (needSnapshot) {
  454.             commitSnapshot = snapshotNoConfig ?
  455.                 // don't use config in this snapshot to avoid endless recursion
  456.                 FileSnapshot.saveNoConfig(lck)
  457.                 : FileSnapshot.save(lck);
  458.         }
  459.     }

  460.     /**
  461.      * Get the modification time of the output file when it was committed.
  462.      *
  463.      * @return modification time of the lock file right before we committed it.
  464.      * @deprecated use {@link #getCommitLastModifiedInstant()} instead
  465.      */
  466.     @Deprecated
  467.     public long getCommitLastModified() {
  468.         return commitSnapshot.lastModified();
  469.     }

  470.     /**
  471.      * Get the modification time of the output file when it was committed.
  472.      *
  473.      * @return modification time of the lock file right before we committed it.
  474.      */
  475.     public Instant getCommitLastModifiedInstant() {
  476.         return commitSnapshot.lastModifiedInstant();
  477.     }

  478.     /**
  479.      * Get the {@link FileSnapshot} just before commit.
  480.      *
  481.      * @return get the {@link FileSnapshot} just before commit.
  482.      */
  483.     public FileSnapshot getCommitSnapshot() {
  484.         return commitSnapshot;
  485.     }

  486.     /**
  487.      * Update the commit snapshot {@link #getCommitSnapshot()} before commit.
  488.      * <p>
  489.      * This may be necessary if you need time stamp before commit occurs, e.g
  490.      * while writing the index.
  491.      */
  492.     public void createCommitSnapshot() {
  493.         saveStatInformation();
  494.     }

  495.     /**
  496.      * Unlock this file and abort this change.
  497.      * <p>
  498.      * The temporary file (if created) is deleted before returning.
  499.      */
  500.     public void unlock() {
  501.         if (os != null) {
  502.             try {
  503.                 os.close();
  504.             } catch (IOException e) {
  505.                 LOG.error(MessageFormat
  506.                         .format(JGitText.get().unlockLockFileFailed, lck), e);
  507.             }
  508.             os = null;
  509.         }

  510.         if (haveLck) {
  511.             haveLck = false;
  512.             try {
  513.                 FileUtils.delete(lck, FileUtils.RETRY);
  514.             } catch (IOException e) {
  515.                 LOG.error(MessageFormat
  516.                         .format(JGitText.get().unlockLockFileFailed, lck), e);
  517.             } finally {
  518.                 closeToken();
  519.             }
  520.         }
  521.         isAppend = false;
  522.         written = false;
  523.     }

  524.     /** {@inheritDoc} */
  525.     @SuppressWarnings("nls")
  526.     @Override
  527.     public String toString() {
  528.         return "LockFile[" + lck + ", haveLck=" + haveLck + "]";
  529.     }
  530. }