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 OsmDataLayer layer; 075 String layerName; 076 String layerFileName; 077 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 for (File f: deletedLayersDir.listFiles()) { 103 deletedLayers.add(f); // FIXME: sort by mtime 104 } 105 106 timer = new Timer(true); 107 timer.schedule(this, 1000L, PROP_INTERVAL.get() * 1000L); 108 MapView.addLayerChangeListener(this); 109 if (Main.isDisplayingMapView()) { 110 for (OsmDataLayer l: Main.map.mapView.getLayersOfType(OsmDataLayer.class)) { 111 registerNewlayer(l); 112 } 113 } 114 } 115 } 116 117 private String getFileName(String layerName, int index) { 118 String result = layerName; 119 for (char illegalCharacter : ILLEGAL_CHARACTERS) { 120 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)), 121 '&' + String.valueOf((int) illegalCharacter) + ';'); 122 } 123 if (index != 0) { 124 result = result + '_' + index; 125 } 126 return result; 127 } 128 129 private void setLayerFileName(AutosaveLayerInfo layer) { 130 int index = 0; 131 while (true) { 132 String filename = getFileName(layer.layer.getName(), index); 133 boolean foundTheSame = false; 134 for (AutosaveLayerInfo info: layersInfo) { 135 if (info != layer && filename.equals(info.layerFileName)) { 136 foundTheSame = true; 137 break; 138 } 139 } 140 141 if (!foundTheSame) { 142 layer.layerFileName = filename; 143 return; 144 } 145 146 index++; 147 } 148 } 149 150 private File getNewLayerFile(AutosaveLayerInfo layer) { 151 int index = 0; 152 Date now = new Date(); 153 while (true) { 154 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s", 155 layer.layerFileName, now, index == 0 ? "" : "_" + index); 156 File result = new File(autosaveDir, filename+".osm"); 157 try { 158 if (result.createNewFile()) { 159 File pidFile = new File(autosaveDir, filename+".pid"); 160 try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) { 161 ps.println(ManagementFactory.getRuntimeMXBean().getName()); 162 } catch (Exception t) { 163 Main.error(t); 164 } 165 return result; 166 } else { 167 Main.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath())); 168 if (index > PROP_INDEX_LIMIT.get()) 169 throw new IOException("index limit exceeded"); 170 } 171 } catch (IOException e) { 172 Main.error(tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage())); 173 return null; 174 } 175 index++; 176 } 177 } 178 179 private void savelayer(AutosaveLayerInfo info) { 180 if (!info.layer.getName().equals(info.layerName)) { 181 setLayerFileName(info); 182 info.layerName = info.layer.getName(); 183 } 184 if (changedDatasets.remove(info.layer.data)) { 185 File file = getNewLayerFile(info); 186 if (file != null) { 187 info.backupFiles.add(file); 188 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */); 189 } 190 } 191 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) { 192 File oldFile = info.backupFiles.remove(); 193 if (!oldFile.delete()) { 194 Main.warn(tr("Unable to delete old backup file {0}", oldFile.getAbsolutePath())); 195 } else { 196 getPidFile(oldFile).delete(); 197 } 198 } 199 } 200 201 @Override 202 public void run() { 203 synchronized (layersLock) { 204 try { 205 for (AutosaveLayerInfo info: layersInfo) { 206 savelayer(info); 207 } 208 changedDatasets.clear(); 209 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) { 210 displayNotification(); 211 } 212 } catch (Exception t) { 213 // Don't let exception stop time thread 214 Main.error("Autosave failed:"); 215 Main.error(t); 216 } 217 } 218 } 219 220 protected void displayNotification() { 221 GuiHelper.runInEDT(new Runnable() { 222 @Override 223 public void run() { 224 new Notification(tr("Your work has been saved automatically.")) 225 .setDuration(Notification.TIME_SHORT) 226 .show(); 227 } 228 }); 229 } 230 231 @Override 232 public void activeLayerChange(Layer oldLayer, Layer newLayer) { 233 // Do nothing 234 } 235 236 private void registerNewlayer(OsmDataLayer layer) { 237 synchronized (layersLock) { 238 layer.data.addDataSetListener(datasetAdapter); 239 AutosaveLayerInfo info = new AutosaveLayerInfo(); 240 info.layer = layer; 241 layersInfo.add(info); 242 } 243 } 244 245 @Override 246 public void layerAdded(Layer newLayer) { 247 if (newLayer instanceof OsmDataLayer) { 248 registerNewlayer((OsmDataLayer) newLayer); 249 } 250 } 251 252 @Override 253 public void layerRemoved(Layer oldLayer) { 254 if (oldLayer instanceof OsmDataLayer) { 255 synchronized (layersLock) { 256 OsmDataLayer osmLayer = (OsmDataLayer) oldLayer; 257 osmLayer.data.removeDataSetListener(datasetAdapter); 258 Iterator<AutosaveLayerInfo> it = layersInfo.iterator(); 259 while (it.hasNext()) { 260 AutosaveLayerInfo info = it.next(); 261 if (info.layer == osmLayer) { 262 263 savelayer(info); 264 File lastFile = info.backupFiles.pollLast(); 265 if (lastFile != null) { 266 moveToDeletedLayersFolder(lastFile); 267 } 268 for (File file: info.backupFiles) { 269 if (file.delete()) { 270 getPidFile(file).delete(); 271 } 272 } 273 274 it.remove(); 275 } 276 } 277 } 278 } 279 } 280 281 @Override 282 public void processDatasetEvent(AbstractDatasetChangedEvent event) { 283 changedDatasets.add(event.getDataset()); 284 } 285 286 private final File getPidFile(File osmFile) { 287 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid")); 288 } 289 290 /** 291 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM. 292 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance. 293 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM 294 */ 295 public List<File> getUnsavedLayersFiles() { 296 List<File> result = new ArrayList<>(); 297 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER); 298 if (files == null) 299 return result; 300 for (File file: files) { 301 if (file.isFile()) { 302 boolean skipFile = false; 303 File pidFile = getPidFile(file); 304 if (pidFile.exists()) { 305 try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) { 306 String jvmId = reader.readLine(); 307 if (jvmId != null) { 308 String pid = jvmId.split("@")[0]; 309 skipFile = jvmPerfDataFileExists(pid); 310 } 311 } catch (Exception t) { 312 Main.error(t); 313 } 314 } 315 if (!skipFile) { 316 result.add(file); 317 } 318 } 319 } 320 return result; 321 } 322 323 private boolean jvmPerfDataFileExists(final String jvmId) { 324 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name")); 325 if (jvmDir.exists() && jvmDir.canRead()) { 326 File[] files = jvmDir.listFiles(new FileFilter() { 327 @Override 328 public boolean accept(File file) { 329 return file.getName().equals(jvmId) && file.isFile(); 330 } 331 }); 332 return files != null && files.length == 1; 333 } 334 return false; 335 } 336 337 public void recoverUnsavedLayers() { 338 List<File> files = getUnsavedLayersFiles(); 339 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files")); 340 Main.worker.submit(openFileTsk); 341 Main.worker.submit(new Runnable() { 342 @Override 343 public void run() { 344 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) { 345 moveToDeletedLayersFolder(f); 346 } 347 } 348 }); 349 } 350 351 /** 352 * Move file to the deleted layers directory. 353 * If moving does not work, it will try to delete the file directly. 354 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS, 355 * some files in the deleted layers directory will be removed. 356 * 357 * @param f the file, usually from the autosave dir 358 */ 359 private void moveToDeletedLayersFolder(File f) { 360 File backupFile = new File(deletedLayersDir, f.getName()); 361 File pidFile = getPidFile(f); 362 363 if (backupFile.exists()) { 364 deletedLayers.remove(backupFile); 365 if (!backupFile.delete()) { 366 Main.warn(String.format("Could not delete old backup file %s", backupFile)); 367 } 368 } 369 if (f.renameTo(backupFile)) { 370 deletedLayers.add(backupFile); 371 pidFile.delete(); 372 } else { 373 Main.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName())); 374 // we cannot move to deleted folder, so just try to delete it directly 375 if (!f.delete()) { 376 Main.warn(String.format("Could not delete backup file %s", f)); 377 } else if (!pidFile.delete()) { 378 Main.warn(String.format("Could not delete PID file %s", pidFile)); 379 } 380 } 381 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) { 382 File next = deletedLayers.remove(); 383 if (next == null) { 384 break; 385 } 386 if (!next.delete()) { 387 Main.warn(String.format("Could not delete archived backup file %s", next)); 388 } 389 } 390 } 391 392 public void discardUnsavedLayers() { 393 for (File f: getUnsavedLayersFiles()) { 394 moveToDeletedLayersFolder(f); 395 } 396 } 397}