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