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 java.io.ByteArrayOutputStream; 022import java.io.File; 023import java.io.IOException; 024import java.io.UncheckedIOException; 025import java.nio.ByteBuffer; 026import java.nio.channels.Channels; 027import java.nio.channels.FileChannel; 028import java.nio.channels.FileLock; 029import java.nio.channels.OverlappingFileLockException; 030import java.nio.file.Files; 031import java.nio.file.NoSuchFileException; 032import java.nio.file.Path; 033import java.nio.file.StandardOpenOption; 034import java.util.Map; 035import java.util.Properties; 036 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040/** 041 * Manages access to a properties file in legacy compatible way. 042 * <p> 043 * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only 044 * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in 045 * certain cases the two implementations may collide on properties files. This locking must remain in place for as long 046 * as {@code maven-compat} code exists. 047 * <p> 048 * This implementation should be used when multiple, legacy Maven versions (older than 3.10.x) share same local repository 049 * concurrently. 050 * 051 * @since 2.0.17 052 * @see NamedLocksTrackingFileManager 053 * @see TrackingFileManagerSupplier 054 */ 055public final class LegacyTrackingFileManager implements TrackingFileManager { 056 private static final Logger LOGGER = LoggerFactory.getLogger(LegacyTrackingFileManager.class); 057 058 @Deprecated 059 @Override 060 public Properties read(File file) { 061 return read(file.toPath()); 062 } 063 064 @Override 065 public Properties read(Path path) { 066 // existence, not readability: an unreadable tracking file must fail, not read as "untracked" 067 if (Files.exists(path)) { 068 synchronized (mutex(path)) { 069 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ); 070 FileLock unused = fileLock(fileChannel, true)) { 071 Properties props = new Properties(); 072 props.load(Channels.newInputStream(fileChannel)); 073 return props; 074 } catch (NoSuchFileException e) { 075 LOGGER.debug("No such file to read {}: {}", path, e.getMessage()); 076 } catch (IOException e) { 077 LOGGER.warn("Failed to read tracking file '{}'", path, e); 078 throw new UncheckedIOException(e); 079 } 080 } 081 } 082 return null; 083 } 084 085 @Deprecated 086 @Override 087 public Properties update(File file, Map<String, String> updates) { 088 return update(file.toPath(), updates); 089 } 090 091 @Override 092 public Properties update(Path path, Map<String, String> updates) { 093 try { 094 Files.createDirectories(path.getParent()); 095 } catch (IOException e) { 096 LOGGER.warn("Failed to create tracking file parent '{}'", path, e); 097 throw new UncheckedIOException(e); 098 } 099 synchronized (mutex(path)) { 100 try (FileChannel fileChannel = FileChannel.open( 101 path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); 102 FileLock unused = fileLock(fileChannel, false)) { 103 Properties props = new Properties(); 104 if (fileChannel.size() > 0) { 105 props.load(Channels.newInputStream(fileChannel)); 106 } 107 108 for (Map.Entry<String, String> update : updates.entrySet()) { 109 if (update.getValue() == null) { 110 props.remove(update.getKey()); 111 } else { 112 props.setProperty(update.getKey(), update.getValue()); 113 } 114 } 115 116 LOGGER.debug("Writing tracking file '{}'", path); 117 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2); 118 props.store( 119 stream, 120 "NOTE: This is a Maven Resolver internal implementation file" 121 + ", its format can be changed without prior notice."); 122 fileChannel.position(0); 123 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray())); 124 fileChannel.truncate(written); 125 return props; 126 } catch (IOException e) { 127 LOGGER.warn("Failed to write tracking file '{}'", path, e); 128 throw new UncheckedIOException(e); 129 } 130 } 131 } 132 133 @Deprecated 134 @Override 135 public boolean delete(File file) { 136 return delete(file.toPath()); 137 } 138 139 @Override 140 public boolean delete(Path path) { 141 if (Files.isReadable(path)) { 142 synchronized (mutex(path)) { 143 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE); 144 FileLock unused = fileLock(fileChannel, false)) { 145 Files.delete(path); 146 return true; 147 } catch (NoSuchFileException e) { 148 LOGGER.debug("No such file to delete {}: {}", path, e.getMessage()); 149 } catch (IOException e) { 150 LOGGER.warn("Failed to delete tracking file '{}'", path, e); 151 throw new UncheckedIOException(e); 152 } 153 } 154 } 155 return false; 156 } 157 158 /** 159 * This method creates a "mutex" object to synchronize on thread level, within same JVM, to prevent multiple 160 * threads from trying to lock the same file at the same time. Threads concurrently working on different files 161 * are okay, as after syncing on mutex, they operate with FS locking, that goal is to synchronize with possible 162 * other Maven processes, and not with other threads in this JVM. 163 */ 164 private static Object mutex(Path path) { 165 // The interned string of path is (mis)used as mutex, to exclude different threads going for same file, 166 // as JVM file locking happens on JVM not on Thread level. This is how original code did it ¯\_(ツ)_/¯ 167 return canonicalPath(path).toString().intern(); 168 } 169 170 /** 171 * Tries the best it can to figure out actual file the workload is about, while resolving cases like symlinked 172 * local repository etc. 173 */ 174 private static Path canonicalPath(Path path) { 175 try { 176 return path.toRealPath(); 177 } catch (IOException e) { 178 return canonicalPath(path.getParent()).resolve(path.getFileName()); 179 } 180 } 181 182 private FileLock fileLock(FileChannel channel, boolean shared) throws IOException { 183 FileLock lock = null; 184 for (int attempts = 8; attempts >= 0; attempts--) { 185 try { 186 lock = channel.lock(0, Long.MAX_VALUE, shared); 187 break; 188 } catch (OverlappingFileLockException | IOException e) { 189 // For Unix process sun.nio.ch.UnixFileDispatcherImpl.lock0() is a native method that can throw 190 // IOException with message "Resource deadlock avoided" 191 // the system call level is involving fcntl() or flock() 192 // If the kernel detects that granting the lock would result in a deadlock 193 // (where two processes are waiting for each other to release locks which can happen when two processes 194 // are trying to lock the same file), 195 // it returns an EDEADLK error, which Java throws as an IOException. 196 // Read another comment from 197 // https://github.com/bdeployteam/bdeploy/blob/7c04e7228d6d48b8990e6703a8d476e21024c639/bhive/src/main/java/io/bdeploy/bhive/objects/LockableDatabase.java#L57 198 // Note (cstamas): seems this MAY also happen where there is ONE process but performs locking on same 199 // file from multiple threads, as Linux kernel performs lock detection on process level. 200 if (attempts <= 0) { 201 throw (e instanceof IOException) ? (IOException) e : new IOException(e); 202 } 203 try { 204 Thread.sleep(50L); 205 } catch (InterruptedException e1) { 206 Thread.currentThread().interrupt(); 207 } 208 } 209 } 210 if (lock == null) { 211 throw new IOException("Could not lock file"); 212 } 213 return lock; 214 } 215}