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 FileSnapshot commitSnapshot;
110 
111 	private LockToken token;
112 
113 	/**
114 	 * Create a new lock for any file.
115 	 *
116 	 * @param f
117 	 *            the file that will be locked.
118 	 */
119 	public LockFile(File f) {
120 		ref = f;
121 		lck = getLockFile(ref);
122 	}
123 
124 	/**
125 	 * Try to establish the lock.
126 	 *
127 	 * @return true if the lock is now held by the caller; false if it is held
128 	 *         by someone else.
129 	 * @throws java.io.IOException
130 	 *             the temporary output file could not be created. The caller
131 	 *             does not hold the lock.
132 	 */
133 	public boolean lock() throws IOException {
134 		if (haveLck) {
135 			throw new IllegalStateException(
136 					MessageFormat.format(JGitText.get().lockAlreadyHeld, ref));
137 		}
138 		FileUtils.mkdirs(lck.getParentFile(), true);
139 		try {
140 			token = FS.DETECTED.createNewFileAtomic(lck);
141 		} catch (IOException e) {
142 			LOG.error(JGitText.get().failedCreateLockFile, lck, e);
143 			throw e;
144 		}
145 		boolean obtainedLock = token.isCreated();
146 		if (obtainedLock) {
147 			haveLck = true;
148 			isAppend = false;
149 			written = false;
150 		} else {
151 			closeToken();
152 		}
153 		return obtainedLock;
154 	}
155 
156 	/**
157 	 * Try to establish the lock for appending.
158 	 *
159 	 * @return true if the lock is now held by the caller; false if it is held
160 	 *         by someone else.
161 	 * @throws java.io.IOException
162 	 *             the temporary output file could not be created. The caller
163 	 *             does not hold the lock.
164 	 */
165 	public boolean lockForAppend() throws IOException {
166 		if (!lock()) {
167 			return false;
168 		}
169 		copyCurrentContent();
170 		isAppend = true;
171 		written = false;
172 		return true;
173 	}
174 
175 	// For tests only
176 	boolean isLocked() {
177 		return haveLck;
178 	}
179 
180 	private FileOutputStream getStream() throws IOException {
181 		return new FileOutputStream(lck, isAppend);
182 	}
183 
184 	/**
185 	 * Copy the current file content into the temporary file.
186 	 * <p>
187 	 * This method saves the current file content by inserting it into the
188 	 * temporary file, so that the caller can safely append rather than replace
189 	 * the primary file.
190 	 * <p>
191 	 * This method does nothing if the current file does not exist, or exists
192 	 * but is empty.
193 	 *
194 	 * @throws java.io.IOException
195 	 *             the temporary file could not be written, or a read error
196 	 *             occurred while reading from the current file. The lock is
197 	 *             released before throwing the underlying IO exception to the
198 	 *             caller.
199 	 * @throws java.lang.RuntimeException
200 	 *             the temporary file could not be written. The lock is released
201 	 *             before throwing the underlying exception to the caller.
202 	 */
203 	public void copyCurrentContent() throws IOException {
204 		requireLock();
205 		try (FileOutputStream out = getStream()) {
206 			try (FileInputStream fis = new FileInputStream(ref)) {
207 				if (fsync) {
208 					FileChannel in = fis.getChannel();
209 					long pos = 0;
210 					long cnt = in.size();
211 					while (0 < cnt) {
212 						long r = out.getChannel().transferFrom(in, pos, cnt);
213 						pos += r;
214 						cnt -= r;
215 					}
216 				} else {
217 					final byte[] buf = new byte[2048];
218 					int r;
219 					while ((r = fis.read(buf)) >= 0) {
220 						out.write(buf, 0, r);
221 					}
222 				}
223 			} catch (FileNotFoundException fnfe) {
224 				if (ref.exists()) {
225 					throw fnfe;
226 				}
227 				// Don't worry about a file that doesn't exist yet, it
228 				// conceptually has no current content to copy.
229 			}
230 		} catch (IOException | RuntimeException | Error ioe) {
231 			unlock();
232 			throw ioe;
233 		}
234 	}
235 
236 	/**
237 	 * Write an ObjectId and LF to the temporary file.
238 	 *
239 	 * @param id
240 	 *            the id to store in the file. The id will be written in hex,
241 	 *            followed by a sole LF.
242 	 * @throws java.io.IOException
243 	 *             the temporary file could not be written. The lock is released
244 	 *             before throwing the underlying IO exception to the caller.
245 	 * @throws java.lang.RuntimeException
246 	 *             the temporary file could not be written. The lock is released
247 	 *             before throwing the underlying exception to the caller.
248 	 */
249 	public void write(ObjectId id) throws IOException {
250 		byte[] buf = new byte[Constants.OBJECT_ID_STRING_LENGTH + 1];
251 		id.copyTo(buf, 0);
252 		buf[Constants.OBJECT_ID_STRING_LENGTH] = '\n';
253 		write(buf);
254 	}
255 
256 	/**
257 	 * Write arbitrary data to the temporary file.
258 	 *
259 	 * @param content
260 	 *            the bytes to store in the temporary file. No additional bytes
261 	 *            are added, so if the file must end with an LF it must appear
262 	 *            at the end of the byte array.
263 	 * @throws java.io.IOException
264 	 *             the temporary file could not be written. The lock is released
265 	 *             before throwing the underlying IO exception to the caller.
266 	 * @throws java.lang.RuntimeException
267 	 *             the temporary file could not be written. The lock is released
268 	 *             before throwing the underlying exception to the caller.
269 	 */
270 	public void write(byte[] content) throws IOException {
271 		requireLock();
272 		try (FileOutputStream out = getStream()) {
273 			if (written) {
274 				throw new IOException(MessageFormat
275 						.format(JGitText.get().lockStreamClosed, ref));
276 			}
277 			if (fsync) {
278 				FileChannel fc = out.getChannel();
279 				ByteBuffer buf = ByteBuffer.wrap(content);
280 				while (0 < buf.remaining()) {
281 					fc.write(buf);
282 				}
283 				fc.force(true);
284 			} else {
285 				out.write(content);
286 			}
287 			written = true;
288 		} catch (IOException | RuntimeException | Error ioe) {
289 			unlock();
290 			throw ioe;
291 		}
292 	}
293 
294 	/**
295 	 * Obtain the direct output stream for this lock.
296 	 * <p>
297 	 * The stream may only be accessed once, and only after {@link #lock()} has
298 	 * been successfully invoked and returned true. Callers must close the
299 	 * stream prior to calling {@link #commit()} to commit the change.
300 	 *
301 	 * @return a stream to write to the new file. The stream is unbuffered.
302 	 */
303 	public OutputStream getOutputStream() {
304 		requireLock();
305 
306 		if (written || os != null) {
307 			throw new IllegalStateException(MessageFormat
308 					.format(JGitText.get().lockStreamMultiple, ref));
309 		}
310 
311 		return new OutputStream() {
312 
313 			private OutputStream out;
314 
315 			private boolean closed;
316 
317 			private OutputStream get() throws IOException {
318 				if (written) {
319 					throw new IOException(MessageFormat
320 							.format(JGitText.get().lockStreamMultiple, ref));
321 				}
322 				if (out == null) {
323 					os = getStream();
324 					if (fsync) {
325 						out = Channels.newOutputStream(os.getChannel());
326 					} else {
327 						out = os;
328 					}
329 				}
330 				return out;
331 			}
332 
333 			@Override
334 			public void write(byte[] b, int o, int n) throws IOException {
335 				get().write(b, o, n);
336 			}
337 
338 			@Override
339 			public void write(byte[] b) throws IOException {
340 				get().write(b);
341 			}
342 
343 			@Override
344 			public void write(int b) throws IOException {
345 				get().write(b);
346 			}
347 
348 			@Override
349 			public void close() throws IOException {
350 				if (closed) {
351 					return;
352 				}
353 				closed = true;
354 				try {
355 					if (written) {
356 						throw new IOException(MessageFormat
357 								.format(JGitText.get().lockStreamClosed, ref));
358 					}
359 					if (out != null) {
360 						if (fsync) {
361 							os.getChannel().force(true);
362 						}
363 						out.close();
364 						os = null;
365 					}
366 					written = true;
367 				} catch (IOException | RuntimeException | Error ioe) {
368 					unlock();
369 					throw ioe;
370 				}
371 			}
372 		};
373 	}
374 
375 	void requireLock() {
376 		if (!haveLck) {
377 			unlock();
378 			throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref));
379 		}
380 	}
381 
382 	/**
383 	 * Request that {@link #commit()} remember modification time.
384 	 * <p>
385 	 * This is an alias for {@code setNeedSnapshot(true)}.
386 	 *
387 	 * @param on
388 	 *            true if the commit method must remember the modification time.
389 	 */
390 	public void setNeedStatInformation(boolean on) {
391 		setNeedSnapshot(on);
392 	}
393 
394 	/**
395 	 * Request that {@link #commit()} remember the
396 	 * {@link org.eclipse.jgit.internal.storage.file.FileSnapshot}.
397 	 *
398 	 * @param on
399 	 *            true if the commit method must remember the FileSnapshot.
400 	 */
401 	public void setNeedSnapshot(boolean on) {
402 		needSnapshot = on;
403 	}
404 
405 	/**
406 	 * Request that {@link #commit()} force dirty data to the drive.
407 	 *
408 	 * @param on
409 	 *            true if dirty data should be forced to the drive.
410 	 */
411 	public void setFSync(boolean on) {
412 		fsync = on;
413 	}
414 
415 	/**
416 	 * Wait until the lock file information differs from the old file.
417 	 * <p>
418 	 * This method tests the last modification date. If both are the same, this
419 	 * method sleeps until it can force the new lock file's modification date to
420 	 * be later than the target file.
421 	 *
422 	 * @throws java.lang.InterruptedException
423 	 *             the thread was interrupted before the last modified date of
424 	 *             the lock file was different from the last modified date of
425 	 *             the target file.
426 	 */
427 	public void waitForStatChange() throws InterruptedException {
428 		FileSnapshot o = FileSnapshot.save(ref);
429 		FileSnapshot n = FileSnapshot.save(lck);
430 		long fsTimeResolution = FS.getFileStoreAttributes(lck.toPath())
431 				.getFsTimestampResolution().toNanos();
432 		while (o.equals(n)) {
433 			TimeUnit.NANOSECONDS.sleep(fsTimeResolution);
434 			try {
435 				Files.setLastModifiedTime(lck.toPath(),
436 						FileTime.from(Instant.now()));
437 			} catch (IOException e) {
438 				n.waitUntilNotRacy();
439 			}
440 			n = FileSnapshot.save(lck);
441 		}
442 	}
443 
444 	/**
445 	 * Commit this change and release the lock.
446 	 * <p>
447 	 * If this method fails (returns false) the lock is still released.
448 	 *
449 	 * @return true if the commit was successful and the file contains the new
450 	 *         data; false if the commit failed and the file remains with the
451 	 *         old data.
452 	 * @throws java.lang.IllegalStateException
453 	 *             the lock is not held.
454 	 */
455 	public boolean commit() {
456 		if (os != null) {
457 			unlock();
458 			throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref));
459 		}
460 
461 		saveStatInformation();
462 		try {
463 			FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE);
464 			haveLck = false;
465 			isAppend = false;
466 			written = false;
467 			closeToken();
468 			return true;
469 		} catch (IOException e) {
470 			unlock();
471 			return false;
472 		}
473 	}
474 
475 	private void closeToken() {
476 		if (token != null) {
477 			token.close();
478 			token = null;
479 		}
480 	}
481 
482 	private void saveStatInformation() {
483 		if (needSnapshot)
484 			commitSnapshot = FileSnapshot.save(lck);
485 	}
486 
487 	/**
488 	 * Get the modification time of the output file when it was committed.
489 	 *
490 	 * @return modification time of the lock file right before we committed it.
491 	 * @deprecated use {@link #getCommitLastModifiedInstant()} instead
492 	 */
493 	@Deprecated
494 	public long getCommitLastModified() {
495 		return commitSnapshot.lastModified();
496 	}
497 
498 	/**
499 	 * Get the modification time of the output file when it was committed.
500 	 *
501 	 * @return modification time of the lock file right before we committed it.
502 	 */
503 	public Instant getCommitLastModifiedInstant() {
504 		return commitSnapshot.lastModifiedInstant();
505 	}
506 
507 	/**
508 	 * Get the {@link FileSnapshot} just before commit.
509 	 *
510 	 * @return get the {@link FileSnapshot} just before commit.
511 	 */
512 	public FileSnapshot getCommitSnapshot() {
513 		return commitSnapshot;
514 	}
515 
516 	/**
517 	 * Update the commit snapshot {@link #getCommitSnapshot()} before commit.
518 	 * <p>
519 	 * This may be necessary if you need time stamp before commit occurs, e.g
520 	 * while writing the index.
521 	 */
522 	public void createCommitSnapshot() {
523 		saveStatInformation();
524 	}
525 
526 	/**
527 	 * Unlock this file and abort this change.
528 	 * <p>
529 	 * The temporary file (if created) is deleted before returning.
530 	 */
531 	public void unlock() {
532 		if (os != null) {
533 			try {
534 				os.close();
535 			} catch (IOException e) {
536 				LOG.error(MessageFormat
537 						.format(JGitText.get().unlockLockFileFailed, lck), e);
538 			}
539 			os = null;
540 		}
541 
542 		if (haveLck) {
543 			haveLck = false;
544 			try {
545 				FileUtils.delete(lck, FileUtils.RETRY);
546 			} catch (IOException e) {
547 				LOG.error(MessageFormat
548 						.format(JGitText.get().unlockLockFileFailed, lck), e);
549 			} finally {
550 				closeToken();
551 			}
552 		}
553 		isAppend = false;
554 		written = false;
555 	}
556 
557 	/** {@inheritDoc} */
558 	@SuppressWarnings("nls")
559 	@Override
560 	public String toString() {
561 		return "LockFile[" + lck + ", haveLck=" + haveLck + "]";
562 	}
563 }