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

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

Rework console output:

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