View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.internal.impl;
20  
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.UncheckedIOException;
25  import java.nio.ByteBuffer;
26  import java.nio.channels.Channels;
27  import java.nio.channels.FileChannel;
28  import java.nio.channels.FileLock;
29  import java.nio.channels.OverlappingFileLockException;
30  import java.nio.file.Files;
31  import java.nio.file.NoSuchFileException;
32  import java.nio.file.Path;
33  import java.nio.file.StandardOpenOption;
34  import java.util.Map;
35  import java.util.Properties;
36  
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * Manages access to a properties file in legacy compatible way.
42   * <p>
43   * Note: the file locking in this component (that predates {@link org.eclipse.aether.SyncContext}) is present only
44   * to back off two parallel implementations that coexist in Maven (this class and {@code maven-compat} one), as in
45   * certain cases the two implementations may collide on properties files. This locking must remain in place for as long
46   * as {@code maven-compat} code exists.
47   * <p>
48   * This implementation should be used when multiple, legacy Maven versions (older than 3.10.x) share same local repository
49   * concurrently.
50   *
51   * @since 2.0.17
52   * @see NamedLocksTrackingFileManager
53   * @see TrackingFileManagerSupplier
54   */
55  public final class LegacyTrackingFileManager implements TrackingFileManager {
56      private static final Logger LOGGER = LoggerFactory.getLogger(LegacyTrackingFileManager.class);
57  
58      @Deprecated
59      @Override
60      public Properties read(File file) {
61          return read(file.toPath());
62      }
63  
64      @Override
65      public Properties read(Path path) {
66          // existence, not readability: an unreadable tracking file must fail, not read as "untracked"
67          if (Files.exists(path)) {
68              synchronized (mutex(path)) {
69                  try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
70                          FileLock unused = fileLock(fileChannel, true)) {
71                      Properties props = new Properties();
72                      props.load(Channels.newInputStream(fileChannel));
73                      return props;
74                  } catch (NoSuchFileException e) {
75                      LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
76                  } catch (IOException e) {
77                      LOGGER.warn("Failed to read tracking file '{}'", path, e);
78                      throw new UncheckedIOException(e);
79                  }
80              }
81          }
82          return null;
83      }
84  
85      @Deprecated
86      @Override
87      public Properties update(File file, Map<String, String> updates) {
88          return update(file.toPath(), updates);
89      }
90  
91      @Override
92      public Properties update(Path path, Map<String, String> updates) {
93          try {
94              Files.createDirectories(path.getParent());
95          } catch (IOException e) {
96              LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
97              throw new UncheckedIOException(e);
98          }
99          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 }