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