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.FileUtils; 67 68 /** 69 * Git style file locking and replacement. 70 * <p> 71 * To modify a ref file Git tries to use an atomic update approach: we write the 72 * new data into a brand new file, then rename it in place over the old name. 73 * This way we can just delete the temporary file if anything goes wrong, and 74 * nothing has been damaged. To coordinate access from multiple processes at 75 * once Git tries to atomically create the new temporary file under a well-known 76 * name. 77 */ 78 public class LockFile { 79 80 /** 81 * Unlock the given file. 82 * <p> 83 * This method can be used for recovering from a thrown 84 * {@link org.eclipse.jgit.errors.LockFailedException} . This method does 85 * not validate that the lock is or is not currently held before attempting 86 * to unlock it. 87 * 88 * @param file 89 * a {@link java.io.File} object. 90 * @return true if unlocked, false if unlocking failed 91 */ 92 public static boolean unlock(File file) { 93 final File lockFile = getLockFile(file); 94 final int flags = FileUtils.RETRY | FileUtils.SKIP_MISSING; 95 try { 96 FileUtils.delete(lockFile, flags); 97 } catch (IOException ignored) { 98 // Ignore and return whether lock file still exists 99 } 100 return !lockFile.exists(); 101 } 102 103 /** 104 * Get the lock file corresponding to the given file. 105 * 106 * @param file 107 * @return lock file 108 */ 109 static File getLockFile(File file) { 110 return new File(file.getParentFile(), 111 file.getName() + LOCK_SUFFIX); 112 } 113 114 /** Filter to skip over active lock files when listing a directory. */ 115 static final FilenameFilter FILTER = new FilenameFilter() { 116 @Override 117 public boolean accept(File dir, String name) { 118 return !name.endsWith(LOCK_SUFFIX); 119 } 120 }; 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 /** 137 * Create a new lock for any file. 138 * 139 * @param f 140 * the file that will be locked. 141 */ 142 public LockFile(File f) { 143 ref = f; 144 lck = getLockFile(ref); 145 } 146 147 /** 148 * Try to establish the lock. 149 * 150 * @return true if the lock is now held by the caller; false if it is held 151 * by someone else. 152 * @throws java.io.IOException 153 * the temporary output file could not be created. The caller 154 * does not hold the lock. 155 */ 156 public boolean lock() throws IOException { 157 FileUtils.mkdirs(lck.getParentFile(), true); 158 if (FS.DETECTED.createNewFile(lck)) { 159 haveLck = true; 160 try { 161 os = new FileOutputStream(lck); 162 } catch (IOException ioe) { 163 unlock(); 164 throw ioe; 165 } 166 } 167 return haveLck; 168 } 169 170 /** 171 * Try to establish the lock for appending. 172 * 173 * @return true if the lock is now held by the caller; false if it is held 174 * by someone else. 175 * @throws java.io.IOException 176 * the temporary output file could not be created. The caller 177 * does not hold the lock. 178 */ 179 public boolean lockForAppend() throws IOException { 180 if (!lock()) 181 return false; 182 copyCurrentContent(); 183 return true; 184 } 185 186 /** 187 * Copy the current file content into the temporary file. 188 * <p> 189 * This method saves the current file content by inserting it into the 190 * temporary file, so that the caller can safely append rather than replace 191 * the primary file. 192 * <p> 193 * This method does nothing if the current file does not exist, or exists 194 * but is empty. 195 * 196 * @throws java.io.IOException 197 * the temporary file could not be written, or a read error 198 * occurred while reading from the current file. The lock is 199 * released before throwing the underlying IO exception to the 200 * caller. 201 * @throws java.lang.RuntimeException 202 * the temporary file could not be written. The lock is released 203 * before throwing the underlying exception to the caller. 204 */ 205 public void copyCurrentContent() throws IOException { 206 requireLock(); 207 try { 208 try (FileInputStream fis = new FileInputStream(ref)) { 209 if (fsync) { 210 FileChannel in = fis.getChannel(); 211 long pos = 0; 212 long cnt = in.size(); 213 while (0 < cnt) { 214 long r = os.getChannel().transferFrom(in, pos, cnt); 215 pos += r; 216 cnt -= r; 217 } 218 } else { 219 final byte[] buf = new byte[2048]; 220 int r; 221 while ((r = fis.read(buf)) >= 0) 222 os.write(buf, 0, r); 223 } 224 } 225 } catch (FileNotFoundException fnfe) { 226 if (ref.exists()) { 227 unlock(); 228 throw fnfe; 229 } 230 // Don't worry about a file that doesn't exist yet, it 231 // conceptually has no current content to copy. 232 // 233 } catch (IOException ioe) { 234 unlock(); 235 throw ioe; 236 } catch (RuntimeException ioe) { 237 unlock(); 238 throw ioe; 239 } catch (Error ioe) { 240 unlock(); 241 throw ioe; 242 } 243 } 244 245 /** 246 * Write an ObjectId and LF to the temporary file. 247 * 248 * @param id 249 * the id to store in the file. The id will be written in hex, 250 * followed by a sole LF. 251 * @throws java.io.IOException 252 * the temporary file could not be written. The lock is released 253 * before throwing the underlying IO exception to the caller. 254 * @throws java.lang.RuntimeException 255 * the temporary file could not be written. The lock is released 256 * before throwing the underlying exception to the caller. 257 */ 258 public void write(ObjectId id) throws IOException { 259 byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1]; 260 id.copyTo(buf, 0); 261 buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n'; 262 write(buf); 263 } 264 265 /** 266 * Write arbitrary data to the temporary file. 267 * 268 * @param content 269 * the bytes to store in the temporary file. No additional bytes 270 * are added, so if the file must end with an LF it must appear 271 * at the end of the byte array. 272 * @throws java.io.IOException 273 * the temporary file could not be written. The lock is released 274 * before throwing the underlying IO exception to the caller. 275 * @throws java.lang.RuntimeException 276 * the temporary file could not be written. The lock is released 277 * before throwing the underlying exception to the caller. 278 */ 279 public void write(byte[] content) throws IOException { 280 requireLock(); 281 try { 282 if (fsync) { 283 FileChannel fc = os.getChannel(); 284 ByteBuffer buf = ByteBuffer.wrap(content); 285 while (0 < buf.remaining()) 286 fc.write(buf); 287 fc.force(true); 288 } else { 289 os.write(content); 290 } 291 os.close(); 292 os = null; 293 } catch (IOException ioe) { 294 unlock(); 295 throw ioe; 296 } catch (RuntimeException ioe) { 297 unlock(); 298 throw ioe; 299 } catch (Error ioe) { 300 unlock(); 301 throw ioe; 302 } 303 } 304 305 /** 306 * Obtain the direct output stream for this lock. 307 * <p> 308 * The stream may only be accessed once, and only after {@link #lock()} has 309 * been successfully invoked and returned true. Callers must close the 310 * stream prior to calling {@link #commit()} to commit the change. 311 * 312 * @return a stream to write to the new file. The stream is unbuffered. 313 */ 314 public OutputStream getOutputStream() { 315 requireLock(); 316 317 final OutputStream out; 318 if (fsync) 319 out = Channels.newOutputStream(os.getChannel()); 320 else 321 out = os; 322 323 return new OutputStream() { 324 @Override 325 public void write(byte[] b, int o, int n) 326 throws IOException { 327 out.write(b, o, n); 328 } 329 330 @Override 331 public void write(byte[] b) throws IOException { 332 out.write(b); 333 } 334 335 @Override 336 public void write(int b) throws IOException { 337 out.write(b); 338 } 339 340 @Override 341 public void close() throws IOException { 342 try { 343 if (fsync) 344 os.getChannel().force(true); 345 out.close(); 346 os = null; 347 } catch (IOException ioe) { 348 unlock(); 349 throw ioe; 350 } catch (RuntimeException ioe) { 351 unlock(); 352 throw ioe; 353 } catch (Error ioe) { 354 unlock(); 355 throw ioe; 356 } 357 } 358 }; 359 } 360 361 void requireLock() { 362 if (os == null) { 363 unlock(); 364 throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref)); 365 } 366 } 367 368 /** 369 * Request that {@link #commit()} remember modification time. 370 * <p> 371 * This is an alias for {@code setNeedSnapshot(true)}. 372 * 373 * @param on 374 * true if the commit method must remember the modification time. 375 */ 376 public void setNeedStatInformation(boolean on) { 377 setNeedSnapshot(on); 378 } 379 380 /** 381 * Request that {@link #commit()} remember the 382 * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot}. 383 * 384 * @param on 385 * true if the commit method must remember the FileSnapshot. 386 */ 387 public void setNeedSnapshot(boolean on) { 388 needSnapshot = on; 389 } 390 391 /** 392 * Request that {@link #commit()} force dirty data to the drive. 393 * 394 * @param on 395 * true if dirty data should be forced to the drive. 396 */ 397 public void setFSync(boolean on) { 398 fsync = on; 399 } 400 401 /** 402 * Wait until the lock file information differs from the old file. 403 * <p> 404 * This method tests the last modification date. If both are the same, this 405 * method sleeps until it can force the new lock file's modification date to 406 * be later than the target file. 407 * 408 * @throws java.lang.InterruptedException 409 * the thread was interrupted before the last modified date of 410 * the lock file was different from the last modified date of 411 * the target file. 412 */ 413 public void waitForStatChange() throws InterruptedException { 414 FileSnapshot o = FileSnapshot.save(ref); 415 FileSnapshot n = FileSnapshot.save(lck); 416 while (o.equals(n)) { 417 Thread.sleep(25 /* milliseconds */); 418 lck.setLastModified(System.currentTimeMillis()); 419 n = FileSnapshot.save(lck); 420 } 421 } 422 423 /** 424 * Commit this change and release the lock. 425 * <p> 426 * If this method fails (returns false) the lock is still released. 427 * 428 * @return true if the commit was successful and the file contains the new 429 * data; false if the commit failed and the file remains with the 430 * old data. 431 * @throws java.lang.IllegalStateException 432 * the lock is not held. 433 */ 434 public boolean commit() { 435 if (os != null) { 436 unlock(); 437 throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref)); 438 } 439 440 saveStatInformation(); 441 try { 442 FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE); 443 haveLck = false; 444 return true; 445 } catch (IOException e) { 446 unlock(); 447 return false; 448 } 449 } 450 451 private void saveStatInformation() { 452 if (needSnapshot) 453 commitSnapshot = FileSnapshot.save(lck); 454 } 455 456 /** 457 * Get the modification time of the output file when it was committed. 458 * 459 * @return modification time of the lock file right before we committed it. 460 */ 461 public long getCommitLastModified() { 462 return commitSnapshot.lastModified(); 463 } 464 465 /** 466 * Get the {@link FileSnapshot} just before commit. 467 * 468 * @return get the {@link FileSnapshot} just before commit. 469 */ 470 public FileSnapshot getCommitSnapshot() { 471 return commitSnapshot; 472 } 473 474 /** 475 * Update the commit snapshot {@link #getCommitSnapshot()} before commit. 476 * <p> 477 * This may be necessary if you need time stamp before commit occurs, e.g 478 * while writing the index. 479 */ 480 public void createCommitSnapshot() { 481 saveStatInformation(); 482 } 483 484 /** 485 * Unlock this file and abort this change. 486 * <p> 487 * The temporary file (if created) is deleted before returning. 488 */ 489 public void unlock() { 490 if (os != null) { 491 try { 492 os.close(); 493 } catch (IOException ioe) { 494 // Ignore this 495 } 496 os = null; 497 } 498 499 if (haveLck) { 500 haveLck = false; 501 try { 502 FileUtils.delete(lck, FileUtils.RETRY); 503 } catch (IOException e) { 504 // couldn't delete the file even after retry. 505 } 506 } 507 } 508 509 /** {@inheritDoc} */ 510 @SuppressWarnings("nls") 511 @Override 512 public String toString() { 513 return "LockFile[" + lck + ", haveLck=" + haveLck + "]"; 514 } 515 }