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