001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006 007import java.io.BufferedReader; 008import java.io.File; 009import java.io.FileFilter; 010import java.io.IOException; 011import java.io.PrintStream; 012import java.lang.management.ManagementFactory; 013import java.nio.charset.StandardCharsets; 014import java.nio.file.Files; 015import java.nio.file.Path; 016import java.util.ArrayList; 017import java.util.Date; 018import java.util.Deque; 019import java.util.HashSet; 020import java.util.Iterator; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Set; 024import java.util.Timer; 025import java.util.TimerTask; 026import java.util.concurrent.ExecutionException; 027import java.util.concurrent.Future; 028import java.util.concurrent.TimeUnit; 029import java.util.regex.Pattern; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask; 033import org.openstreetmap.josm.data.osm.DataSet; 034import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 035import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter; 036import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener; 037import org.openstreetmap.josm.data.preferences.BooleanProperty; 038import org.openstreetmap.josm.data.preferences.IntegerProperty; 039import org.openstreetmap.josm.gui.Notification; 040import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 041import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 042import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 043import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 044import org.openstreetmap.josm.gui.layer.OsmDataLayer; 045import org.openstreetmap.josm.gui.util.GuiHelper; 046import org.openstreetmap.josm.io.OsmExporter; 047import org.openstreetmap.josm.io.OsmImporter; 048import org.openstreetmap.josm.tools.Utils; 049 050/** 051 * Saves data layers periodically so they can be recovered in case of a crash. 052 * 053 * There are 2 directories 054 * - autosave dir: copies of the currently open data layers are saved here every 055 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding 056 * files are removed. If this dir is non-empty on start, JOSM assumes 057 * that it crashed last time. 058 * - deleted layers dir: "secondary archive" - when autosaved layers are restored 059 * they are copied to this directory. We cannot keep them in the autosave folder, 060 * but just deleting it would be dangerous: Maybe a feature inside the file 061 * caused JOSM to crash. If the data is valuable, the user can still try to 062 * open with another versions of JOSM or fix the problem manually. 063 * 064 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files. 065 * 066 * @since 3378 (creation) 067 * @since 10386 (new LayerChangeListener interface) 068 */ 069public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener { 070 071 private static final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'}; 072 private static final String AUTOSAVE_DIR = "autosave"; 073 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers"; 074 075 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true); 076 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1); 077 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5); 078 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", (int) TimeUnit.MINUTES.toSeconds(5)); 079 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000); 080 /** Defines if a notification should be displayed after each autosave */ 081 public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false); 082 083 protected static final class AutosaveLayerInfo { 084 private final OsmDataLayer layer; 085 private String layerName; 086 private String layerFileName; 087 private final Deque<File> backupFiles = new LinkedList<>(); 088 089 AutosaveLayerInfo(OsmDataLayer layer) { 090 this.layer = layer; 091 } 092 } 093 094 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this); 095 private final Set<DataSet> changedDatasets = new HashSet<>(); 096 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<>(); 097 private final Object layersLock = new Object(); 098 private final Deque<File> deletedLayers = new LinkedList<>(); 099 100 private final File autosaveDir = new File(Main.pref.getUserDataDirectory(), AUTOSAVE_DIR); 101 private final File deletedLayersDir = new File(Main.pref.getUserDataDirectory(), DELETED_LAYERS_DIR); 102 103 /** 104 * Replies the autosave directory. 105 * @return the autosave directory 106 * @since 10299 107 */ 108 public final Path getAutosaveDir() { 109 return autosaveDir.toPath(); 110 } 111 112 /** 113 * Starts the autosave background task. 114 */ 115 public void schedule() { 116 if (PROP_INTERVAL.get() > 0) { 117 118 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) { 119 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath())); 120 return; 121 } 122 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) { 123 Main.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath())); 124 return; 125 } 126 127 File[] files = deletedLayersDir.listFiles(); 128 if (files != null) { 129 for (File f: files) { 130 deletedLayers.add(f); // FIXME: sort by mtime 131 } 132 } 133 134 new Timer(true).schedule(this, TimeUnit.SECONDS.toMillis(1), TimeUnit.SECONDS.toMillis(PROP_INTERVAL.get())); 135 Main.getLayerManager().addLayerChangeListener(this, true); 136 } 137 } 138 139 private static String getFileName(String layerName, int index) { 140 String result = layerName; 141 for (char illegalCharacter : ILLEGAL_CHARACTERS) { 142 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)), 143 '&' + String.valueOf((int) illegalCharacter) + ';'); 144 } 145 if (index != 0) { 146 result = result + '_' + index; 147 } 148 return result; 149 } 150 151 private void setLayerFileName(AutosaveLayerInfo layer) { 152 int index = 0; 153 while (true) { 154 String filename = getFileName(layer.layer.getName(), index); 155 boolean foundTheSame = false; 156 for (AutosaveLayerInfo info: layersInfo) { 157 if (info != layer && filename.equals(info.layerFileName)) { 158 foundTheSame = true; 159 break; 160 } 161 } 162 163 if (!foundTheSame) { 164 layer.layerFileName = filename; 165 return; 166 } 167 168 index++; 169 } 170 } 171 172 protected File getNewLayerFile(AutosaveLayerInfo layer, Date now, int startIndex) { 173 int index = startIndex; 174 while (true) { 175 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s", 176 layer.layerFileName, now, index == 0 ? "" : ('_' + Integer.toString(index))); 177 File result = new File(autosaveDir, filename + '.' + Main.pref.get("autosave.extension", "osm")); 178 try { 179 if (index > PROP_INDEX_LIMIT.get()) 180 throw new IOException("index limit exceeded"); 181 if (result.createNewFile()) { 182 createNewPidFile(autosaveDir, filename); 183 return result; 184 } else { 185 Main.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath())); 186 } 187 } catch (IOException e) { 188 Main.error(e, tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage())); 189 return null; 190 } 191 index++; 192 } 193 } 194 195 private static void createNewPidFile(File autosaveDir, String filename) { 196 File pidFile = new File(autosaveDir, filename+".pid"); 197 try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) { 198 ps.println(ManagementFactory.getRuntimeMXBean().getName()); 199 } catch (IOException | SecurityException t) { 200 Main.error(t); 201 } 202 } 203 204 private void savelayer(AutosaveLayerInfo info) { 205 if (!info.layer.getName().equals(info.layerName)) { 206 setLayerFileName(info); 207 info.layerName = info.layer.getName(); 208 } 209 if (changedDatasets.remove(info.layer.data)) { 210 File file = getNewLayerFile(info, new Date(), 0); 211 if (file != null) { 212 info.backupFiles.add(file); 213 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */); 214 } 215 } 216 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) { 217 File oldFile = info.backupFiles.remove(); 218 if (Utils.deleteFile(oldFile, marktr("Unable to delete old backup file {0}"))) { 219 Utils.deleteFile(getPidFile(oldFile), marktr("Unable to delete old backup file {0}")); 220 } 221 } 222 } 223 224 @Override 225 public void run() { 226 synchronized (layersLock) { 227 try { 228 for (AutosaveLayerInfo info: layersInfo) { 229 savelayer(info); 230 } 231 changedDatasets.clear(); 232 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) { 233 GuiHelper.runInEDT(this::displayNotification); 234 } 235 } catch (RuntimeException t) { 236 // Don't let exception stop time thread 237 Main.error("Autosave failed:"); 238 Main.error(t); 239 } 240 } 241 } 242 243 protected void displayNotification() { 244 new Notification(tr("Your work has been saved automatically.")) 245 .setDuration(Notification.TIME_SHORT) 246 .show(); 247 } 248 249 @Override 250 public void layerOrderChanged(LayerOrderChangeEvent e) { 251 // Do nothing 252 } 253 254 private void registerNewlayer(OsmDataLayer layer) { 255 synchronized (layersLock) { 256 layer.data.addDataSetListener(datasetAdapter); 257 layersInfo.add(new AutosaveLayerInfo(layer)); 258 } 259 } 260 261 @Override 262 public void layerAdded(LayerAddEvent e) { 263 if (e.getAddedLayer() instanceof OsmDataLayer) { 264 registerNewlayer((OsmDataLayer) e.getAddedLayer()); 265 } 266 } 267 268 @Override 269 public void layerRemoving(LayerRemoveEvent e) { 270 if (e.getRemovedLayer() instanceof OsmDataLayer) { 271 synchronized (layersLock) { 272 OsmDataLayer osmLayer = (OsmDataLayer) e.getRemovedLayer(); 273 osmLayer.data.removeDataSetListener(datasetAdapter); 274 Iterator<AutosaveLayerInfo> it = layersInfo.iterator(); 275 while (it.hasNext()) { 276 AutosaveLayerInfo info = it.next(); 277 if (info.layer == osmLayer) { 278 279 savelayer(info); 280 File lastFile = info.backupFiles.pollLast(); 281 if (lastFile != null) { 282 moveToDeletedLayersFolder(lastFile); 283 } 284 for (File file: info.backupFiles) { 285 if (Utils.deleteFile(file)) { 286 Utils.deleteFile(getPidFile(file)); 287 } 288 } 289 290 it.remove(); 291 } 292 } 293 } 294 } 295 } 296 297 @Override 298 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 299 changedDatasets.add(event.getDataset()); 300 } 301 302 protected File getPidFile(File osmFile) { 303 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid")); 304 } 305 306 /** 307 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM. 308 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance. 309 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM 310 */ 311 public List<File> getUnsavedLayersFiles() { 312 List<File> result = new ArrayList<>(); 313 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER); 314 if (files == null) 315 return result; 316 for (File file: files) { 317 if (file.isFile()) { 318 boolean skipFile = false; 319 File pidFile = getPidFile(file); 320 if (pidFile.exists()) { 321 try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) { 322 String jvmId = reader.readLine(); 323 if (jvmId != null) { 324 String pid = jvmId.split("@")[0]; 325 skipFile = jvmPerfDataFileExists(pid); 326 } 327 } catch (IOException | SecurityException t) { 328 Main.error(t); 329 } 330 } 331 if (!skipFile) { 332 result.add(file); 333 } 334 } 335 } 336 return result; 337 } 338 339 private static boolean jvmPerfDataFileExists(final String jvmId) { 340 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name")); 341 if (jvmDir.exists() && jvmDir.canRead()) { 342 File[] files = jvmDir.listFiles((FileFilter) file -> file.getName().equals(jvmId) && file.isFile()); 343 return files != null && files.length == 1; 344 } 345 return false; 346 } 347 348 /** 349 * Recover the unsaved layers and open them asynchronously. 350 * @return A future that can be used to wait for the completion of this task. 351 */ 352 public Future<?> recoverUnsavedLayers() { 353 List<File> files = getUnsavedLayersFiles(); 354 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files")); 355 final Future<?> openFilesFuture = Main.worker.submit(openFileTsk); 356 return Main.worker.submit(() -> { 357 try { 358 // Wait for opened tasks to be generated. 359 openFilesFuture.get(); 360 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) { 361 moveToDeletedLayersFolder(f); 362 } 363 } catch (InterruptedException | ExecutionException e) { 364 Main.error(e); 365 } 366 }); 367 } 368 369 /** 370 * Move file to the deleted layers directory. 371 * If moving does not work, it will try to delete the file directly. 372 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS, 373 * some files in the deleted layers directory will be removed. 374 * 375 * @param f the file, usually from the autosave dir 376 */ 377 private void moveToDeletedLayersFolder(File f) { 378 File backupFile = new File(deletedLayersDir, f.getName()); 379 File pidFile = getPidFile(f); 380 381 if (backupFile.exists()) { 382 deletedLayers.remove(backupFile); 383 Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}")); 384 } 385 if (f.renameTo(backupFile)) { 386 deletedLayers.add(backupFile); 387 Utils.deleteFile(pidFile); 388 } else { 389 Main.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName())); 390 // we cannot move to deleted folder, so just try to delete it directly 391 if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) { 392 Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}")); 393 } 394 } 395 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) { 396 File next = deletedLayers.remove(); 397 if (next == null) { 398 break; 399 } 400 Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}")); 401 } 402 } 403 404 /** 405 * Mark all unsaved layers as deleted. They are still preserved in the deleted layers folder. 406 */ 407 public void discardUnsavedLayers() { 408 for (File f: getUnsavedLayersFiles()) { 409 moveToDeletedLayersFolder(f); 410 } 411 } 412}