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