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

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

fix #5288 - multiple JOSM instances: Bogus crash recovery attempt

  • Property svn:eol-style set to native
File size: 15.5 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.layer.Layer;
37import org.openstreetmap.josm.gui.layer.OsmDataLayer;
38import org.openstreetmap.josm.io.OsmExporter;
39import org.openstreetmap.josm.io.OsmImporter;
40
41/**
42 * Saves data layers periodically so they can be recovered in case of a crash.
43 *
44 * There are 2 directories
45 * - autosave dir: copies of the currently open data layers are saved here every
46 * PROP_INTERVAL seconds. When a data layer is closed normally, the corresponding
47 * files are removed. If this dir is non-empty on start, JOSM assumes
48 * that it crashed last time.
49 * - deleted layers dir: "secondary archive" - when autosaved layers are restored
50 * they are copied to this directory. We cannot keep them in the autosave folder,
51 * but just deleting it would be dangerous: Maybe a feature inside the file
52 * caused JOSM to crash. If the data is valuable, the user can still try to
53 * open with another versions of JOSM or fix the problem manually.
54 *
55 * The deleted layers dir keeps at most PROP_DELETED_LAYERS files.
56 */
57public class AutosaveTask extends TimerTask implements LayerChangeListener, Listener {
58
59 private static final char[] ILLEGAL_CHARACTERS = { '/', '\n', '\r', '\t', '\0', '\f', '`', '?', '*', '\\', '<', '>', '|', '\"', ':' };
60 private static final String AUTOSAVE_DIR = "autosave";
61 private static final String DELETED_LAYERS_DIR = "autosave/deleted_layers";
62
63 public static final BooleanProperty PROP_AUTOSAVE_ENABLED = new BooleanProperty("autosave.enabled", true);
64 public static final IntegerProperty PROP_FILES_PER_LAYER = new IntegerProperty("autosave.filesPerLayer", 1);
65 public static final IntegerProperty PROP_DELETED_LAYERS = new IntegerProperty("autosave.deletedLayersBackupCount", 5);
66 public static final IntegerProperty PROP_INTERVAL = new IntegerProperty("autosave.interval", 5 * 60);
67 public static final IntegerProperty PROP_INDEX_LIMIT = new IntegerProperty("autosave.index-limit", 1000);
68
69 private static class AutosaveLayerInfo {
70 OsmDataLayer layer;
71 String layerName;
72 String layerFileName;
73 final Deque<File> backupFiles = new LinkedList<File>();
74 }
75
76 private final DataSetListenerAdapter datasetAdapter = new DataSetListenerAdapter(this);
77 private final Set<DataSet> changedDatasets = new HashSet<DataSet>();
78 private final List<AutosaveLayerInfo> layersInfo = new ArrayList<AutosaveLayerInfo>();
79 private Timer timer;
80 private final Object layersLock = new Object();
81 private final Deque<File> deletedLayers = new LinkedList<File>();
82
83 private final File autosaveDir = new File(Main.pref.getPreferencesDir() + AUTOSAVE_DIR);
84 private final File deletedLayersDir = new File(Main.pref.getPreferencesDir() + DELETED_LAYERS_DIR);
85
86 public void schedule() {
87 if (PROP_INTERVAL.get() > 0) {
88
89 if (!autosaveDir.exists() && !autosaveDir.mkdirs()) {
90 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", autosaveDir.getAbsolutePath()));
91 return;
92 }
93 if (!deletedLayersDir.exists() && !deletedLayersDir.mkdirs()) {
94 System.out.println(tr("Unable to create directory {0}, autosave will be disabled", deletedLayersDir.getAbsolutePath()));
95 return;
96 }
97
98 for (File f: deletedLayersDir.listFiles()) {
99 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}
Note: See TracBrowser for help on using the repository browser.