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 TrackingFileManagerProvider
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          if (Files.isReadable(path)) {
67              synchronized (mutex(path)) {
68                  try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
69                          FileLock unused = fileLock(fileChannel, true)) {
70                      Properties props = new Properties();
71                      props.load(Channels.newInputStream(fileChannel));
72                      return props;
73                  } catch (NoSuchFileException e) {
74                      LOGGER.debug("No such file to read {}: {}", path, e.getMessage());
75                  } catch (IOException e) {
76                      LOGGER.warn("Failed to read tracking file '{}'", path, e);
77                      throw new UncheckedIOException(e);
78                  }
79              }
80          }
81          return null;
82      }
83  
84      @Deprecated
85      @Override
86      public Properties update(File file, Map<String, String> updates) {
87          return update(file.toPath(), updates);
88      }
89  
90      @Override
91      public Properties update(Path path, Map<String, String> updates) {
92          try {
93              Files.createDirectories(path.getParent());
94          } catch (IOException e) {
95              LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
96              throw new UncheckedIOException(e);
97          }
98          synchronized (mutex(path)) {
99              try (FileChannel fileChannel = FileChannel.open(
100                             path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
101                     FileLock unused = fileLock(fileChannel, false)) {
102                 Properties props = new Properties();
103                 if (fileChannel.size() > 0) {
104                     props.load(Channels.newInputStream(fileChannel));
105                 }
106 
107                 for (Map.Entry<String, String> update : updates.entrySet()) {
108                     if (update.getValue() == null) {
109                         props.remove(update.getKey());
110                     } else {
111                         props.setProperty(update.getKey(), update.getValue());
112                     }
113                 }
114 
115                 LOGGER.debug("Writing tracking file '{}'", path);
116                 ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
117                 props.store(
118                         stream,
119                         "NOTE: This is a Maven Resolver internal implementation file"
120                                 + ", its format can be changed without prior notice.");
121                 fileChannel.position(0);
122                 int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
123                 fileChannel.truncate(written);
124                 return props;
125             } catch (IOException e) {
126                 LOGGER.warn("Failed to write tracking file '{}'", path, e);
127                 throw new UncheckedIOException(e);
128             }
129         }
130     }
131 
132     @Deprecated
133     @Override
134     public boolean delete(File file) {
135         return delete(file.toPath());
136     }
137 
138     @Override
139     public boolean delete(Path path) {
140         if (Files.isReadable(path)) {
141             synchronized (mutex(path)) {
142                 try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE);
143                         FileLock unused = fileLock(fileChannel, false)) {
144                     Files.delete(path);
145                     return true;
146                 } catch (NoSuchFileException e) {
147                     LOGGER.debug("No such file to delete {}: {}", path, e.getMessage());
148                 } catch (IOException e) {
149                     LOGGER.warn("Failed to delete tracking file '{}'", path, e);
150                     throw new UncheckedIOException(e);
151                 }
152             }
153         }
154         return false;
155     }
156 
157     /**
158      * This method creates a "mutex" object to synchronize on thread level, within same JVM, to prevent multiple
159      * threads from trying to lock the same file at the same time. Threads concurrently working on different files
160      * are okay, as after syncing on mutex, they operate with FS locking, that goal is to synchronize with possible
161      * other Maven processes, and not with other threads in this JVM.
162      */
163     private static Object mutex(Path path) {
164         // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
165         // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
166         return canonicalPath(path).toString().intern();
167     }
168 
169     /**
170      * Tries the best it can to figure out actual file the workload is about, while resolving cases like symlinked
171      * local repository etc.
172      */
173     private static Path canonicalPath(Path path) {
174         try {
175             return path.toRealPath();
176         } catch (IOException e) {
177             return canonicalPath(path.getParent()).resolve(path.getFileName());
178         }
179     }
180 
181     private FileLock fileLock(FileChannel channel, boolean shared) throws IOException {
182         FileLock lock = null;
183         for (int attempts = 8; attempts >= 0; attempts--) {
184             try {
185                 lock = channel.lock(0, Long.MAX_VALUE, shared);
186                 break;
187             } catch (OverlappingFileLockException | IOException e) {
188                 // For Unix process sun.nio.ch.UnixFileDispatcherImpl.lock0() is a native method that can throw
189                 // IOException with message "Resource deadlock avoided"
190                 // the system call level is involving fcntl() or flock()
191                 // If the kernel detects that granting the lock would result in a deadlock
192                 // (where two processes are waiting for each other to release locks which can happen when two processes
193                 // are trying to lock the same file),
194                 // it returns an EDEADLK error, which Java throws as an IOException.
195                 // Read another comment from
196                 // https://github.com/bdeployteam/bdeploy/blob/7c04e7228d6d48b8990e6703a8d476e21024c639/bhive/src/main/java/io/bdeploy/bhive/objects/LockableDatabase.java#L57
197                 // Note (cstamas): seems this MAY also happen where there is ONE process but performs locking on same
198                 // file from multiple threads, as Linux kernel performs lock detection on process level.
199                 if (attempts <= 0) {
200                     throw (e instanceof IOException) ? (IOException) e : new IOException(e);
201                 }
202                 try {
203                     Thread.sleep(50L);
204                 } catch (InterruptedException e1) {
205                     Thread.currentThread().interrupt();
206                 }
207             }
208         }
209         if (lock == null) {
210             throw new IOException("Could not lock file");
211         }
212         return lock;
213     }
214 }