View Javadoc
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  
12  package org.eclipse.jgit.internal.storage.file;
13  
14  import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX;
15  
16  import java.io.File;
17  import java.io.FileInputStream;
18  import java.io.FileNotFoundException;
19  import java.io.FileOutputStream;
20  import java.io.FilenameFilter;
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.nio.ByteBuffer;
24  import java.nio.channels.Channels;
25  import java.nio.channels.FileChannel;
26  import java.nio.file.Files;
27  import java.nio.file.StandardCopyOption;
28  import java.nio.file.attribute.FileTime;
29  import java.text.MessageFormat;
30  import java.time.Instant;
31  import java.util.concurrent.TimeUnit;
32  
33  import org.eclipse.jgit.internal.JGitText;
34  import org.eclipse.jgit.lib.Constants;
35  import org.eclipse.jgit.lib.ObjectId;
36  import org.eclipse.jgit.util.FS;
37  import org.eclipse.jgit.util.FS.LockToken;
38  import org.eclipse.jgit.util.FileUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Git style file locking and replacement.
44   * <p>
45   * To modify a ref file Git tries to use an atomic update approach: we write the
46   * new data into a brand new file, then rename it in place over the old name.
47   * This way we can just delete the temporary file if anything goes wrong, and
48   * nothing has been damaged. To coordinate access from multiple processes at
49   * once Git tries to atomically create the new temporary file under a well-known
50   * name.
51   */
52  public class LockFile {
53  	private static final Logger LOG = LoggerFactory.getLogger(LockFile.class);
54  
55  	/**
56  	 * Unlock the given file.
57  	 * <p>
58  	 * This method can be used for recovering from a thrown
59  	 * {@link org.eclipse.jgit.errors.LockFailedException} . This method does
60  	 * not validate that the lock is or is not currently held before attempting
61  	 * to unlock it.
62  	 *
63  	 * @param file
64  	 *            a {@link java.io.File} object.
65  	 * @return true if unlocked, false if unlocking failed
66  	 */
67  	public static boolean unlock(File file) {
68  		final File lockFile = getLockFile(file);
69  		final int flags = FileUtils.RETRY | FileUtils.SKIP_MISSING;
70  		try {
71  			FileUtils.delete(lockFile, flags);
72  		} catch (IOException ignored) {
73  			// Ignore and return whether lock file still exists
74  		}
75  		return !lockFile.exists();
76  	}
77  
78  	/**
79  	 * Get the lock file corresponding to the given file.
80  	 *
81  	 * @param file
82  	 * @return lock file
83  	 */
84  	static File getLockFile(File file) {
85  		return new File(file.getParentFile(),
86  				file.getName() + LOCK_SUFFIX);
87  	}
88  
89  	/** Filter to skip over active lock files when listing a directory. */
90  	static final FilenameFilter FILTER = (File dir,
91  			String name) -> !name.endsWith(LOCK_SUFFIX);
92  
93  	private final File ref;
94  
95  	private final File lck;
96  
97  	private boolean haveLck;
98  
99  	private FileOutputStream os;
100 
101 	private boolean needSnapshot;
102 
103 	private boolean fsync;
104 
105 	private boolean isAppend;
106 
107 	private boolean written;
108 
109 	private boolean snapshotNoConfig;
110 
111 	private FileSnapshot commitSnapshot;
112 
113 	private LockToken token;
114 
115 	/**
116 	 * Create a new lock for any file.
117 	 *
118 	 * @param f
119 	 *            the file that will be locked.
120 	 */
121 	public LockFile(File f) {
122 		ref = f;
123 		lck = getLockFile(ref);
124 	}
125 
126 	/**
127 	 * Try to establish the lock.
128 	 *
129 	 * @return true if the lock is now held by the caller; false if it is held
130 	 *         by someone else.
131 	 * @throws java.io.IOException
132 	 *             the temporary output file could not be created. The caller
133 	 *             does not hold the lock.
134 	 */
135 	public boolean lock() throws IOException {
136 		if (haveLck) {
137 			throw new IllegalStateException(
138 					MessageFormat.format(JGitText.get().lockAlreadyHeld, ref));
139 		}
140 		FileUtils.mkdirs(lck.getParentFile(), true);
141 		try {
142 			token = FS.DETECTED.createNewFileAtomic(lck);
143 		} catch (IOException e) {
144 			LOG.error(JGitText.get().failedCreateLockFile, lck, e);
145 			throw e;
146 		}
147 		boolean obtainedLock = token.isCreated();
148 		if (obtainedLock) {
149 			haveLck = true;
150 			isAppend = false;
151 			written = false;
152 		} else {
153 			closeToken();
154 		}
155 		return obtainedLock;
156 	}
157 
158 	/**
159 	 * Try to establish the lock for appending.
160 	 *
161 	 * @return true if the lock is now held by the caller; false if it is held
162 	 *         by someone else.
163 	 * @throws java.io.IOException
164 	 *             the temporary output file could not be created. The caller
165 	 *             does not hold the lock.
166 	 */
167 	public boolean lockForAppend() throws IOException {
168 		if (!lock()) {
169 			return false;
170 		}
171 		copyCurrentContent();
172 		isAppend = true;
173 		written = false;
174 		return true;
175 	}
176 
177 	// For tests only
178 	boolean isLocked() {
179 		return haveLck;
180 	}
181 
182 	private FileOutputStream getStream() throws IOException {
183 		return new FileOutputStream(lck, isAppend);
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 (FileOutputStream out = getStream()) {
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 = out.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 						out.write(buf, 0, r);
223 					}
224 				}
225 			} catch (FileNotFoundException fnfe) {
226 				if (ref.exists()) {
227 					throw fnfe;
228 				}
229 				// Don't worry about a file that doesn't exist yet, it
230 				// conceptually has no current content to copy.
231 			}
232 		} catch (IOException | RuntimeException | Error ioe) {
233 			unlock();
234 			throw ioe;
235 		}
236 	}
237 
238 	/**
239 	 * Write an ObjectId and LF to the temporary file.
240 	 *
241 	 * @param id
242 	 *            the id to store in the file. The id will be written in hex,
243 	 *            followed by a sole LF.
244 	 * @throws java.io.IOException
245 	 *             the temporary file could not be written. The lock is released
246 	 *             before throwing the underlying IO exception to the caller.
247 	 * @throws java.lang.RuntimeException
248 	 *             the temporary file could not be written. The lock is released
249 	 *             before throwing the underlying exception to the caller.
250 	 */
251 	public void write(ObjectId id) throws IOException {
252 		byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1];
253 		id.copyTo(buf, 0);
254 		buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n';
255 		write(buf);
256 	}
257 
258 	/**
259 	 * Write arbitrary data to the temporary file.
260 	 *
261 	 * @param content
262 	 *            the bytes to store in the temporary file. No additional bytes
263 	 *            are added, so if the file must end with an LF it must appear
264 	 *            at the end of the byte array.
265 	 * @throws java.io.IOException
266 	 *             the temporary file could not be written. The lock is released
267 	 *             before throwing the underlying IO exception to the caller.
268 	 * @throws java.lang.RuntimeException
269 	 *             the temporary file could not be written. The lock is released
270 	 *             before throwing the underlying exception to the caller.
271 	 */
272 	public void write(byte[] content) throws IOException {
273 		requireLock();
274 		try (FileOutputStream out = getStream()) {
275 			if (written) {
276 				throw new IOException(MessageFormat
277 						.format(JGitText.get().lockStreamClosed, ref));
278 			}
279 			if (fsync) {
280 				FileChannel fc = out.getChannel();
281 				ByteBuffer buf = ByteBuffer.wrap(content);
282 				while (0 < buf.remaining()) {
283 					fc.write(buf);
284 				}
285 				fc.force(true);
286 			} else {
287 				out.write(content);
288 			}
289 			written = true;
290 		} catch (IOException | RuntimeException | Error ioe) {
291 			unlock();
292 			throw ioe;
293 		}
294 	}
295 
296 	/**
297 	 * Obtain the direct output stream for this lock.
298 	 * <p>
299 	 * The stream may only be accessed once, and only after {@link #lock()} has
300 	 * been successfully invoked and returned true. Callers must close the
301 	 * stream prior to calling {@link #commit()} to commit the change.
302 	 *
303 	 * @return a stream to write to the new file. The stream is unbuffered.
304 	 */
305 	public OutputStream getOutputStream() {
306 		requireLock();
307 
308 		if (written || os != null) {
309 			throw new IllegalStateException(MessageFormat
310 					.format(JGitText.get().lockStreamMultiple, ref));
311 		}
312 
313 		return new OutputStream() {
314 
315 			private OutputStream out;
316 
317 			private boolean closed;
318 
319 			private OutputStream get() throws IOException {
320 				if (written) {
321 					throw new IOException(MessageFormat
322 							.format(JGitText.get().lockStreamMultiple, ref));
323 				}
324 				if (out == null) {
325 					os = getStream();
326 					if (fsync) {
327 						out = Channels.newOutputStream(os.getChannel());
328 					} else {
329 						out = os;
330 					}
331 				}
332 				return out;
333 			}
334 
335 			@Override
336 			public void write(byte[] b, int o, int n) throws IOException {
337 				get().write(b, o, n);
338 			}
339 
340 			@Override
341 			public void write(byte[] b) throws IOException {
342 				get().write(b);
343 			}
344 
345 			@Override
346 			public void write(int b) throws IOException {
347 				get().write(b);
348 			}
349 
350 			@Override
351 			public void close() throws IOException {
352 				if (closed) {
353 					return;
354 				}
355 				closed = true;
356 				try {
357 					if (written) {
358 						throw new IOException(MessageFormat
359 								.format(JGitText.get().lockStreamClosed, ref));
360 					}
361 					if (out != null) {
362 						if (fsync) {
363 							os.getChannel().force(true);
364 						}
365 						out.close();
366 						os = null;
367 					}
368 					written = true;
369 				} catch (IOException | RuntimeException | Error ioe) {
370 					unlock();
371 					throw ioe;
372 				}
373 			}
374 		};
375 	}
376 
377 	void requireLock() {
378 		if (!haveLck) {
379 			unlock();
380 			throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref));
381 		}
382 	}
383 
384 	/**
385 	 * Request that {@link #commit()} remember modification time.
386 	 * <p>
387 	 * This is an alias for {@code setNeedSnapshot(true)}.
388 	 *
389 	 * @param on
390 	 *            true if the commit method must remember the modification time.
391 	 */
392 	public void setNeedStatInformation(boolean on) {
393 		setNeedSnapshot(on);
394 	}
395 
396 	/**
397 	 * Request that {@link #commit()} remember the
398 	 * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot}.
399 	 *
400 	 * @param on
401 	 *            true if the commit method must remember the FileSnapshot.
402 	 */
403 	public void setNeedSnapshot(boolean on) {
404 		needSnapshot = on;
405 	}
406 
407 	/**
408 	 * Request that {@link #commit()} remember the
409 	 * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot} without using
410 	 * config file to get filesystem timestamp resolution.
411 	 * This method should be invoked before the file is accessed.
412 	 * It is used by FileBasedConfig to avoid endless recursion.
413 	 *
414 	 * @param on
415 	 *            true if the commit method must remember the FileSnapshot.
416 	 */
417 	public void setNeedSnapshotNoConfig(boolean on) {
418 		needSnapshot = on;
419 		snapshotNoConfig = on;
420 	}
421 
422 	/**
423 	 * Request that {@link #commit()} force dirty data to the drive.
424 	 *
425 	 * @param on
426 	 *            true if dirty data should be forced to the drive.
427 	 */
428 	public void setFSync(boolean on) {
429 		fsync = on;
430 	}
431 
432 	/**
433 	 * Wait until the lock file information differs from the old file.
434 	 * <p>
435 	 * This method tests the last modification date. If both are the same, this
436 	 * method sleeps until it can force the new lock file's modification date to
437 	 * be later than the target file.
438 	 *
439 	 * @throws java.lang.InterruptedException
440 	 *             the thread was interrupted before the last modified date of
441 	 *             the lock file was different from the last modified date of
442 	 *             the target file.
443 	 */
444 	public void waitForStatChange() throws InterruptedException {
445 		FileSnapshot o = FileSnapshot.save(ref);
446 		FileSnapshot n = FileSnapshot.save(lck);
447 		long fsTimeResolution = FS.getFileStoreAttributes(lck.toPath())
448 				.getFsTimestampResolution().toNanos();
449 		while (o.equals(n)) {
450 			TimeUnit.NANOSECONDS.sleep(fsTimeResolution);
451 			try {
452 				Files.setLastModifiedTime(lck.toPath(),
453 						FileTime.from(Instant.now()));
454 			} catch (IOException e) {
455 				n.waitUntilNotRacy();
456 			}
457 			n = FileSnapshot.save(lck);
458 		}
459 	}
460 
461 	/**
462 	 * Commit this change and release the lock.
463 	 * <p>
464 	 * If this method fails (returns false) the lock is still released.
465 	 *
466 	 * @return true if the commit was successful and the file contains the new
467 	 *         data; false if the commit failed and the file remains with the
468 	 *         old data.
469 	 * @throws java.lang.IllegalStateException
470 	 *             the lock is not held.
471 	 */
472 	public boolean commit() {
473 		if (os != null) {
474 			unlock();
475 			throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref));
476 		}
477 
478 		saveStatInformation();
479 		try {
480 			FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE);
481 			haveLck = false;
482 			isAppend = false;
483 			written = false;
484 			closeToken();
485 			return true;
486 		} catch (IOException e) {
487 			unlock();
488 			return false;
489 		}
490 	}
491 
492 	private void closeToken() {
493 		if (token != null) {
494 			token.close();
495 			token = null;
496 		}
497 	}
498 
499 	private void saveStatInformation() {
500 		if (needSnapshot) {
501 			commitSnapshot = snapshotNoConfig ?
502 				// don't use config in this snapshot to avoid endless recursion
503 				FileSnapshot.saveNoConfig(lck)
504 				: FileSnapshot.save(lck);
505 		}
506 	}
507 
508 	/**
509 	 * Get the modification time of the output file when it was committed.
510 	 *
511 	 * @return modification time of the lock file right before we committed it.
512 	 * @deprecated use {@link #getCommitLastModifiedInstant()} instead
513 	 */
514 	@Deprecated
515 	public long getCommitLastModified() {
516 		return commitSnapshot.lastModified();
517 	}
518 
519 	/**
520 	 * Get the modification time of the output file when it was committed.
521 	 *
522 	 * @return modification time of the lock file right before we committed it.
523 	 */
524 	public Instant getCommitLastModifiedInstant() {
525 		return commitSnapshot.lastModifiedInstant();
526 	}
527 
528 	/**
529 	 * Get the {@link FileSnapshot} just before commit.
530 	 *
531 	 * @return get the {@link FileSnapshot} just before commit.
532 	 */
533 	public FileSnapshot getCommitSnapshot() {
534 		return commitSnapshot;
535 	}
536 
537 	/**
538 	 * Update the commit snapshot {@link #getCommitSnapshot()} before commit.
539 	 * <p>
540 	 * This may be necessary if you need time stamp before commit occurs, e.g
541 	 * while writing the index.
542 	 */
543 	public void createCommitSnapshot() {
544 		saveStatInformation();
545 	}
546 
547 	/**
548 	 * Unlock this file and abort this change.
549 	 * <p>
550 	 * The temporary file (if created) is deleted before returning.
551 	 */
552 	public void unlock() {
553 		if (os != null) {
554 			try {
555 				os.close();
556 			} catch (IOException e) {
557 				LOG.error(MessageFormat
558 						.format(JGitText.get().unlockLockFileFailed, lck), e);
559 			}
560 			os = null;
561 		}
562 
563 		if (haveLck) {
564 			haveLck = false;
565 			try {
566 				FileUtils.delete(lck, FileUtils.RETRY);
567 			} catch (IOException e) {
568 				LOG.error(MessageFormat
569 						.format(JGitText.get().unlockLockFileFailed, lck), e);
570 			} finally {
571 				closeToken();
572 			}
573 		}
574 		isAppend = false;
575 		written = false;
576 	}
577 
578 	/** {@inheritDoc} */
579 	@SuppressWarnings("nls")
580 	@Override
581 	public String toString() {
582 		return "LockFile[" + lck + ", haveLck=" + haveLck + "]";
583 	}
584 }