source: josm/trunk/src/org/openstreetmap/josm/gui/layer/AutosaveTask.java@ 12856

Last change on this file since 12856 was 12856, checked in by bastiK, 7 years ago

see #15229 - add parameter to base directory methods

  • Property svn:eol-style set to native
File size: 17.1 KB
RevLine 
[3378]1// License: GPL. For details, see LICENSE file.
[12634]2package org.openstreetmap.josm.gui.layer;
[3378]3
[9296]4import static org.openstreetmap.josm.tools.I18n.marktr;
[3378]5import static org.openstreetmap.josm.tools.I18n.tr;
6
[5566]7import java.io.BufferedReader;
[3378]8import java.io.File;
[5566]9import java.io.FileFilter;
[3378]10import java.io.IOException;
[5566]11import java.io.PrintStream;
12import java.lang.management.ManagementFactory;
[7082]13import java.nio.charset.StandardCharsets;
[7315]14import java.nio.file.Files;
[10299]15import java.nio.file.Path;
[3378]16import java.util.ArrayList;
17import java.util.Date;
18import java.util.Deque;
19import java.util.HashSet;
20import java.util.Iterator;
21import java.util.LinkedList;
22import java.util.List;
23import java.util.Set;
24import java.util.Timer;
25import java.util.TimerTask;
[10444]26import java.util.concurrent.ExecutionException;
27import java.util.concurrent.Future;
[11288]28import java.util.concurrent.TimeUnit;
[3378]29import java.util.regex.Pattern;
30
[3679]31import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
[3378]32import org.openstreetmap.josm.data.osm.DataSet;
33import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
34import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter;
35import org.openstreetmap.josm.data.osm.event.DataSetListenerAdapter.Listener;
[3461]36import org.openstreetmap.josm.data.preferences.BooleanProperty;
[3378]37import org.openstreetmap.josm.data.preferences.IntegerProperty;
[12634]38import org.openstreetmap.josm.gui.MainApplication;
[6357]39import org.openstreetmap.josm.gui.Notification;
[12671]40import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
41import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
[10386]42import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
43import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
44import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
45import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
[6357]46import org.openstreetmap.josm.gui.util.GuiHelper;
[12846]47import org.openstreetmap.josm.spi.preferences.Config;
[12620]48import org.openstreetmap.josm.tools.Logging;
[9296]49import org.openstreetmap.josm.tools.Utils;
[3378]50
[3679]51/**
52 * Saves data layers periodically so they can be recovered in case of a crash.
[3686]53 *
54 * There are 2 directories
55 * - autosave dir: copies of the currently open data layers are saved here every
56 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
57 * files are removed. If this dir is non-empty on start, JOSM assumes
58 * that it crashed last time.
59 * - deleted layers dir: "secondary archive" - when autosaved layers are restored
60 * they are copied to this directory. We cannot keep them in the autosave folder,
61 * but just deleting it would be dangerous: Maybe a feature inside the file
62 * caused JOSM to crash. If the data is valuable, the user can still try to
63 * open with another versions of JOSM or fix the problem manually.
64 *
65 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
[10386]66 *
67 * @since 3378 (creation)
68 * @since 10386 (new LayerChangeListener interface)
[3679]69 */
[3378]70public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
71
[8443]72 private static final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
[3378]73 private static final String AUTOSAVE_DIR = "autosave";
74 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
75
[12374]76 /**
77 * If autosave is enabled
78 */
[3461]79 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
[12374]80 /**
81 * The number of files to store per layer
82 */
[3378]83 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
[12374]84 /**
85 * How many deleted layers should be stored
86 */
[3378]87 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
[12374]88 /**
89 * The autosave interval, in seconds
90 */
[11288]91 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", (int) TimeUnit.MINUTES.toSeconds(5));
[12374]92 /**
93 * The maximum number of autosave files to store
94 */
[3461]95 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
[12374]96 /**
97 * Defines if a notification should be displayed after each autosave
98 */
[6357]99 public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false);
[3378]100
[10299]101 protected static final class AutosaveLayerInfo {
102 private final OsmDataLayer layer;
[8285]103 private String layerName;
104 private String layerFileName;
105 private final Deque<File> backupFiles = new LinkedList<>();
[10299]106
107 AutosaveLayerInfo(OsmDataLayer layer) {
108 this.layer = layer;
109 }
[3378]110 }
111
112 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
[7005]113 private final Set<DataSet> changedDatasets = new HashSet<>();
114 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<>();
[3461]115 private final Object layersLock = new Object();
[7005]116 private final Deque<File> deletedLayers = new LinkedList<>();
[3378]117
[12856]118 private final File autosaveDir = new File(Config.getDirs().getUserDataDirectory(true), AUTOSAVE_DIR);
119 private final File deletedLayersDir = new File(Config.getDirs().getUserDataDirectory(true), DELETED_LAYERS_DIR);
[3378]120
[10299]121 /**
122 * Replies the autosave directory.
123 * @return the autosave directory
124 * @since 10299
125 */
126 public final Path getAutosaveDir() {
127 return autosaveDir.toPath();
128 }
129
[10444]130 /**
131 * Starts the autosave background task.
132 */
[3378]133 public void schedule() {
134 if (PROP_INTERVAL.get() > 0) {
135
136 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
[12620]137 Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
[3378]138 return;
139 }
140 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
[12620]141 Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
[3378]142 return;
143 }
144
[8308]145 File[] files = deletedLayersDir.listFiles();
146 if (files != null) {
147 for (File f: files) {
148 deletedLayers.add(f); // FIXME: sort by mtime
149 }
[3378]150 }
151
[11288]152 new Timer(true).schedule(this, TimeUnit.SECONDS.toMillis(1), TimeUnit.SECONDS.toMillis(PROP_INTERVAL.get()));
[12636]153 MainApplication.getLayerManager().addAndFireLayerChangeListener(this);
[3378]154 }
155 }
156
[8870]157 private static String getFileName(String layerName, int index) {
[3378]158 String result = layerName;
[6104]159 for (char illegalCharacter : ILLEGAL_CHARACTERS) {
160 result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)),
161 '&' + String.valueOf((int) illegalCharacter) + ';');
[3378]162 }
163 if (index != 0) {
164 result = result + '_' + index;
165 }
166 return result;
167 }
168
169 private void setLayerFileName(AutosaveLayerInfo layer) {
170 int index = 0;
171 while (true) {
172 String filename = getFileName(layer.layer.getName(), index);
173 boolean foundTheSame = false;
174 for (AutosaveLayerInfo info: layersInfo) {
[3381]175 if (info != layer && filename.equals(info.layerFileName)) {
[3378]176 foundTheSame = true;
177 break;
178 }
179 }
180
181 if (!foundTheSame) {
182 layer.layerFileName = filename;
183 return;
184 }
185
186 index++;
187 }
188 }
189
[10299]190 protected File getNewLayerFile(AutosaveLayerInfo layer, Date now, int startIndex) {
191 int index = startIndex;
[3378]192 while (true) {
[7951]193 String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s",
[10300]194 layer.layerFileName, now, index == 0 ? "" : ('_' + Integer.toString(index)));
[12846]195 File result = new File(autosaveDir, filename + '.' + Config.getPref().get("autosave.extension", "osm"));
[3378]196 try {
[10299]197 if (index > PROP_INDEX_LIMIT.get())
198 throw new IOException("index limit exceeded");
[5566]199 if (result.createNewFile()) {
[10299]200 createNewPidFile(autosaveDir, filename);
[3378]201 return result;
[5566]202 } else {
[12620]203 Logging.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
[3378]204 }
205 } catch (IOException e) {
[12620]206 Logging.log(Logging.LEVEL_ERROR, tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()), e);
[3378]207 return null;
208 }
209 index++;
210 }
211 }
212
[10299]213 private static void createNewPidFile(File autosaveDir, String filename) {
214 File pidFile = new File(autosaveDir, filename+".pid");
215 try (PrintStream ps = new PrintStream(pidFile, "UTF-8")) {
216 ps.println(ManagementFactory.getRuntimeMXBean().getName());
217 } catch (IOException | SecurityException t) {
[12620]218 Logging.error(t);
[10299]219 }
220 }
221
[7029]222 private void savelayer(AutosaveLayerInfo info) {
[3378]223 if (!info.layer.getName().equals(info.layerName)) {
224 setLayerFileName(info);
225 info.layerName = info.layer.getName();
226 }
[5512]227 if (changedDatasets.remove(info.layer.data)) {
[10299]228 File file = getNewLayerFile(info, new Date(), 0);
[3378]229 if (file != null) {
230 info.backupFiles.add(file);
[4852]231 new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
[3378]232 }
233 }
234 while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
235 File oldFile = info.backupFiles.remove();
[9296]236 if (Utils.deleteFile(oldFile, marktr("Unable to delete old backup file {0}"))) {
237 Utils.deleteFile(getPidFile(oldFile), marktr("Unable to delete old backup file {0}"));
[3378]238 }
239 }
240 }
241
242 @Override
243 public void run() {
244 synchronized (layersLock) {
245 try {
246 for (AutosaveLayerInfo info: layersInfo) {
247 savelayer(info);
248 }
249 changedDatasets.clear();
[6357]250 if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) {
[10621]251 GuiHelper.runInEDT(this::displayNotification);
[6357]252 }
[11746]253 } catch (RuntimeException t) { // NOPMD
[3378]254 // Don't let exception stop time thread
[12620]255 Logging.error("Autosave failed:");
256 Logging.error(t);
[3378]257 }
258 }
259 }
260
[6357]261 protected void displayNotification() {
[10621]262 new Notification(tr("Your work has been saved automatically."))
263 .setDuration(Notification.TIME_SHORT)
264 .show();
[6357]265 }
266
[3378]267 @Override
[10386]268 public void layerOrderChanged(LayerOrderChangeEvent e) {
[3378]269 // Do nothing
270 }
271
272 private void registerNewlayer(OsmDataLayer layer) {
273 synchronized (layersLock) {
274 layer.data.addDataSetListener(datasetAdapter);
[10299]275 layersInfo.add(new AutosaveLayerInfo(layer));
[3378]276 }
277 }
278
279 @Override
[10386]280 public void layerAdded(LayerAddEvent e) {
281 if (e.getAddedLayer() instanceof OsmDataLayer) {
282 registerNewlayer((OsmDataLayer) e.getAddedLayer());
[3378]283 }
284 }
285
286 @Override
[10386]287 public void layerRemoving(LayerRemoveEvent e) {
288 if (e.getRemovedLayer() instanceof OsmDataLayer) {
[3378]289 synchronized (layersLock) {
[10386]290 OsmDataLayer osmLayer = (OsmDataLayer) e.getRemovedLayer();
[3378]291 osmLayer.data.removeDataSetListener(datasetAdapter);
292 Iterator<AutosaveLayerInfo> it = layersInfo.iterator();
293 while (it.hasNext()) {
294 AutosaveLayerInfo info = it.next();
295 if (info.layer == osmLayer) {
296
[7029]297 savelayer(info);
298 File lastFile = info.backupFiles.pollLast();
299 if (lastFile != null) {
300 moveToDeletedLayersFolder(lastFile);
301 }
302 for (File file: info.backupFiles) {
[9296]303 if (Utils.deleteFile(file)) {
304 Utils.deleteFile(getPidFile(file));
[3378]305 }
306 }
307
308 it.remove();
309 }
310 }
311 }
312 }
313 }
314
315 @Override
316 public void processDatasetEvent(AbstractDatasetChangedEvent event) {
317 changedDatasets.add(event.getDataset());
318 }
319
[10299]320 protected File getPidFile(File osmFile) {
[5566]321 return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid"));
322 }
[6069]323
[5566]324 /**
325 * Replies the list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM.
326 * These files are hence unsaved layers from an old instance of JOSM that crashed and may be recovered by this instance.
327 * @return The list of .osm files still present in autosave dir, that are not currently managed by another instance of JOSM
328 */
[3435]329 public List<File> getUnsavedLayersFiles() {
[7005]330 List<File> result = new ArrayList<>();
[5566]331 File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER);
[3378]332 if (files == null)
333 return result;
[3435]334 for (File file: files) {
335 if (file.isFile()) {
[5566]336 boolean skipFile = false;
337 File pidFile = getPidFile(file);
338 if (pidFile.exists()) {
[7315]339 try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) {
[7037]340 String jvmId = reader.readLine();
341 if (jvmId != null) {
342 String pid = jvmId.split("@")[0];
343 skipFile = jvmPerfDataFileExists(pid);
[5566]344 }
[10212]345 } catch (IOException | SecurityException t) {
[12620]346 Logging.error(t);
[5566]347 }
348 }
349 if (!skipFile) {
350 result.add(file);
351 }
[3378]352 }
[3435]353 }
354 return result;
355 }
[6069]356
[10043]357 private static boolean jvmPerfDataFileExists(final String jvmId) {
[5566]358 File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name"));
359 if (jvmDir.exists() && jvmDir.canRead()) {
[10608]360 File[] files = jvmDir.listFiles((FileFilter) file -> file.getName().equals(jvmId) && file.isFile());
[5566]361 return files != null && files.length == 1;
362 }
363 return false;
364 }
[3435]365
[10444]366 /**
367 * Recover the unsaved layers and open them asynchronously.
368 * @return A future that can be used to wait for the completion of this task.
369 */
370 public Future<?> recoverUnsavedLayers() {
[3679]371 List<File> files = getUnsavedLayersFiles();
372 final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
[12634]373 final Future<?> openFilesFuture = MainApplication.worker.submit(openFileTsk);
374 return MainApplication.worker.submit(() -> {
[10608]375 try {
376 // Wait for opened tasks to be generated.
377 openFilesFuture.get();
378 for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
379 moveToDeletedLayersFolder(f);
[3679]380 }
[10608]381 } catch (InterruptedException | ExecutionException e) {
[12620]382 Logging.error(e);
[3839]383 }
[3679]384 });
[3378]385 }
386
[3686]387 /**
388 * Move file to the deleted layers directory.
389 * If moving does not work, it will try to delete the file directly.
390 * Afterwards, if the number of deleted layers gets larger than PROP_DELETED_LAYERS,
391 * some files in the deleted layers directory will be removed.
392 *
393 * @param f the file, usually from the autosave dir
394 */
[3559]395 private void moveToDeletedLayersFolder(File f) {
396 File backupFile = new File(deletedLayersDir, f.getName());
[5566]397 File pidFile = getPidFile(f);
[6069]398
[3559]399 if (backupFile.exists()) {
[3686]400 deletedLayers.remove(backupFile);
[9296]401 Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}"));
[3559]402 }
403 if (f.renameTo(backupFile)) {
404 deletedLayers.add(backupFile);
[9296]405 Utils.deleteFile(pidFile);
[3559]406 } else {
[12620]407 Logging.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
[3686]408 // we cannot move to deleted folder, so just try to delete it directly
[9296]409 if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) {
410 Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}"));
[3686]411 }
[3559]412 }
413 while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
[3675]414 File next = deletedLayers.remove();
[3686]415 if (next == null) {
416 break;
417 }
[9296]418 Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}"));
[3559]419 }
420 }
421
[10444]422 /**
423 * Mark all unsaved layers as deleted. They are still preserved in the deleted layers folder.
424 */
[6995]425 public void discardUnsavedLayers() {
[3686]426 for (File f: getUnsavedLayersFiles()) {
[3559]427 moveToDeletedLayersFolder(f);
[3461]428 }
429 }
[3378]430}
Note: See TracBrowser for help on using the repository browser.