diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java index 3bfd04778..4e123f4c2 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/LockFileTest.java @@ -1,51 +1,23 @@ /* - * Copyright (C) 2012, GitHub Inc. - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2012, 2021 GitHub Inc. and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.io.File; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.errors.LockFailedException; @@ -82,4 +54,167 @@ public void lockFailedExceptionRecovery() throws Exception { } } } + + @Test + public void testLockTwice() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + lock.write("other".getBytes(StandardCharsets.US_ASCII)); + lock.commit(); + assertFalse(lock.isLocked()); + checkFile(f, "other"); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try (OutputStream out = lock.getOutputStream()) { + out.write("second".getBytes(StandardCharsets.US_ASCII)); + } + lock.commit(); + assertFalse(lock.isLocked()); + checkFile(f, "second"); + } + + @Test + public void testLockTwiceUnlock() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + lock.write("other".getBytes(StandardCharsets.US_ASCII)); + lock.unlock(); + assertFalse(lock.isLocked()); + checkFile(f, "content"); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try (OutputStream out = lock.getOutputStream()) { + out.write("second".getBytes(StandardCharsets.US_ASCII)); + } + lock.commit(); + assertFalse(lock.isLocked()); + checkFile(f, "second"); + } + + @Test + public void testLockWriteTwiceThrows1() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + lock.write("other".getBytes(StandardCharsets.US_ASCII)); + try { + lock.write("second".getBytes(StandardCharsets.US_ASCII)); + fail(); + } catch (Exception e) { + // expected + } + lock.unlock(); + } + + @Test + public void testLockWriteTwiceThrows2() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try (OutputStream out = lock.getOutputStream()) { + out.write("other".getBytes(StandardCharsets.US_ASCII)); + } + try { + lock.write("second".getBytes(StandardCharsets.US_ASCII)); + fail(); + } catch (Exception e) { + // expected + } + lock.unlock(); + } + + @Test + public void testLockWriteTwiceThrows3() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + lock.write("other".getBytes(StandardCharsets.US_ASCII)); + try (OutputStream out = lock.getOutputStream()) { + out.write("second".getBytes(StandardCharsets.US_ASCII)); + fail(); + } catch (Exception e) { + // expected + } + lock.unlock(); + } + + @Test + public void testLockWriteTwiceThrows4() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try (OutputStream out = lock.getOutputStream()) { + out.write("other".getBytes(StandardCharsets.US_ASCII)); + } + try (OutputStream out = lock.getOutputStream()) { + out.write("second".getBytes(StandardCharsets.US_ASCII)); + fail(); + } catch (Exception e) { + // expected + } + lock.unlock(); + } + + @Test + public void testLockUnclosedCommitThrows() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try (OutputStream out = lock.getOutputStream()) { + out.write("other".getBytes(StandardCharsets.US_ASCII)); + lock.commit(); + fail(); + } catch (Exception e) { + // expected + } + } + + @Test + public void testLockNested() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + try { + lock.lock(); + fail(); + } catch (IllegalStateException e) { + // expected + } + assertTrue(lock.isLocked()); + lock.unlock(); + } + + @Test + public void testLockHeld() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lock()); + assertTrue(lock.isLocked()); + LockFile lock2 = new LockFile(f); + assertFalse(lock2.lock()); + assertFalse(lock2.isLocked()); + assertTrue(lock.isLocked()); + lock.unlock(); + } + + @Test + public void testLockForAppend() throws Exception { + File f = writeTrashFile("somefile", "content"); + LockFile lock = new LockFile(f); + assertTrue(lock.lockForAppend()); + assertTrue(lock.isLocked()); + lock.write("other".getBytes(StandardCharsets.US_ASCII)); + lock.commit(); + assertFalse(lock.isLocked()); + checkFile(f, "contentother"); + } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 9e483a6c6..1888a87c0 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -408,11 +408,18 @@ listingPacks=Listing packs localObjectsIncomplete=Local objects incomplete. localRefIsMissingObjects=Local ref {0} is missing object(s). localRepository=local repository +lockAlreadyHeld=Lock on {0} already held lockCountMustBeGreaterOrEqual1=lockCount must be >= 1 lockError=lock error: {0} lockFailedRetry=locking {0} failed after {1} retries lockOnNotClosed=Lock on {0} not closed. lockOnNotHeld=Lock on {0} not held. +lockStreamClosed=Output to lock on {0} already closed +lockStreamMultiple=Output to lock on {0} already opened +logInconsistentFiletimeDiff={}: inconsistent duration from file timestamps on {}, {}: {} > {}, but diff = {}. Aborting measurement at resolution {}. +logLargerFiletimeDiff={}: inconsistent duration from file timestamps on {}, {}: diff = {} > {} (last good value). Aborting measurement. +logSmallerFiletime={}: got smaller file timestamp on {}, {}: {} < {}. Aborting measurement at resolution {}. +logXDGConfigHomeInvalid=Environment variable XDG_CONFIG_HOME contains an invalid path {} maxCountMustBeNonNegative=max count must be >= 0 mergeConflictOnNonNoteEntries=Merge conflict on non-note entries: base = {0}, ours = {1}, theirs = {2} mergeConflictOnNotes=Merge conflict on note {0}. base = {1}, ours = {2}, theirs = {2} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 535306967..9d8824467 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -1,45 +1,12 @@ /* * Copyright (C) 2010, 2013 Sasa Zivkov - * Copyright (C) 2012, Research In Motion Limited - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2012, 2021 Research In Motion Limited and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal; @@ -470,10 +437,17 @@ public static JGitText get() { /***/ public String localRefIsMissingObjects; /***/ public String localRepository; /***/ public String lockCountMustBeGreaterOrEqual1; + /***/ public String lockAlreadyHeld; /***/ public String lockError; /***/ public String lockFailedRetry; /***/ public String lockOnNotClosed; /***/ public String lockOnNotHeld; + /***/ public String lockStreamClosed; + /***/ public String lockStreamMultiple; + /***/ public String logInconsistentFiletimeDiff; + /***/ public String logLargerFiletimeDiff; + /***/ public String logSmallerFiletime; + /***/ public String logXDGConfigHomeInvalid; /***/ public String maxCountMustBeNonNegative; /***/ public String mergeConflictOnNonNoteEntries; /***/ public String mergeConflictOnNotes; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java index 6bcd7c070..f57581a29 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java @@ -1,45 +1,12 @@ /* * Copyright (C) 2007, Robin Rosenberg - * Copyright (C) 2006-2008, Shawn O. Pearce - * and other copyright owners as documented in the project's IP log. + * Copyright (C) 2006-2021, Shawn O. Pearce and others * - * This program and the accompanying materials are made available - * under the terms of the Eclipse Distribution License v1.0 which - * accompanies this distribution, is reproduced below, and is - * available at http://www.eclipse.org/org/documents/edl-v10.php + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or - * without modification, are permitted provided that the following - * conditions are met: - * - * - Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * - Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following - * disclaimer in the documentation and/or other materials provided - * with the distribution. - * - * - Neither the name of the Eclipse Foundation, Inc. nor the - * names of its contributors may be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND - * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.internal.storage.file; @@ -129,11 +96,15 @@ static File getLockFile(File file) { private boolean haveLck; - FileOutputStream os; + private FileOutputStream os; private boolean needSnapshot; - boolean fsync; + private boolean fsync; + + private boolean isAppend; + + private boolean written; private FileSnapshot commitSnapshot; @@ -160,6 +131,10 @@ public LockFile(File f) { * does not hold the lock. */ public boolean lock() throws IOException { + if (haveLck) { + throw new IllegalStateException( + MessageFormat.format(JGitText.get().lockAlreadyHeld, ref)); + } FileUtils.mkdirs(lck.getParentFile(), true); try { token = FS.DETECTED.createNewFileAtomic(lck); @@ -167,18 +142,15 @@ public boolean lock() throws IOException { LOG.error(JGitText.get().failedCreateLockFile, lck, e); throw e; } - if (token.isCreated()) { + boolean obtainedLock = token.isCreated(); + if (obtainedLock) { haveLck = true; - try { - os = new FileOutputStream(lck); - } catch (IOException ioe) { - unlock(); - throw ioe; - } + isAppend = false; + written = false; } else { closeToken(); } - return haveLck; + return obtainedLock; } /** @@ -191,12 +163,24 @@ public boolean lock() throws IOException { * does not hold the lock. */ public boolean lockForAppend() throws IOException { - if (!lock()) + if (!lock()) { return false; + } copyCurrentContent(); + isAppend = true; + written = false; return true; } + // For tests only + boolean isLocked() { + return haveLck; + } + + private FileOutputStream getStream() throws IOException { + return new FileOutputStream(lck, isAppend); + } + /** * Copy the current file content into the temporary file. *

@@ -218,32 +202,31 @@ public boolean lockForAppend() throws IOException { */ public void copyCurrentContent() throws IOException { requireLock(); - try { + try (FileOutputStream out = getStream()) { try (FileInputStream fis = new FileInputStream(ref)) { if (fsync) { FileChannel in = fis.getChannel(); long pos = 0; long cnt = in.size(); while (0 < cnt) { - long r = os.getChannel().transferFrom(in, pos, cnt); + long r = out.getChannel().transferFrom(in, pos, cnt); pos += r; cnt -= r; } } else { final byte[] buf = new byte[2048]; int r; - while ((r = fis.read(buf)) >= 0) - os.write(buf, 0, r); + while ((r = fis.read(buf)) >= 0) { + out.write(buf, 0, r); } } } catch (FileNotFoundException fnfe) { if (ref.exists()) { - unlock(); throw fnfe; } // Don't worry about a file that doesn't exist yet, it // conceptually has no current content to copy. - // + } } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -286,18 +269,22 @@ public void write(ObjectId id) throws IOException { */ public void write(byte[] content) throws IOException { requireLock(); - try { + try (FileOutputStream out = getStream()) { + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamClosed, ref)); + } if (fsync) { - FileChannel fc = os.getChannel(); + FileChannel fc = out.getChannel(); ByteBuffer buf = ByteBuffer.wrap(content); - while (0 < buf.remaining()) + while (0 < buf.remaining()) { fc.write(buf); + } fc.force(true); } else { - os.write(content); + out.write(content); } - os.close(); - os = null; + written = true; } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -316,36 +303,68 @@ public void write(byte[] content) throws IOException { public OutputStream getOutputStream() { requireLock(); - final OutputStream out; - if (fsync) - out = Channels.newOutputStream(os.getChannel()); - else - out = os; + if (written || os != null) { + throw new IllegalStateException(MessageFormat + .format(JGitText.get().lockStreamMultiple, ref)); + } return new OutputStream() { + + private OutputStream out; + + private boolean closed; + + private OutputStream get() throws IOException { + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamMultiple, ref)); + } + if (out == null) { + os = getStream(); + if (fsync) { + out = Channels.newOutputStream(os.getChannel()); + } else { + out = os; + } + } + return out; + } + @Override public void write(byte[] b, int o, int n) throws IOException { - out.write(b, o, n); + get().write(b, o, n); } @Override public void write(byte[] b) throws IOException { - out.write(b); + get().write(b); } @Override public void write(int b) throws IOException { - out.write(b); + get().write(b); } @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; try { - if (fsync) + if (written) { + throw new IOException(MessageFormat + .format(JGitText.get().lockStreamClosed, ref)); + } + if (out != null) { + if (fsync) { os.getChannel().force(true); + } out.close(); os = null; + } + written = true; } catch (IOException | RuntimeException | Error ioe) { unlock(); throw ioe; @@ -355,7 +374,7 @@ public void close() throws IOException { } void requireLock() { - if (os == null) { + if (!haveLck) { unlock(); throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref)); } @@ -444,6 +463,8 @@ public boolean commit() { try { FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE); haveLck = false; + isAppend = false; + written = false; closeToken(); return true; } catch (IOException e) { @@ -530,6 +551,8 @@ public void unlock() { closeToken(); } } + isAppend = false; + written = false; } /** {@inheritDoc} */