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