blob: 836a6c95d4eb0d3b725ec4b2a76f1fdc0663496d [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.history.core;
import com.intellij.history.core.changes.ChangeSet;
import com.intellij.history.utils.LocalHistoryLog;
import com.intellij.ide.BrowserUtil;
import com.intellij.ide.actions.ShowFilePathAction;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.newvfs.ManagingFS;
import com.intellij.util.Consumer;
import com.intellij.util.io.storage.AbstractStorage;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.event.HyperlinkEvent;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.MessageFormat;
public class ChangeListStorageImpl implements ChangeListStorage {
private static final int VERSION = 5;
private static final String STORAGE_FILE = "changes";
private final File myStorageDir;
private LocalHistoryStorage myStorage;
private long myLastId;
private boolean isCompletelyBroken = false;
public ChangeListStorageImpl(File storageDir) throws IOException {
myStorageDir = storageDir;
initStorage(myStorageDir);
}
private synchronized void initStorage(File storageDir) throws IOException {
String path = storageDir.getPath() + "/" + STORAGE_FILE;
LocalHistoryStorage result = new LocalHistoryStorage(path);
long fsTimestamp = getVFSTimestamp();
int storedVersion = result.getVersion();
boolean versionMismatch = storedVersion != VERSION;
boolean timestampMismatch = result.getFSTimestamp() != fsTimestamp;
if (versionMismatch || timestampMismatch) {
if (versionMismatch) {
LocalHistoryLog.LOG.info(MessageFormat.format(
"local history version mismatch (was: {0}, expected: {1}), rebuilding...", storedVersion, VERSION));
}
if (timestampMismatch) LocalHistoryLog.LOG.info("FS has been rebuild, rebuilding local history...");
result.dispose();
if (!FileUtil.delete(storageDir)) {
throw new IOException("cannot clear storage dir: " + storageDir);
}
result = new LocalHistoryStorage(path);
result.setVersion(VERSION);
result.setFSTimestamp(fsTimestamp);
}
myLastId = result.getLastId();
myStorage = result;
}
private static long getVFSTimestamp() {
return ManagingFS.getInstance().getCreationTimestamp();
}
private void handleError(Throwable e, @Nullable String message) {
long storageTimestamp = -1;
long vfsTimestamp = getVFSTimestamp();
long timestamp = System.currentTimeMillis();
try {
storageTimestamp = myStorage.getFSTimestamp();
}
catch (Exception ex) {
LocalHistoryLog.LOG.warn("cannot read storage timestamp", ex);
}
LocalHistoryLog.LOG.error("Local history is broken" +
"(version:" + VERSION +
",current timestamp:" + DateFormat.getDateTimeInstance().format(timestamp) +
",storage timestamp:" + DateFormat.getDateTimeInstance().format(storageTimestamp) +
",vfs timestamp:" + DateFormat.getDateTimeInstance().format(vfsTimestamp) + ")\n" + message, e);
myStorage.dispose();
try {
FileUtil.delete(myStorageDir);
initStorage(myStorageDir);
}
catch (Throwable ex) {
LocalHistoryLog.LOG.error("cannot recreate storage", ex);
isCompletelyBroken = true;
}
notifyUser("Local History storage file has become corrupted and will be rebuilt.");
}
public static void notifyUser(String message) {
final String logFile = PathManager.getLogPath();
/*String createIssuePart = "<br>" +
"<br>" +
"Please attach log files from <a href=\"file\">" + logFile + "</a><br>" +
"to the <a href=\"url\">YouTrack issue</a>";*/
Notifications.Bus.notify(new Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID,
"Local History is broken",
message /*+ createIssuePart*/,
NotificationType.ERROR,
new NotificationListener() {
@Override
public void hyperlinkUpdate(@NotNull Notification notification,
@NotNull HyperlinkEvent event) {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
if ("url".equals(event.getDescription())) {
BrowserUtil.browse("http://youtrack.jetbrains.net/issue/IDEA-71270");
}
else {
File file = new File(logFile);
ShowFilePathAction.openFile(file);
}
}
}
}), null);
}
public synchronized void close() {
myStorage.dispose();
}
public synchronized long nextId() {
return ++myLastId;
}
@Nullable
public synchronized ChangeSetHolder readPrevious(int id, TIntHashSet recursionGuard) {
if (isCompletelyBroken) return null;
int prevId = 0;
try {
prevId = id == -1 ? myStorage.getLastRecord() : doReadPrevSafely(id, recursionGuard);
if (prevId == 0) return null;
return doReadBlock(prevId);
}
catch (Throwable e) {
String message = null;
if (prevId != 0) {
try {
Pair<Long, Integer> prevOS = myStorage.getOffsetAndSize(prevId);
long prevRecordTimestamp = myStorage.getTimestamp(prevId);
int lastRecord = myStorage.getLastRecord();
Pair<Long, Integer> lastOS = myStorage.getOffsetAndSize(lastRecord);
long lastRecordTimestamp = myStorage.getTimestamp(lastRecord);
message = "invalid record is: " + prevId + " offset: " + prevOS.first + " size: " + prevOS.second
+ " (created " + DateFormat.getDateTimeInstance().format(prevRecordTimestamp) + ") "
+ "last record is: " + lastRecord + " offset: " + lastOS.first + " size: " + lastOS.second
+ " (created " + DateFormat.getDateTimeInstance().format(lastRecordTimestamp) + ")";
}
catch (Exception e1) {
message = "cannot retrieve more debug info: " + e1.getMessage();
}
}
handleError(e, message);
return null;
}
}
@NotNull
private ChangeSetHolder doReadBlock(int id) throws IOException {
DataInputStream in = myStorage.readStream(id);
try {
return new ChangeSetHolder(id, new ChangeSet(in));
}
finally {
in.close();
}
}
public synchronized void writeNextSet(ChangeSet changeSet) {
if (isCompletelyBroken) return;
try {
AbstractStorage.StorageDataOutput out = myStorage.writeStream(myStorage.createNextRecord(), true);
try {
changeSet.write(out);
}
finally {
out.close();
}
myStorage.setLastId(myLastId);
myStorage.force();
}
catch (IOException e) {
handleError(e, null);
}
}
public synchronized void purge(long period, int intervalBetweenActivities, Consumer<ChangeSet> processor) {
if (isCompletelyBroken) return;
TIntHashSet recursionGuard = new TIntHashSet(1000);
try {
int firstObsoleteId = findFirstObsoleteBlock(period, intervalBetweenActivities, recursionGuard);
if (firstObsoleteId == 0) return;
int eachBlockId = firstObsoleteId;
while (eachBlockId != 0) {
processor.consume(doReadBlock(eachBlockId).changeSet);
eachBlockId = doReadPrevSafely(eachBlockId, recursionGuard);
}
myStorage.deleteRecordsUpTo(firstObsoleteId);
myStorage.force();
}
catch (IOException e) {
handleError(e, null);
}
}
private int findFirstObsoleteBlock(long period, int intervalBetweenActivities, TIntHashSet recursionGuard) throws IOException {
long prevTimestamp = 0;
long length = 0;
int last = myStorage.getLastRecord();
while (last != 0) {
long t = myStorage.getTimestamp(last);
if (prevTimestamp == 0) prevTimestamp = t;
long delta = prevTimestamp - t;
prevTimestamp = t;
// we sum only intervals between changes during one 'day' (intervalBetweenActivities) and add '1' between two 'days'
length += delta < intervalBetweenActivities ? delta : 1;
if (length >= period) return last;
last = doReadPrevSafely(last, recursionGuard);
}
return 0;
}
private int doReadPrevSafely(int id, TIntHashSet recursionGuard) throws IOException {
recursionGuard.add(id);
int prev = myStorage.getPrevRecord(id);
if (!recursionGuard.add(prev)) throw new IOException("Recursive records found");
return prev;
}
}