1 /* 2 * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com> 3 * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> 4 * and other copyright owners as documented in the project's IP log. 5 * 6 * This program and the accompanying materials are made available 7 * under the terms of the Eclipse Distribution License v1.0 which 8 * accompanies this distribution, is reproduced below, and is 9 * available at http://www.eclipse.org/org/documents/edl-v10.php 10 * 11 * All rights reserved. 12 * 13 * Redistribution and use in source and binary forms, with or 14 * without modification, are permitted provided that the following 15 * conditions are met: 16 * 17 * - Redistributions of source code must retain the above copyright 18 * notice, this list of conditions and the following disclaimer. 19 * 20 * - Redistributions in binary form must reproduce the above 21 * copyright notice, this list of conditions and the following 22 * disclaimer in the documentation and/or other materials provided 23 * with the distribution. 24 * 25 * - Neither the name of the Eclipse Foundation, Inc. nor the 26 * names of its contributors may be used to endorse or promote 27 * products derived from this software without specific prior 28 * written permission. 29 * 30 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 31 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 32 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 33 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 34 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 35 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 36 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 37 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 40 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 41 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 42 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 43 */ 44 45 package org.eclipse.jgit.internal.storage.file; 46 47 import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX; 48 49 import java.io.File; 50 import java.io.FileInputStream; 51 import java.io.FileNotFoundException; 52 import java.io.FileOutputStream; 53 import java.io.FilenameFilter; 54 import java.io.IOException; 55 import java.io.OutputStream; 56 import java.nio.ByteBuffer; 57 import java.nio.channels.Channels; 58 import java.nio.channels.FileChannel; 59 import java.nio.file.StandardCopyOption; 60 import java.text.MessageFormat; 61 62 import org.eclipse.jgit.internal.JGitText; 63 import org.eclipse.jgit.lib.Constants; 64 import org.eclipse.jgit.lib.ObjectId; 65 import org.eclipse.jgit.util.FS; 66 import org.eclipse.jgit.util.FS.LockToken; 67 import org.eclipse.jgit.util.FileUtils; 68 import org.slf4j.Logger; 69 import org.slf4j.LoggerFactory; 70 71 /** 72 * Git style file locking and replacement. 73 * <p> 74 * To modify a ref file Git tries to use an atomic update approach: we write the 75 * new data into a brand new file, then rename it in place over the old name. 76 * This way we can just delete the temporary file if anything goes wrong, and 77 * nothing has been damaged. To coordinate access from multiple processes at 78 * once Git tries to atomically create the new temporary file under a well-known 79 * name. 80 */ 81 public class LockFile { 82 private final static Logger LOG = LoggerFactory.getLogger(LockFile.class); 83 84 /** 85 * Unlock the given file. 86 * <p> 87 * This method can be used for recovering from a thrown 88 * {@link org.eclipse.jgit.errors.LockFailedException} . This method does 89 * not validate that the lock is or is not currently held before attempting 90 * to unlock it. 91 * 92 * @param file 93 * a {@link java.io.File} object. 94 * @return true if unlocked, false if unlocking failed 95 */ 96 public static boolean unlock(File file) { 97 final File lockFile = getLockFile(file); 98 final int flags = FileUtils.RETRY | FileUtils.SKIP_MISSING; 99 try { 100 FileUtils.delete(lockFile, flags); 101 } catch (IOException ignored) { 102 // Ignore and return whether lock file still exists 103 } 104 return !lockFile.exists(); 105 } 106 107 /** 108 * Get the lock file corresponding to the given file. 109 * 110 * @param file 111 * @return lock file 112 */ 113 static File getLockFile(File file) { 114 return new File(file.getParentFile(), 115 file.getName() + LOCK_SUFFIX); 116 } 117 118 /** Filter to skip over active lock files when listing a directory. */ 119 static final FilenameFilter FILTER = (File dir, 120 String name) -> !name.endsWith(LOCK_SUFFIX); 121 122 private final File ref; 123 124 private final File lck; 125 126 private boolean haveLck; 127 128 FileOutputStream os; 129 130 private boolean needSnapshot; 131 132 boolean fsync; 133 134 private FileSnapshot commitSnapshot; 135 136 private LockToken token; 137 138 /** 139 * Create a new lock for any file. 140 * 141 * @param f 142 * the file that will be locked. 143 */ 144 public LockFile(File f) { 145 ref = f; 146 lck = getLockFile(ref); 147 } 148 149 /** 150 * Try to establish the lock. 151 * 152 * @return true if the lock is now held by the caller; false if it is held 153 * by someone else. 154 * @throws java.io.IOException 155 * the temporary output file could not be created. The caller 156 * does not hold the lock. 157 */ 158 public boolean lock() throws IOException { 159 FileUtils.mkdirs(lck.getParentFile(), true); 160 token = FS.DETECTED.createNewFileAtomic(lck); 161 if (token.isCreated()) { 162 haveLck = true; 163 try { 164 os = new FileOutputStream(lck); 165 } catch (IOException ioe) { 166 unlock(); 167 throw ioe; 168 } 169 } else { 170 closeToken(); 171 } 172 return haveLck; 173 } 174 175 /** 176 * Try to establish the lock for appending. 177 * 178 * @return true if the lock is now held by the caller; false if it is held 179 * by someone else. 180 * @throws java.io.IOException 181 * the temporary output file could not be created. The caller 182 * does not hold the lock. 183 */ 184 public boolean lockForAppend() throws IOException { 185 if (!lock()) 186 return false; 187 copyCurrentContent(); 188 return true; 189 } 190 191 /** 192 * Copy the current file content into the temporary file. 193 * <p> 194 * This method saves the current file content by inserting it into the 195 * temporary file, so that the caller can safely append rather than replace 196 * the primary file. 197 * <p> 198 * This method does nothing if the current file does not exist, or exists 199 * but is empty. 200 * 201 * @throws java.io.IOException 202 * the temporary file could not be written, or a read error 203 * occurred while reading from the current file. The lock is 204 * released before throwing the underlying IO exception to the 205 * caller. 206 * @throws java.lang.RuntimeException 207 * the temporary file could not be written. The lock is released 208 * before throwing the underlying exception to the caller. 209 */ 210 public void copyCurrentContent() throws IOException { 211 requireLock(); 212 try { 213 try (FileInputStream fis = new FileInputStream(ref)) { 214 if (fsync) { 215 FileChannel in = fis.getChannel(); 216 long pos = 0; 217 long cnt = in.size(); 218 while (0 < cnt) { 219 long r = os.getChannel().transferFrom(in, pos, cnt); 220 pos += r; 221 cnt -= r; 222 } 223 } else { 224 final byte[] buf = new byte[2048]; 225 int r; 226 while ((r = fis.read(buf)) >= 0) 227 os.write(buf, 0, r); 228 } 229 } 230 } catch (FileNotFoundException fnfe) { 231 if (ref.exists()) { 232 unlock(); 233 throw fnfe; 234 } 235 // Don't worry about a file that doesn't exist yet, it 236 // conceptually has no current content to copy. 237 // 238 } catch (IOException | RuntimeException | Error ioe) { 239 unlock(); 240 throw ioe; 241 } 242 } 243 244 /** 245 * Write an ObjectId and LF to the temporary file. 246 * 247 * @param id 248 * the id to store in the file. The id will be written in hex, 249 * followed by a sole LF. 250 * @throws java.io.IOException 251 * the temporary file could not be written. The lock is released 252 * before throwing the underlying IO exception to the caller. 253 * @throws java.lang.RuntimeException 254 * the temporary file could not be written. The lock is released 255 * before throwing the underlying exception to the caller. 256 */ 257 public void write(ObjectId id) throws IOException { 258 byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1]; 259 id.copyTo(buf, 0); 260 buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n'; 261 write(buf); 262 } 263 264 /** 265 * Write arbitrary data to the temporary file. 266 * 267 * @param content 268 * the bytes to store in the temporary file. No additional bytes 269 * are added, so if the file must end with an LF it must appear 270 * at the end of the byte array. 271 * @throws java.io.IOException 272 * the temporary file could not be written. The lock is released 273 * before throwing the underlying IO exception to the caller. 274 * @throws java.lang.RuntimeException 275 * the temporary file could not be written. The lock is released 276 * before throwing the underlying exception to the caller. 277 */ 278 public void write(byte[] content) throws IOException { 279 requireLock(); 280 try { 281 if (fsync) { 282 FileChannel fc = os.getChannel(); 283 ByteBuffer buf = ByteBuffer.wrap(content); 284 while (0 < buf.remaining()) 285 fc.write(buf); 286 fc.force(true); 287 } else { 288 os.write(content); 289 } 290 os.close(); 291 os = null; 292 } catch (IOException | RuntimeException | Error ioe) { 293 unlock(); 294 throw ioe; 295 } 296 } 297 298 /** 299 * Obtain the direct output stream for this lock. 300 * <p> 301 * The stream may only be accessed once, and only after {@link #lock()} has 302 * been successfully invoked and returned true. Callers must close the 303 * stream prior to calling {@link #commit()} to commit the change. 304 * 305 * @return a stream to write to the new file. The stream is unbuffered. 306 */ 307 public OutputStream getOutputStream() { 308 requireLock(); 309 310 final OutputStream out; 311 if (fsync) 312 out = Channels.newOutputStream(os.getChannel()); 313 else 314 out = os; 315 316 return new OutputStream() { 317 @Override 318 public void write(byte[] b, int o, int n) 319 throws IOException { 320 out.write(b, o, n); 321 } 322 323 @Override 324 public void write(byte[] b) throws IOException { 325 out.write(b); 326 } 327 328 @Override 329 public void write(int b) throws IOException { 330 out.write(b); 331 } 332 333 @Override 334 public void close() throws IOException { 335 try { 336 if (fsync) 337 os.getChannel().force(true); 338 out.close(); 339 os = null; 340 } catch (IOException | RuntimeException | Error ioe) { 341 unlock(); 342 throw ioe; 343 } 344 } 345 }; 346 } 347 348 void requireLock() { 349 if (os == null) { 350 unlock(); 351 throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref)); 352 } 353 } 354 355 /** 356 * Request that {@link #commit()} remember modification time. 357 * <p> 358 * This is an alias for {@code setNeedSnapshot(true)}. 359 * 360 * @param on 361 * true if the commit method must remember the modification time. 362 */ 363 public void setNeedStatInformation(boolean on) { 364 setNeedSnapshot(on); 365 } 366 367 /** 368 * Request that {@link #commit()} remember the 369 * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot}. 370 * 371 * @param on 372 * true if the commit method must remember the FileSnapshot. 373 */ 374 public void setNeedSnapshot(boolean on) { 375 needSnapshot = on; 376 } 377 378 /** 379 * Request that {@link #commit()} force dirty data to the drive. 380 * 381 * @param on 382 * true if dirty data should be forced to the drive. 383 */ 384 public void setFSync(boolean on) { 385 fsync = on; 386 } 387 388 /** 389 * Wait until the lock file information differs from the old file. 390 * <p> 391 * This method tests the last modification date. If both are the same, this 392 * method sleeps until it can force the new lock file's modification date to 393 * be later than the target file. 394 * 395 * @throws java.lang.InterruptedException 396 * the thread was interrupted before the last modified date of 397 * the lock file was different from the last modified date of 398 * the target file. 399 */ 400 public void waitForStatChange() throws InterruptedException { 401 FileSnapshot o = FileSnapshot.save(ref); 402 FileSnapshot n = FileSnapshot.save(lck); 403 while (o.equals(n)) { 404 Thread.sleep(25 /* milliseconds */); 405 lck.setLastModified(System.currentTimeMillis()); 406 n = FileSnapshot.save(lck); 407 } 408 } 409 410 /** 411 * Commit this change and release the lock. 412 * <p> 413 * If this method fails (returns false) the lock is still released. 414 * 415 * @return true if the commit was successful and the file contains the new 416 * data; false if the commit failed and the file remains with the 417 * old data. 418 * @throws java.lang.IllegalStateException 419 * the lock is not held. 420 */ 421 public boolean commit() { 422 if (os != null) { 423 unlock(); 424 throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref)); 425 } 426 427 saveStatInformation(); 428 try { 429 FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE); 430 haveLck = false; 431 closeToken(); 432 return true; 433 } catch (IOException e) { 434 unlock(); 435 return false; 436 } 437 } 438 439 private void closeToken() { 440 if (token != null) { 441 token.close(); 442 token = null; 443 } 444 } 445 446 private void saveStatInformation() { 447 if (needSnapshot) 448 commitSnapshot = FileSnapshot.save(lck); 449 } 450 451 /** 452 * Get the modification time of the output file when it was committed. 453 * 454 * @return modification time of the lock file right before we committed it. 455 */ 456 public long getCommitLastModified() { 457 return commitSnapshot.lastModified(); 458 } 459 460 /** 461 * Get the {@link FileSnapshot} just before commit. 462 * 463 * @return get the {@link FileSnapshot} just before commit. 464 */ 465 public FileSnapshot getCommitSnapshot() { 466 return commitSnapshot; 467 } 468 469 /** 470 * Update the commit snapshot {@link #getCommitSnapshot()} before commit. 471 * <p> 472 * This may be necessary if you need time stamp before commit occurs, e.g 473 * while writing the index. 474 */ 475 public void createCommitSnapshot() { 476 saveStatInformation(); 477 } 478 479 /** 480 * Unlock this file and abort this change. 481 * <p> 482 * The temporary file (if created) is deleted before returning. 483 */ 484 public void unlock() { 485 if (os != null) { 486 try { 487 os.close(); 488 } catch (IOException e) { 489 LOG.error(MessageFormat 490 .format(JGitText.get().unlockLockFileFailed, lck), e); 491 } 492 os = null; 493 } 494 495 if (haveLck) { 496 haveLck = false; 497 try { 498 FileUtils.delete(lck, FileUtils.RETRY); 499 } catch (IOException e) { 500 LOG.error(MessageFormat 501 .format(JGitText.get().unlockLockFileFailed, lck), e); 502 } finally { 503 closeToken(); 504 } 505 } 506 } 507 508 /** {@inheritDoc} */ 509 @SuppressWarnings("nls") 510 @Override 511 public String toString() { 512 return "LockFile[" + lck + ", haveLck=" + haveLck + "]"; 513 } 514 }