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, 19 months ago

see #15229 - add parameter to base directory methods

  • Property svn:eol-style set to native
File size: 17.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.io.BufferedReader;
8import java.io.File;
9import java.io.FileFilter;
10import java.io.IOException;
11import java.io.PrintStream;
12import java.lang.management.ManagementFactory;
13import java.nio.charset.StandardCharsets;
14import java.nio.file.Files;
15import java.nio.file.Path;
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;
26import java.util.concurrent.ExecutionException;
27import java.util.concurrent.Future;
28import java.util.concurrent.TimeUnit;
29import java.util.regex.Pattern;
30
31import org.openstreetmap.josm.actions.OpenFileAction.OpenFileTask;
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;
36import org.openstreetmap.josm.data.preferences.BooleanProperty;
37import org.openstreetmap.josm.data.preferences.IntegerProperty;
38import org.openstreetmap.josm.gui.MainApplication;
39import org.openstreetmap.josm.gui.Notification;
40import org.openstreetmap.josm.gui.io.importexport.OsmExporter;
41import org.openstreetmap.josm.gui.io.importexport.OsmImporter;
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;
46import org.openstreetmap.josm.gui.util.GuiHelper;
47import org.openstreetmap.josm.spi.preferences.Config;
48import org.openstreetmap.josm.tools.Logging;
49import org.openstreetmap.josm.tools.Utils;
50
51/**
52 * Saves data layers periodically so they can be recovered in case of a crash.
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.
66 *
67 * @since  3378 (creation)
68 * @since 10386 (new LayerChangeListener interface)
69 */
70public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
71
72    private static final char[] ILLEGAL_CHARACTERS = {'/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':'};
73    private static final String AUTOSAVE_DIR = "autosave";
74    private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
75
76    /**
77     * If autosave is enabled
78     */
79    public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
80    /**
81     * The number of files to store per layer
82     */
83    public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
84    /**
85     * How many deleted layers should be stored
86     */
87    public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
88    /**
89     * The autosave interval, in seconds
90     */
91    public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", (int) TimeUnit.MINUTES.toSeconds(5));
92    /**
93     * The maximum number of autosave files to store
94     */
95    public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
96    /**
97     * Defines if a notification should be displayed after each autosave
98     */
99    public static final BooleanProperty PROP_NOTIFICATION = new BooleanProperty("autosave.notification", false);
100
101    protected static final class AutosaveLayerInfo {
102        private final OsmDataLayer layer;
103        private String layerName;
104        private String layerFileName;
105        private final Deque<File> backupFiles = new LinkedList<>();
106
107        AutosaveLayerInfo(OsmDataLayer layer) {
108            this.layer = layer;
109        }
110    }
111
112    private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
113    private final Set<DataSet> changedDatasets = new HashSet<>();
114    private final List<AutosaveLayerInfo> layersInfo = new ArrayList<>();
115    private final Object layersLock = new Object();
116    private final Deque<File> deletedLayers = new LinkedList<>();
117
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);
120
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
130    /**
131     * Starts the autosave background task.
132     */
133    public void schedule() {
134        if (PROP_INTERVAL.get() > 0) {
135
136            if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
137                Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
138                return;
139            }
140            if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
141                Logging.warn(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
142                return;
143            }
144
145            File[] files = deletedLayersDir.listFiles();
146            if (files != null) {
147                for (File f: files) {
148                    deletedLayers.add(f); // FIXME: sort by mtime
149                }
150            }
151
152            new Timer(true).schedule(this, TimeUnit.SECONDS.toMillis(1), TimeUnit.SECONDS.toMillis(PROP_INTERVAL.get()));
153            MainApplication.getLayerManager().addAndFireLayerChangeListener(this);
154        }
155    }
156
157    private static String getFileName(String layerName, int index) {
158        String result = layerName;
159        for (char illegalCharacter : ILLEGAL_CHARACTERS) {
160            result = result.replaceAll(Pattern.quote(String.valueOf(illegalCharacter)),
161                    '&' + String.valueOf((int) illegalCharacter) + ';');
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) {
175                if (info != layer && filename.equals(info.layerFileName)) {
176                    foundTheSame = true;
177                    break;
178                }
179            }
180
181            if (!foundTheSame) {
182                layer.layerFileName = filename;
183                return;
184            }
185
186            index++;
187        }
188    }
189
190    protected File getNewLayerFile(AutosaveLayerInfo layer, Date now, int startIndex) {
191        int index = startIndex;
192        while (true) {
193            String filename = String.format("%1$s_%2$tY%2$tm%2$td_%2$tH%2$tM%2$tS%2$tL%3$s",
194                    layer.layerFileName, now, index == 0 ? "" : ('_' + Integer.toString(index)));
195            File result = new File(autosaveDir, filename + '.' + Config.getPref().get("autosave.extension", "osm"));
196            try {
197                if (index > PROP_INDEX_LIMIT.get())
198                    throw new IOException("index limit exceeded");
199                if (result.createNewFile()) {
200                    createNewPidFile(autosaveDir, filename);
201                    return result;
202                } else {
203                    Logging.warn(tr("Unable to create file {0}, other filename will be used", result.getAbsolutePath()));
204                }
205            } catch (IOException e) {
206                Logging.log(Logging.LEVEL_ERROR, tr("IOError while creating file, autosave will be skipped: {0}", e.getMessage()), e);
207                return null;
208            }
209            index++;
210        }
211    }
212
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) {
218            Logging.error(t);
219        }
220    }
221
222    private void savelayer(AutosaveLayerInfo info) {
223        if (!info.layer.getName().equals(info.layerName)) {
224            setLayerFileName(info);
225            info.layerName = info.layer.getName();
226        }
227        if (changedDatasets.remove(info.layer.data)) {
228            File file = getNewLayerFile(info, new Date(), 0);
229            if (file != null) {
230                info.backupFiles.add(file);
231                new OsmExporter().exportData(file, info.layer, true /* no backup with appended ~ */);
232            }
233        }
234        while (info.backupFiles.size() > PROP_FILES_PER_LAYER.get()) {
235            File oldFile = info.backupFiles.remove();
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}"));
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();
250                if (PROP_NOTIFICATION.get() && !layersInfo.isEmpty()) {
251                    GuiHelper.runInEDT(this::displayNotification);
252                }
253            } catch (RuntimeException t) { // NOPMD
254                // Don't let exception stop time thread
255                Logging.error("Autosave failed:");
256                Logging.error(t);
257            }
258        }
259    }
260
261    protected void displayNotification() {
262        new Notification(tr("Your work has been saved automatically."))
263        .setDuration(Notification.TIME_SHORT)
264        .show();
265    }
266
267    @Override
268    public void layerOrderChanged(LayerOrderChangeEvent e) {
269        // Do nothing
270    }
271
272    private void registerNewlayer(OsmDataLayer layer) {
273        synchronized (layersLock) {
274            layer.data.addDataSetListener(datasetAdapter);
275            layersInfo.add(new AutosaveLayerInfo(layer));
276        }
277    }
278
279    @Override
280    public void layerAdded(LayerAddEvent e) {
281        if (e.getAddedLayer() instanceof OsmDataLayer) {
282            registerNewlayer((OsmDataLayer) e.getAddedLayer());
283        }
284    }
285
286    @Override
287    public void layerRemoving(LayerRemoveEvent e) {
288        if (e.getRemovedLayer() instanceof OsmDataLayer) {
289            synchronized (layersLock) {
290                OsmDataLayer osmLayer = (OsmDataLayer) e.getRemovedLayer();
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
297                        savelayer(info);
298                        File lastFile = info.backupFiles.pollLast();
299                        if (lastFile != null) {
300                            moveToDeletedLayersFolder(lastFile);
301                        }
302                        for (File file: info.backupFiles) {
303                            if (Utils.deleteFile(file)) {
304                                Utils.deleteFile(getPidFile(file));
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
320    protected File getPidFile(File osmFile) {
321        return new File(autosaveDir, osmFile.getName().replaceFirst("[.][^.]+$", ".pid"));
322    }
323
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     */
329    public List<File> getUnsavedLayersFiles() {
330        List<File> result = new ArrayList<>();
331        File[] files = autosaveDir.listFiles(OsmImporter.FILE_FILTER);
332        if (files == null)
333            return result;
334        for (File file: files) {
335            if (file.isFile()) {
336                boolean skipFile = false;
337                File pidFile = getPidFile(file);
338                if (pidFile.exists()) {
339                    try (BufferedReader reader = Files.newBufferedReader(pidFile.toPath(), StandardCharsets.UTF_8)) {
340                        String jvmId = reader.readLine();
341                        if (jvmId != null) {
342                            String pid = jvmId.split("@")[0];
343                            skipFile = jvmPerfDataFileExists(pid);
344                        }
345                    } catch (IOException | SecurityException t) {
346                        Logging.error(t);
347                    }
348                }
349                if (!skipFile) {
350                    result.add(file);
351                }
352            }
353        }
354        return result;
355    }
356
357    private static boolean jvmPerfDataFileExists(final String jvmId) {
358        File jvmDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "hsperfdata_" + System.getProperty("user.name"));
359        if (jvmDir.exists() && jvmDir.canRead()) {
360            File[] files = jvmDir.listFiles((FileFilter) file -> file.getName().equals(jvmId) && file.isFile());
361            return files != null && files.length == 1;
362        }
363        return false;
364    }
365
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() {
371        List<File> files = getUnsavedLayersFiles();
372        final OpenFileTask openFileTsk = new OpenFileTask(files, null, tr("Restoring files"));
373        final Future<?> openFilesFuture = MainApplication.worker.submit(openFileTsk);
374        return MainApplication.worker.submit(() -> {
375            try {
376                // Wait for opened tasks to be generated.
377                openFilesFuture.get();
378                for (File f: openFileTsk.getSuccessfullyOpenedFiles()) {
379                    moveToDeletedLayersFolder(f);
380                }
381            } catch (InterruptedException | ExecutionException e) {
382                Logging.error(e);
383            }
384        });
385    }
386
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     */
395    private void moveToDeletedLayersFolder(File f) {
396        File backupFile = new File(deletedLayersDir, f.getName());
397        File pidFile = getPidFile(f);
398
399        if (backupFile.exists()) {
400            deletedLayers.remove(backupFile);
401            Utils.deleteFile(backupFile, marktr("Unable to delete old backup file {0}"));
402        }
403        if (f.renameTo(backupFile)) {
404            deletedLayers.add(backupFile);
405            Utils.deleteFile(pidFile);
406        } else {
407            Logging.warn(String.format("Could not move autosaved file %s to %s folder", f.getName(), deletedLayersDir.getName()));
408            // we cannot move to deleted folder, so just try to delete it directly
409            if (Utils.deleteFile(f, marktr("Unable to delete backup file {0}"))) {
410                Utils.deleteFile(pidFile, marktr("Unable to delete PID file {0}"));
411            }
412        }
413        while (deletedLayers.size() > PROP_DELETED_LAYERS.get()) {
414            File next = deletedLayers.remove();
415            if (next == null) {
416                break;
417            }
418            Utils.deleteFile(next, marktr("Unable to delete archived backup file {0}"));
419        }
420    }
421
422    /**
423     * Mark all unsaved layers as deleted. They are still preserved in the deleted layers folder.
424     */
425    public void discardUnsavedLayers() {
426        for (File f: getUnsavedLayersFiles()) {
427            moveToDeletedLayersFolder(f);
428        }
429    }
430}
Note: See TracBrowser for help on using the repository browser.