source: josm/trunk/src/org/openstreetmap/josm/data/AutosaveTask.java@ 6643

Last change on this file since 6643 was 6643, checked in by Don-vip, 10 years ago

global replacement of e.printStackTrace() by Main.error(e)

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