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