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