001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.internal.impl; 020 021import javax.inject.Named; 022import javax.inject.Singleton; 023 024import java.io.ByteArrayOutputStream; 025import java.io.File; 026import java.io.IOException; 027import java.io.UncheckedIOException; 028import java.nio.ByteBuffer; 029import java.nio.channels.Channels; 030import java.nio.channels.FileChannel; 031import java.nio.channels.FileLock; 032import java.nio.channels.OverlappingFileLockException; 033import java.nio.file.Files; 034import java.nio.file.NoSuchFileException; 035import java.nio.file.Path; 036import java.nio.file.StandardOpenOption; 037import java.util.Map; 038import java.util.Properties; 039 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * Manages access to a properties file. 045 * <p> 046 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only 047 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in 048 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long 049 * as {@code maven-compat} code exists. 050 * 051 * <em>IMPORTANT:</em> This class is kept fully in sync with the master branch one (w/ simple change to convert File 052 * to Path instances). 053 */ 054@Singleton 055@Named 056public final class DefaultTrackingFileManager implements TrackingFileManager { 057 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class); 058 059 @Override 060 public Properties read(File file) { 061 Path path = file.toPath(); 062 if (Files.isReadable(path)) { 063 synchronized (mutex(path)) { 064 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ); 065 FileLock unused = fileLock(fileChannel, true)) { 066 Properties props = new Properties(); 067 props.load(Channels.newInputStream(fileChannel)); 068 return props; 069 } catch (NoSuchFileException e) { 070 LOGGER.debug("No such file to read {}: {}", path, e.getMessage()); 071 } catch (IOException e) { 072 LOGGER.warn("Failed to read tracking file '{}'", path, e); 073 throw new UncheckedIOException(e); 074 } 075 } 076 } 077 return null; 078 } 079 080 @Override 081 public Properties update(File file, Map<String, String> updates) { 082 Path path = file.toPath(); 083 try { 084 Files.createDirectories(path.getParent()); 085 } catch (IOException e) { 086 LOGGER.warn("Failed to create tracking file parent '{}'", path, e); 087 throw new UncheckedIOException(e); 088 } 089 synchronized (mutex(path)) { 090 try (FileChannel fileChannel = FileChannel.open( 091 path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); 092 FileLock unused = fileLock(fileChannel, false)) { 093 Properties props = new Properties(); 094 if (fileChannel.size() > 0) { 095 props.load(Channels.newInputStream(fileChannel)); 096 } 097 098 for (Map.Entry<String, String> update : updates.entrySet()) { 099 if (update.getValue() == null) { 100 props.remove(update.getKey()); 101 } else { 102 props.setProperty(update.getKey(), update.getValue()); 103 } 104 } 105 106 LOGGER.debug("Writing tracking file '{}'", path); 107 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 108 props.store( 109 stream, 110 "NOTE: This is a Maven Resolver internal implementation file" 111 + ", its format can be changed without prior notice."); 112 fileChannel.position(0); 113 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray())); 114 fileChannel.truncate(written); 115 return props; 116 } catch (IOException e) { 117 LOGGER.warn("Failed to write tracking file '{}'", path, e); 118 throw new UncheckedIOException(e); 119 } 120 } 121 } 122 123 @Override 124 public boolean delete(File file) { 125 Path path = file.toPath(); 126 if (Files.isReadable(path)) { 127 synchronized (mutex(path)) { 128 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE); 129 FileLock unused = fileLock(fileChannel, false)) { 130 Files.delete(path); 131 return true; 132 } catch (NoSuchFileException e) { 133 LOGGER.debug("No such file to delete {}: {}", path, e.getMessage()); 134 } catch (IOException e) { 135 LOGGER.warn("Failed to delete tracking file '{}'", path, e); 136 throw new UncheckedIOException(e); 137 } 138 } 139 } 140 return false; 141 } 142 143 /** 144 * This method creates a "mutex" object to synchronize on thread level, within same JVM, to prevent multiple 145 * threads from trying to lock the same file at the same time. Threads concurrently working on different files 146 * are okay, as after syncing on mutex, they operate with FS locking, that goal is to synchronize with possible 147 * other Maven processes, and not with other threads in this JVM. 148 */ 149 private static Object mutex(Path path) { 150 // The interned string of path is (mis)used as mutex, to exclude different threads going for same file, 151 // as JVM file locking happens on JVM not on Thread level. This is how original code did it ¯\_(ツ)_/¯ 152 return canonicalPath(path).toString().intern(); 153 } 154 155 /** 156 * Tries the best it can to figure out actual file the workload is about, while resolving cases like symlinked 157 * local repository etc. 158 */ 159 private static Path canonicalPath(Path path) { 160 try { 161 return path.toRealPath(); 162 } catch (IOException e) { 163 return canonicalPath(path.getParent()).resolve(path.getFileName()); 164 } 165 } 166 167 private FileLock fileLock(FileChannel channel, boolean shared) throws IOException { 168 FileLock lock = null; 169 for (int attempts = 8; attempts >= 0; attempts--) { 170 try { 171 lock = channel.lock(0, Long.MAX_VALUE, shared); 172 break; 173 } catch (OverlappingFileLockException | IOException e) { 174 // For Unix process sun.nio.ch.UnixFileDispatcherImpl.lock0() is a native method that can throw 175 // IOException with message "Resource deadlock avoided" 176 // the system call level is involving fcntl() or flock() 177 // If the kernel detects that granting the lock would result in a deadlock 178 // (where two processes are waiting for each other to release locks which can happen when two processes 179 // are trying to lock the same file), 180 // it returns an EDEADLK error, which Java throws as an IOException. 181 // Read another comment from 182 // https://github.com/bdeployteam/bdeploy/blob/7c04e7228d6d48b8990e6703a8d476e21024c639/bhive/src/main/java/io/bdeploy/bhive/objects/LockableDatabase.java#L57 183 // Note (cstamas): seems this MAY also happen where there is ONE process but performs locking on same 184 // file from multiple threads, as Linux kernel performs lock detection on process level. 185 if (attempts <= 0) { 186 throw (e instanceof IOException) ? (IOException) e : new IOException(e); 187 } 188 try { 189 Thread.sleep(50L); 190 } catch (InterruptedException e1) { 191 Thread.currentThread().interrupt(); 192 } 193 } 194 } 195 if (lock == null) { 196 throw new IOException("Could not lock file"); 197 } 198 return lock; 199 } 200}