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