package com.siberika.idea.pascal.lang.compiled; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.VirtualFile; import com.siberika.idea.pascal.PPUFileType; import com.siberika.idea.pascal.PascalBundle; import com.siberika.idea.pascal.PascalException; import com.siberika.idea.pascal.jps.sdk.PascalSdkData; import com.siberika.idea.pascal.jps.sdk.PascalSdkUtil; import com.siberika.idea.pascal.sdk.BasePascalSdkType; import com.siberika.idea.pascal.sdk.FPCSdkType; import com.siberika.idea.pascal.util.DocUtil; import com.siberika.idea.pascal.util.ModuleUtil; import com.siberika.idea.pascal.util.StrUtil; import com.siberika.idea.pascal.util.SysUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.text.ParseException; import java.util.Collection; import java.util.concurrent.TimeUnit; /** * Author: George Bakhtadze * Date: 26/11/2013 */ public class PPUDecompilerCache { private static final Logger LOG = Logger.getInstance(PPUDecompilerCache.class); public static final String PPUDUMP_OPTIONS_COMMON = "-Vhisd"; public static final String PPUDUMP_OPTIONS_FORMAT = "-Fx"; public static final String PPUDUMP_OPTIONS_VERSION = "-V"; public static final String PPUDUMP_VERSION_MIN = "2.7.1"; private final Module module; private final LoadingCache<String, PPUDumpParser.Section> cache; public PPUDecompilerCache(@NotNull Module module) { this.module = module; cache = CacheBuilder.newBuilder().expireAfterAccess(2, TimeUnit.HOURS).build(new Loader()); } public static String decompile(Module module, String filename, @Nullable VirtualFile file) { Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); if (null == sdk) { return PascalBundle.message("decompile.wrong.sdk"); } PPUDecompilerCache decompilerCache; //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (sdk) { decompilerCache = (PPUDecompilerCache) BasePascalSdkType.getAdditionalData(sdk).getValue(PascalSdkData.Keys.DECOMPILER_CACHE.getKey()); if ((null == decompilerCache) || (decompilerCache.module != module)) { decompilerCache = new PPUDecompilerCache(module); BasePascalSdkType.getAdditionalData(sdk).setValue(PascalSdkData.Keys.DECOMPILER_CACHE.getKey(), decompilerCache); } } String unitName = FileUtil.getNameWithoutExtension(com.siberika.idea.pascal.jps.util.FileUtil.getFilename(filename)); PPUDumpParser.Section stub = decompilerCache.getContents(unitName, file); return stub != null ? stub.getResult() : ""; } private class Loader extends CacheLoader<String, PPUDumpParser.Section> { @Override public PPUDumpParser.Section load(@NotNull String key) throws Exception { Sdk sdk = ModuleRootManager.getInstance(module).getSdk(); if (null == sdk) { return new PPUDumpParser.Section(PascalBundle.message("decompile.wrong.sdk")); } if ((sdk.getHomePath() == null) || !(sdk.getSdkType() instanceof FPCSdkType)) { return new PPUDumpParser.Section(PascalBundle.message("decompile.wrong.sdktype")); } Collection<VirtualFile> files = ModuleUtil.getCompiledByNameNoCase(module, key, PPUFileType.INSTANCE); if (files.isEmpty()) { return new PPUDumpParser.Section(PascalBundle.message("decompile.file.notfound", key)); } File ppuDump = BasePascalSdkType.getDecompilerCommand(sdk, PascalSdkUtil.getPPUDumpExecutable(sdk.getHomePath() != null ? sdk.getHomePath() : "")); String xml = ""; try { if (!ppuDump.isFile() || !ppuDump.canExecute()) { return new PPUDumpParser.Section(PascalBundle.message("decompile.wrong.ppudump", ppuDump.getCanonicalPath())); } xml = SysUtils.runAndGetStdOut(sdk.getHomePath(), ppuDump.getCanonicalPath(), PPUDUMP_OPTIONS_COMMON, PPUDUMP_OPTIONS_FORMAT, files.iterator().next().getPath()); if (xml != null) { return PPUDumpParser.parse(xml, PPUDecompilerCache.this); } else { return new PPUDumpParser.Section(PascalBundle.message("decompile.empty.result")); } } catch (IOException e) { LOG.info("I/O error: " + e.getMessage(), e); return new PPUDumpParser.Section(PascalBundle.message("decompile.io.error")); } catch (ParseException e) { LOG.info("Parse error: " + e.getMessage(), e); String ver = getPPUDumpVersion(ppuDump); if (ver.compareTo(PPUDUMP_VERSION_MIN) < 0) { return new PPUDumpParser.Section(PascalBundle.message("decompile.version.error", ver, PPUDUMP_VERSION_MIN)); } else { return new PPUDumpParser.Section(PascalBundle.message("decompile.parse.error", xml)); } } catch (PascalException e1) { return new PPUDumpParser.Section(e1.getMessage()); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { LOG.info("Unknown error: " + e.getMessage(), e); return new PPUDumpParser.Section(PascalBundle.message("decompile.unknown.error", StrUtil.limit(xml, 2048))); } } } static String getPPUDumpVersion(File ppuDump) { String res = ""; try { res = SysUtils.runAndGetStdOut(ppuDump.getParent(), ppuDump.getCanonicalPath(), PPUDUMP_OPTIONS_COMMON, PPUDUMP_OPTIONS_VERSION); if (res != null) { int i1 = res.indexOf("Version"); int i2 = res.indexOf("\n"); if ((i1 < res.length()) && (i2 > i1)) { res = res.substring(i1 + 8, i2); } } } catch (Exception e) { LOG.info("Error: " + e.getMessage(), e); } return res; } /** * Retrieves decompiled contents from cache. * Seems that compiled modules can't be found during PSI reparse. For that case virtualFile parameter is used. * @param unitName compiled unit name * @param virtualFile if this is not null it should be used instead of search by name * @return decompiled data */ PPUDumpParser.Section getContents(@NotNull String unitName, @Nullable VirtualFile virtualFile) { VirtualFile file = virtualFile; if (null == file) { Collection<VirtualFile> files = ModuleUtil.getCompiledByNameNoCase(module, unitName, PPUFileType.INSTANCE); if (!files.isEmpty()) { file = files.iterator().next(); } } if (file != null) { try { String key = getKey(file.getName()); PPUDumpParser.Section section = cache.getIfPresent(key); boolean needReparsePsi = null == section; section = cache.get(key); if (section.isError()) { LOG.info("ERROR: Invalidating ppu cache for key: " + key); cache.invalidate(key); } else if (needReparsePsi) { DocUtil.reparsePsi(module.getProject(), file); } return section; } catch (Exception e) { if (e.getCause() instanceof ProcessCanceledException) { throw (ProcessCanceledException) e.getCause(); } else { LOG.info(String.format("Error: Exception while decompiling unit %s: %s", unitName, e.getMessage()), e); } } } return new PPUDumpParser.Section(PascalBundle.message("decompile.unit.not.found", unitName)); } private String getKey(String unitName) { return FileUtil.getNameWithoutExtension(unitName); } }