/* * Copyright 2000-2016 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.execution; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.io.DataExternalizer; import com.intellij.util.io.EnumeratorStringDescriptor; import com.intellij.util.io.IOUtil; import com.intellij.util.io.PersistentHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.DataInput; import java.io.DataOutput; import java.io.File; import java.io.IOException; import java.util.Date; import java.util.Map; import java.util.concurrent.ScheduledFuture; /** * @author Dmitry Avdeev */ public class TestStateStorage implements Disposable { private static final File TEST_HISTORY_PATH = new File(PathManager.getSystemPath(), "testHistory"); private static final int CURRENT_VERSION = 1; private final File myFile; public static File getTestHistoryRoot(Project project) { return new File(TEST_HISTORY_PATH, project.getLocationHash()); } public static class Record { public final int magnitude; public final long configurationHash; public final Date date; public Record(int magnitude, Date date, long configurationHash) { this.magnitude = magnitude; this.date = date; this.configurationHash = configurationHash; } } private static final Logger LOG = Logger.getInstance(TestStateStorage.class); @Nullable private PersistentHashMap<String, Record> myMap; private volatile ScheduledFuture<?> myMapFlusher; public static TestStateStorage getInstance(@NotNull Project project) { return ServiceManager.getService(project, TestStateStorage.class); } public TestStateStorage(Project project) { String directoryPath = getTestHistoryRoot(project).getPath(); myFile = new File(directoryPath + "/testStateMap"); FileUtilRt.createParentDirs(myFile); try { myMap = initializeMap(); } catch (IOException e) { LOG.error(e); } myMapFlusher = FlushingDaemon.everyFiveSeconds(this::flushMap); } private PersistentHashMap<String, Record> initializeMap() throws IOException { return IOUtil.openCleanOrResetBroken(getComputable(myFile), myFile); } private synchronized void flushMap() { if (myMapFlusher == null) return; // disposed if (myMap != null && myMap.isDirty()) myMap.force(); } @NotNull private static ThrowableComputable<PersistentHashMap<String, Record>, IOException> getComputable(final File file) { return () -> new PersistentHashMap<>(file, EnumeratorStringDescriptor.INSTANCE, new DataExternalizer<Record>() { @Override public void save(@NotNull DataOutput out, Record value) throws IOException { out.writeInt(value.magnitude); out.writeLong(value.date.getTime()); out.writeLong(value.configurationHash); } @Override public Record read(@NotNull DataInput in) throws IOException { return new Record(in.readInt(), new Date(in.readLong()), in.readLong()); } }, 4096, CURRENT_VERSION); } @Nullable public synchronized Record getState(String testUrl) { try { return myMap == null ? null : myMap.get(testUrl); } catch (IOException e) { thingsWentWrongLetsReinitialize(e, "Can't get state for " + testUrl); return null; } } public synchronized void removeState(String url) { if (myMap != null) { try { myMap.remove(url); } catch (IOException e) { thingsWentWrongLetsReinitialize(e, "Can't remove state for " + url); } } } @Nullable public synchronized Map<String, Record> getRecentTests(int limit, Date since) { if (myMap == null) return null; Map<String, Record> result = ContainerUtil.newHashMap(); try { for (String key : myMap.getAllKeysWithExistingMapping()) { Record record = myMap.get(key); if (record != null && record.date.compareTo(since) > 0) { result.put(key, record); if (result.size() >= limit) { break; } } } } catch (IOException e) { thingsWentWrongLetsReinitialize(e, "Can't get recent tests"); } return result; } public synchronized void writeState(@NotNull String testUrl, Record record) { if (myMap == null) return; try { myMap.put(testUrl, record); } catch (IOException e) { thingsWentWrongLetsReinitialize(e, "Can't write state for " + testUrl); } } @Override public synchronized void dispose() { myMapFlusher.cancel(false); myMapFlusher = null; if (myMap == null) return; try { myMap.close(); } catch (IOException e) { LOG.error(e); } finally { myMap = null; } } private void thingsWentWrongLetsReinitialize(IOException e, String message) { try { if (myMap != null) { try { myMap.close(); } catch (IOException ignore) { } IOUtil.deleteAllFilesStartingWith(myFile); } myMap = initializeMap(); LOG.error(message, e); } catch (IOException e1) { LOG.error("Cannot repair", e1); myMap = null; } } }