Esempio n. 1
0
/** @author max */
public class PersistentFSImpl extends PersistentFS implements ApplicationComponent {
  private static final Logger LOG =
      Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.PersistentFS");

  private final MessageBus myEventBus;

  private final ReadWriteLock myRootsLock = new ReentrantReadWriteLock();
  private final Map<String, VirtualFileSystemEntry> myRoots =
      ContainerUtil.newTroveMap(FileUtil.PATH_HASHING_STRATEGY);
  private final TIntObjectHashMap<VirtualFileSystemEntry> myRootsById =
      new TIntObjectHashMap<VirtualFileSystemEntry>();

  private final ConcurrentIntObjectMap<VirtualFileSystemEntry> myIdToDirCache =
      ContainerUtil.createConcurrentIntObjectMap();
  private final Object myInputLock = new Object();

  private final AtomicBoolean myShutDown = new AtomicBoolean(false);

  @SuppressWarnings("FieldCanBeLocal")
  private final LowMemoryWatcher myWatcher =
      LowMemoryWatcher.register(
          new Runnable() {
            @Override
            public void run() {
              clearIdCache();
            }
          });

  public PersistentFSImpl(@NotNull MessageBus bus) {
    myEventBus = bus;
    ShutDownTracker.getInstance()
        .registerShutdownTask(
            new Runnable() {
              @Override
              public void run() {
                performShutdown();
              }
            });
  }

  @Override
  public void initComponent() {
    FSRecords.connect();
  }

  @Override
  public void disposeComponent() {
    performShutdown();
  }

  private void performShutdown() {
    if (myShutDown.compareAndSet(false, true)) {
      LOG.info("VFS dispose started");
      FSRecords.dispose();
      LOG.info("VFS dispose completed");
    }
  }

  @Override
  @NonNls
  @NotNull
  public String getComponentName() {
    return "app.component.PersistentFS";
  }

  @Override
  public boolean areChildrenLoaded(@NotNull final VirtualFile dir) {
    return areChildrenLoaded(getFileId(dir));
  }

  @Override
  public long getCreationTimestamp() {
    return FSRecords.getCreationTimestamp();
  }

  @NotNull
  private static NewVirtualFileSystem getDelegate(@NotNull VirtualFile file) {
    return (NewVirtualFileSystem) file.getFileSystem();
  }

  @Override
  public boolean wereChildrenAccessed(@NotNull final VirtualFile dir) {
    return FSRecords.wereChildrenAccessed(getFileId(dir));
  }

  @Override
  @NotNull
  public String[] list(@NotNull final VirtualFile file) {
    int id = getFileId(file);

    FSRecords.NameId[] nameIds = FSRecords.listAll(id);
    if (!areChildrenLoaded(id)) {
      nameIds = persistAllChildren(file, id, nameIds);
    }
    return ContainerUtil.map2Array(
        nameIds,
        String.class,
        new Function<FSRecords.NameId, String>() {
          @Override
          public String fun(FSRecords.NameId id) {
            return id.name.toString();
          }
        });
  }

  @Override
  @NotNull
  public String[] listPersisted(@NotNull VirtualFile parent) {
    return listPersisted(FSRecords.list(getFileId(parent)));
  }

  @NotNull
  private static String[] listPersisted(@NotNull int[] childrenIds) {
    String[] names = ArrayUtil.newStringArray(childrenIds.length);
    for (int i = 0; i < childrenIds.length; i++) {
      names[i] = FSRecords.getName(childrenIds[i]);
    }
    return names;
  }

  @NotNull
  private static FSRecords.NameId[] persistAllChildren(
      @NotNull final VirtualFile file, final int id, @NotNull FSRecords.NameId[] current) {
    final NewVirtualFileSystem fs = replaceWithNativeFS(getDelegate(file));

    String[] delegateNames = VfsUtil.filterNames(fs.list(file));
    if (delegateNames.length == 0 && current.length > 0) {
      return current;
    }

    Set<String> toAdd = ContainerUtil.newHashSet(delegateNames);
    for (FSRecords.NameId nameId : current) {
      toAdd.remove(nameId.name.toString());
    }

    final TIntArrayList childrenIds = new TIntArrayList(current.length + toAdd.size());
    final List<FSRecords.NameId> nameIds =
        ContainerUtil.newArrayListWithCapacity(current.length + toAdd.size());
    for (FSRecords.NameId nameId : current) {
      childrenIds.add(nameId.id);
      nameIds.add(nameId);
    }
    for (String newName : toAdd) {
      FakeVirtualFile child = new FakeVirtualFile(file, newName);
      FileAttributes attributes = fs.getAttributes(child);
      if (attributes != null) {
        int childId = createAndFillRecord(fs, child, id, attributes);
        childrenIds.add(childId);
        nameIds.add(new FSRecords.NameId(childId, FileNameCache.storeName(newName), newName));
      }
    }

    FSRecords.updateList(id, childrenIds.toNativeArray());
    setChildrenCached(id);

    return nameIds.toArray(new FSRecords.NameId[nameIds.size()]);
  }

  public static void setChildrenCached(int id) {
    int flags = FSRecords.getFlags(id);
    FSRecords.setFlags(id, flags | CHILDREN_CACHED_FLAG, true);
  }

  @Override
  @NotNull
  public FSRecords.NameId[] listAll(@NotNull VirtualFile parent) {
    final int parentId = getFileId(parent);

    FSRecords.NameId[] nameIds = FSRecords.listAll(parentId);
    if (!areChildrenLoaded(parentId)) {
      return persistAllChildren(parent, parentId, nameIds);
    }

    return nameIds;
  }

  private static boolean areChildrenLoaded(final int parentId) {
    return (FSRecords.getFlags(parentId) & CHILDREN_CACHED_FLAG) != 0;
  }

  @Override
  @Nullable
  public DataInputStream readAttribute(
      @NotNull final VirtualFile file, @NotNull final FileAttribute att) {
    return FSRecords.readAttributeWithLock(getFileId(file), att);
  }

  @Override
  @NotNull
  public DataOutputStream writeAttribute(
      @NotNull final VirtualFile file, @NotNull final FileAttribute att) {
    return FSRecords.writeAttribute(getFileId(file), att);
  }

  @Nullable
  private static DataInputStream readContent(@NotNull VirtualFile file) {
    return FSRecords.readContent(getFileId(file));
  }

  @Nullable
  private static DataInputStream readContentById(int contentId) {
    return FSRecords.readContentById(contentId);
  }

  @NotNull
  private static DataOutputStream writeContent(@NotNull VirtualFile file, boolean readOnly) {
    return FSRecords.writeContent(getFileId(file), readOnly);
  }

  private static void writeContent(
      @NotNull VirtualFile file, ByteSequence content, boolean readOnly) throws IOException {
    FSRecords.writeContent(getFileId(file), content, readOnly);
  }

  @Override
  public int storeUnlinkedContent(@NotNull byte[] bytes) {
    return FSRecords.storeUnlinkedContent(bytes);
  }

  @Override
  public int getModificationCount(@NotNull final VirtualFile file) {
    return FSRecords.getModCount(getFileId(file));
  }

  @Override
  public int getCheapFileSystemModificationCount() {
    return FSRecords.getLocalModCount();
  }

  @Override
  public int getFilesystemModificationCount() {
    return FSRecords.getModCount();
  }

  private static boolean writeAttributesToRecord(
      final int id,
      final int parentId,
      @NotNull VirtualFile file,
      @NotNull NewVirtualFileSystem fs,
      @NotNull FileAttributes attributes) {
    String name = file.getName();
    if (!name.isEmpty()) {
      if (namesEqual(fs, name, FSRecords.getName(id)))
        return false; // TODO: Handle root attributes change.
    } else {
      if (areChildrenLoaded(id)) return false; // TODO: hack
    }

    FSRecords.writeAttributesToRecord(id, parentId, attributes, name);

    return true;
  }

  @Override
  public int getFileAttributes(int id) {
    assert id > 0;
    //noinspection MagicConstant
    return FSRecords.getFlags(id);
  }

  @Override
  public boolean isDirectory(@NotNull final VirtualFile file) {
    return isDirectory(getFileAttributes(getFileId(file)));
  }

  private static int getParent(final int id) {
    assert id > 0;
    return FSRecords.getParent(id);
  }

  private static boolean namesEqual(@NotNull VirtualFileSystem fs, @NotNull String n1, String n2) {
    return fs.isCaseSensitive() ? n1.equals(n2) : n1.equalsIgnoreCase(n2);
  }

  @Override
  public boolean exists(@NotNull final VirtualFile fileOrDirectory) {
    return ((VirtualFileWithId) fileOrDirectory).getId() > 0;
  }

  @Override
  public long getTimeStamp(@NotNull final VirtualFile file) {
    return FSRecords.getTimestamp(getFileId(file));
  }

  @Override
  public void setTimeStamp(@NotNull final VirtualFile file, final long modStamp)
      throws IOException {
    final int id = getFileId(file);
    FSRecords.setTimestamp(id, modStamp);
    getDelegate(file).setTimeStamp(file, modStamp);
  }

  private static int getFileId(@NotNull VirtualFile file) {
    final int id = ((VirtualFileWithId) file).getId();
    if (id <= 0) {
      throw new InvalidVirtualFileAccessException(file);
    }
    return id;
  }

  @Override
  public boolean isSymLink(@NotNull VirtualFile file) {
    return isSymLink(getFileAttributes(getFileId(file)));
  }

  @Override
  public String resolveSymLink(@NotNull VirtualFile file) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean isSpecialFile(@NotNull VirtualFile file) {
    return isSpecialFile(getFileAttributes(getFileId(file)));
  }

  @Override
  public boolean isWritable(@NotNull VirtualFile file) {
    return (getFileAttributes(getFileId(file)) & IS_READ_ONLY) == 0;
  }

  @Override
  public boolean isHidden(@NotNull VirtualFile file) {
    return (getFileAttributes(getFileId(file)) & IS_HIDDEN) != 0;
  }

  @Override
  public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag)
      throws IOException {
    getDelegate(file).setWritable(file, writableFlag);
    boolean oldWritable = isWritable(file);
    if (oldWritable != writableFlag) {
      processEvent(
          new VFilePropertyChangeEvent(
              this, file, VirtualFile.PROP_WRITABLE, oldWritable, writableFlag, false));
    }
  }

  @Override
  public int getId(
      @NotNull VirtualFile parent, @NotNull String childName, @NotNull NewVirtualFileSystem fs) {
    int parentId = getFileId(parent);
    int[] children = FSRecords.list(parentId);

    if (children.length > 0) {
      // fast path, check that some child has same nameId as given name, this avoid O(N) on
      // retrieving names for processing non-cached children
      int nameId = FSRecords.getNameId(childName);
      for (final int childId : children) {
        if (nameId == FSRecords.getNameId(childId)) {
          return childId;
        }
      }
      // for case sensitive system the above check is exhaustive in consistent state of vfs
    }

    for (final int childId : children) {
      if (namesEqual(fs, childName, FSRecords.getName(childId))) return childId;
    }

    final VirtualFile fake = new FakeVirtualFile(parent, childName);
    final FileAttributes attributes = fs.getAttributes(fake);
    if (attributes != null) {
      final int child = createAndFillRecord(fs, fake, parentId, attributes);
      FSRecords.updateList(parentId, ArrayUtil.append(children, child));
      return child;
    }

    return 0;
  }

  @Override
  public long getLength(@NotNull final VirtualFile file) {
    long len;
    if (mustReloadContent(file)) {
      len = reloadLengthFromDelegate(file, getDelegate(file));
    } else {
      final int id = getFileId(file);
      len = FSRecords.getLength(id);
    }

    return len;
  }

  @NotNull
  @Override
  public VirtualFile copyFile(
      Object requestor,
      @NotNull VirtualFile file,
      @NotNull VirtualFile parent,
      @NotNull String name)
      throws IOException {
    getDelegate(file).copyFile(requestor, file, parent, name);
    processEvent(new VFileCopyEvent(requestor, file, parent, name));

    final VirtualFile child = parent.findChild(name);
    if (child == null) {
      throw new IOException("Cannot create child");
    }
    return child;
  }

  @NotNull
  @Override
  public VirtualFile createChildDirectory(
      Object requestor, @NotNull VirtualFile parent, @NotNull String dir) throws IOException {
    getDelegate(parent).createChildDirectory(requestor, parent, dir);
    processEvent(new VFileCreateEvent(requestor, parent, dir, true, false));

    final VirtualFile child = parent.findChild(dir);
    if (child == null) {
      throw new IOException("Cannot create child directory '" + dir + "' at " + parent.getPath());
    }
    return child;
  }

  @NotNull
  @Override
  public VirtualFile createChildFile(
      Object requestor, @NotNull VirtualFile parent, @NotNull String file) throws IOException {
    getDelegate(parent).createChildFile(requestor, parent, file);
    processEvent(new VFileCreateEvent(requestor, parent, file, false, false));

    final VirtualFile child = parent.findChild(file);
    if (child == null) {
      throw new IOException("Cannot create child file '" + file + "' at " + parent.getPath());
    }
    return child;
  }

  @Override
  public void deleteFile(final Object requestor, @NotNull final VirtualFile file)
      throws IOException {
    final NewVirtualFileSystem delegate = getDelegate(file);
    delegate.deleteFile(requestor, file);

    if (!delegate.exists(file)) {
      processEvent(new VFileDeleteEvent(requestor, file, false));
    }
  }

  @Override
  public void renameFile(final Object requestor, @NotNull VirtualFile file, @NotNull String newName)
      throws IOException {
    getDelegate(file).renameFile(requestor, file, newName);
    String oldName = file.getName();
    if (!newName.equals(oldName)) {
      processEvent(
          new VFilePropertyChangeEvent(
              requestor, file, VirtualFile.PROP_NAME, oldName, newName, false));
    }
  }

  @Override
  @NotNull
  public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
    return contentsToByteArray(file, true);
  }

  @Override
  @NotNull
  public byte[] contentsToByteArray(@NotNull final VirtualFile file, boolean cacheContent)
      throws IOException {
    InputStream contentStream = null;
    boolean reloadFromDelegate;
    boolean outdated;
    int fileId;
    synchronized (myInputLock) {
      fileId = getFileId(file);
      outdated = checkFlag(fileId, MUST_RELOAD_CONTENT) || FSRecords.getLength(fileId) == -1L;
      reloadFromDelegate = outdated || (contentStream = readContent(file)) == null;
    }

    if (reloadFromDelegate) {
      final NewVirtualFileSystem delegate = getDelegate(file);

      final byte[] content;
      if (outdated) {
        // in this case, file can have out-of-date length. so, update it first (it's needed for
        // correct contentsToByteArray() work)
        // see IDEA-90813 for possible bugs
        FSRecords.setLength(fileId, delegate.getLength(file));
        content = delegate.contentsToByteArray(file);
      } else {
        // a bit of optimization
        content = delegate.contentsToByteArray(file);
        FSRecords.setLength(fileId, content.length);
      }

      ApplicationEx application = (ApplicationEx) ApplicationManager.getApplication();
      // we should cache every local files content
      // because the local history feature is currently depends on this cache,
      // perforce offline mode as well
      if ((!delegate.isReadOnly()
              ||
              // do not cache archive content unless asked
              cacheContent && !application.isInternal() && !application.isUnitTestMode())
          && content.length <= PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) {
        synchronized (myInputLock) {
          writeContent(file, new ByteSequence(content), delegate.isReadOnly());
          setFlag(file, MUST_RELOAD_CONTENT, false);
        }
      }

      return content;
    } else {
      try {
        final int length = (int) file.getLength();
        assert length >= 0 : file;
        return FileUtil.loadBytes(contentStream, length);
      } catch (IOException e) {
        throw FSRecords.handleError(e);
      }
    }
  }

  @Override
  @NotNull
  public byte[] contentsToByteArray(int contentId) throws IOException {
    final DataInputStream stream = readContentById(contentId);
    assert stream != null : contentId;
    return FileUtil.loadBytes(stream);
  }

  @Override
  @NotNull
  public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
    synchronized (myInputLock) {
      InputStream contentStream;
      if (mustReloadContent(file) || (contentStream = readContent(file)) == null) {
        NewVirtualFileSystem delegate = getDelegate(file);
        long len = reloadLengthFromDelegate(file, delegate);
        InputStream nativeStream = delegate.getInputStream(file);

        if (len > PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) return nativeStream;
        return createReplicator(file, nativeStream, len, delegate.isReadOnly());
      } else {
        return contentStream;
      }
    }
  }

  private static long reloadLengthFromDelegate(
      @NotNull VirtualFile file, @NotNull NewVirtualFileSystem delegate) {
    final long len = delegate.getLength(file);
    FSRecords.setLength(getFileId(file), len);
    return len;
  }

  private InputStream createReplicator(
      @NotNull final VirtualFile file,
      final InputStream nativeStream,
      final long fileLength,
      final boolean readOnly)
      throws IOException {
    if (nativeStream instanceof BufferExposingByteArrayInputStream) {
      // optimization
      BufferExposingByteArrayInputStream byteStream =
          (BufferExposingByteArrayInputStream) nativeStream;
      byte[] bytes = byteStream.getInternalBuffer();
      storeContentToStorage(fileLength, file, readOnly, bytes, bytes.length);
      return nativeStream;
    }
    @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
    final BufferExposingByteArrayOutputStream cache =
        new BufferExposingByteArrayOutputStream((int) fileLength);
    return new ReplicatorInputStream(nativeStream, cache) {
      @Override
      public void close() throws IOException {
        super.close();
        storeContentToStorage(fileLength, file, readOnly, cache.getInternalBuffer(), cache.size());
      }
    };
  }

  private void storeContentToStorage(
      long fileLength,
      @NotNull VirtualFile file,
      boolean readOnly,
      @NotNull byte[] bytes,
      int bytesLength)
      throws IOException {
    synchronized (myInputLock) {
      if (bytesLength == fileLength) {
        writeContent(file, new ByteSequence(bytes, 0, bytesLength), readOnly);
        setFlag(file, MUST_RELOAD_CONTENT, false);
      } else {
        setFlag(file, MUST_RELOAD_CONTENT, true);
      }
    }
  }

  private static boolean mustReloadContent(@NotNull VirtualFile file) {
    int fileId = getFileId(file);
    return checkFlag(fileId, MUST_RELOAD_CONTENT) || FSRecords.getLength(fileId) == -1L;
  }

  @Override
  @NotNull
  public OutputStream getOutputStream(
      @NotNull final VirtualFile file,
      final Object requestor,
      final long modStamp,
      final long timeStamp)
      throws IOException {
    return new ByteArrayOutputStream() {
      private boolean closed; // protection against user calling .close() twice

      @Override
      public void close() throws IOException {
        if (closed) return;
        super.close();

        VFileContentChangeEvent event =
            new VFileContentChangeEvent(
                requestor, file, file.getModificationStamp(), modStamp, false);
        List<VFileContentChangeEvent> events = Collections.singletonList(event);
        BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
        publisher.before(events);

        NewVirtualFileSystem delegate = getDelegate(file);
        OutputStream ioFileStream = delegate.getOutputStream(file, requestor, modStamp, timeStamp);
        // FSRecords.ContentOutputStream already buffered, no need to wrap in BufferedStream
        OutputStream persistenceStream = writeContent(file, delegate.isReadOnly());

        try {
          persistenceStream.write(buf, 0, count);
        } finally {
          try {
            ioFileStream.write(buf, 0, count);
          } finally {
            closed = true;
            persistenceStream.close();
            ioFileStream.close();

            executeTouch(file, false, event.getModificationStamp());
            publisher.after(events);
          }
        }
      }
    };
  }

  @Override
  public int acquireContent(@NotNull VirtualFile file) {
    return FSRecords.acquireFileContent(getFileId(file));
  }

  @Override
  public void releaseContent(int contentId) {
    FSRecords.releaseContent(contentId);
  }

  @Override
  public int getCurrentContentId(@NotNull VirtualFile file) {
    return FSRecords.getContentId(getFileId(file));
  }

  @Override
  public void moveFile(
      final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent)
      throws IOException {
    getDelegate(file).moveFile(requestor, file, newParent);
    processEvent(new VFileMoveEvent(requestor, file, newParent));
  }

  @RequiredWriteAction
  private void processEvent(@NotNull VFileEvent event) {
    processEvents(Collections.singletonList(event));
  }

  private static class EventWrapper {
    private final VFileDeleteEvent event;
    private final int id;

    private EventWrapper(final VFileDeleteEvent event, final int id) {
      this.event = event;
      this.id = id;
    }
  }

  @NotNull
  private static final Comparator<EventWrapper> DEPTH_COMPARATOR =
      new Comparator<EventWrapper>() {
        @Override
        public int compare(@NotNull final EventWrapper o1, @NotNull final EventWrapper o2) {
          return o1.event.getFileDepth() - o2.event.getFileDepth();
        }
      };

  @NotNull
  private static List<VFileEvent> validateEvents(@NotNull List<VFileEvent> events) {
    final List<EventWrapper> deletionEvents = ContainerUtil.newArrayList();
    for (int i = 0, size = events.size(); i < size; i++) {
      final VFileEvent event = events.get(i);
      if (event instanceof VFileDeleteEvent && event.isValid()) {
        deletionEvents.add(new EventWrapper((VFileDeleteEvent) event, i));
      }
    }

    final TIntHashSet invalidIDs;
    if (deletionEvents.isEmpty()) {
      invalidIDs = EmptyIntHashSet.INSTANCE;
    } else {
      ContainerUtil.quickSort(deletionEvents, DEPTH_COMPARATOR);

      invalidIDs = new TIntHashSet(deletionEvents.size());
      final Set<VirtualFile> dirsToBeDeleted = new THashSet<VirtualFile>(deletionEvents.size());
      nextEvent:
      for (EventWrapper wrapper : deletionEvents) {
        final VirtualFile candidate = wrapper.event.getFile();
        VirtualFile parent = candidate;
        while (parent != null) {
          if (dirsToBeDeleted.contains(parent)) {
            invalidIDs.add(wrapper.id);
            continue nextEvent;
          }
          parent = parent.getParent();
        }

        if (candidate.isDirectory()) {
          dirsToBeDeleted.add(candidate);
        }
      }
    }

    final List<VFileEvent> filtered = new ArrayList<VFileEvent>(events.size() - invalidIDs.size());
    for (int i = 0, size = events.size(); i < size; i++) {
      final VFileEvent event = events.get(i);
      if (event.isValid() && !(event instanceof VFileDeleteEvent && invalidIDs.contains(i))) {
        filtered.add(event);
      }
    }
    return filtered;
  }

  @RequiredWriteAction
  @Override
  public void processEvents(@NotNull List<VFileEvent> events) {
    ApplicationManager.getApplication().assertWriteAccessAllowed();

    List<VFileEvent> validated = validateEvents(events);

    BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
    publisher.before(validated);

    THashMap<VirtualFile, List<VFileEvent>> parentToChildrenEventsChanges = null;
    for (VFileEvent event : validated) {
      VirtualFile changedParent = null;
      if (event instanceof VFileCreateEvent) {
        changedParent = ((VFileCreateEvent) event).getParent();
        ((VFileCreateEvent) event).resetCache();
      } else if (event instanceof VFileDeleteEvent) {
        changedParent = ((VFileDeleteEvent) event).getFile().getParent();
      }

      if (changedParent != null) {
        if (parentToChildrenEventsChanges == null)
          parentToChildrenEventsChanges = new THashMap<VirtualFile, List<VFileEvent>>();
        List<VFileEvent> parentChildrenChanges = parentToChildrenEventsChanges.get(changedParent);
        if (parentChildrenChanges == null) {
          parentToChildrenEventsChanges.put(
              changedParent, parentChildrenChanges = new SmartList<VFileEvent>());
        }
        parentChildrenChanges.add(event);
      } else {
        applyEvent(event);
      }
    }

    if (parentToChildrenEventsChanges != null) {
      parentToChildrenEventsChanges.forEachEntry(
          new TObjectObjectProcedure<VirtualFile, List<VFileEvent>>() {
            @Override
            public boolean execute(VirtualFile parent, List<VFileEvent> childrenEvents) {
              applyChildrenChangeEvents(parent, childrenEvents);
              return true;
            }
          });
      parentToChildrenEventsChanges.clear();
    }

    publisher.after(validated);
  }

  private void applyChildrenChangeEvents(VirtualFile parent, List<VFileEvent> events) {
    final NewVirtualFileSystem delegate = getDelegate(parent);
    TIntArrayList childrenIdsUpdated = new TIntArrayList();
    List<VirtualFile> childrenToBeUpdated = new SmartList<VirtualFile>();

    final int parentId = getFileId(parent);
    assert parentId != 0;
    TIntHashSet parentChildrenIds = new TIntHashSet(FSRecords.list(parentId));
    boolean hasRemovedChildren = false;

    for (VFileEvent event : events) {
      if (event instanceof VFileCreateEvent) {
        String name = ((VFileCreateEvent) event).getChildName();
        final VirtualFile fake = new FakeVirtualFile(parent, name);
        final FileAttributes attributes = delegate.getAttributes(fake);

        if (attributes != null) {
          final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
          assert parent instanceof VirtualDirectoryImpl : parent;
          final VirtualDirectoryImpl dir = (VirtualDirectoryImpl) parent;
          VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
          childrenToBeUpdated.add(child);
          childrenIdsUpdated.add(childId);
          parentChildrenIds.add(childId);
        }
      } else if (event instanceof VFileDeleteEvent) {
        VirtualFile file = ((VFileDeleteEvent) event).getFile();
        if (!file.exists()) {
          LOG.error("Deleting a file, which does not exist: " + file.getPath());
          continue;
        }

        hasRemovedChildren = true;
        int id = getFileId(file);

        childrenToBeUpdated.add(file);
        childrenIdsUpdated.add(-id);
        parentChildrenIds.remove(id);
      }
    }

    FSRecords.updateList(parentId, parentChildrenIds.toArray());

    if (hasRemovedChildren) clearIdCache();
    VirtualDirectoryImpl parentImpl = (VirtualDirectoryImpl) parent;

    for (int i = 0, len = childrenIdsUpdated.size(); i < len; ++i) {
      final int childId = childrenIdsUpdated.get(i);
      final VirtualFile childFile = childrenToBeUpdated.get(i);

      if (childId > 0) {
        parentImpl.addChild((VirtualFileSystemEntry) childFile);
      } else {
        FSRecords.deleteRecordRecursively(-childId);
        parentImpl.removeChild(childFile);
        invalidateSubtree(childFile);
      }
    }
  }

  @Override
  @Nullable
  public VirtualFileSystemEntry findRoot(
      @NotNull String basePath, @NotNull NewVirtualFileSystem fs) {
    if (basePath.isEmpty()) {
      LOG.error("Invalid root, fs=" + fs);
      return null;
    }

    String rootUrl = normalizeRootUrl(basePath, fs);

    myRootsLock.readLock().lock();
    try {
      VirtualFileSystemEntry root = myRoots.get(rootUrl);
      if (root != null) return root;
    } finally {
      myRootsLock.readLock().unlock();
    }

    final VirtualFileSystemEntry newRoot;
    int rootId = FSRecords.findRootRecord(rootUrl);

    VfsData.Segment segment = VfsData.getSegment(rootId, true);
    VfsData.DirectoryData directoryData = new VfsData.DirectoryData();
    if (fs instanceof ArchiveFileSystem) {
      String parentPath =
          basePath.substring(0, basePath.indexOf(ArchiveFileSystem.ARCHIVE_SEPARATOR));
      VirtualFile parentFile = LocalFileSystem.getInstance().findFileByPath(parentPath);
      if (parentFile == null) return null;
      FileType type = FileTypeRegistry.getInstance().getFileTypeByFileName(parentFile.getName());
      if (!(type instanceof ArchiveFileType)) return null;
      newRoot = new ArchiveRoot(fs, rootId, segment, directoryData, parentFile);
    } else {
      newRoot = new FsRoot(fs, rootId, segment, directoryData, basePath);
    }

    FileAttributes attributes =
        fs.getAttributes(
            new StubVirtualFile() {
              @NotNull
              @Override
              public String getPath() {
                return newRoot.getPath();
              }

              @Nullable
              @Override
              public VirtualFile getParent() {
                return null;
              }
            });
    if (attributes == null || !attributes.isDirectory()) {
      return null;
    }

    boolean mark = false;

    myRootsLock.writeLock().lock();
    try {
      VirtualFileSystemEntry root = myRoots.get(rootUrl);
      if (root != null) return root;

      VfsData.initFile(rootId, segment, -1, directoryData);
      mark = writeAttributesToRecord(rootId, 0, newRoot, fs, attributes);

      myRoots.put(rootUrl, newRoot);
      myRootsById.put(rootId, newRoot);
    } finally {
      myRootsLock.writeLock().unlock();
    }

    if (!mark && attributes.lastModified != FSRecords.getTimestamp(rootId)) {
      newRoot.markDirtyRecursively();
    }

    LOG.assertTrue(
        rootId == newRoot.getId(),
        "root=" + newRoot + " expected=" + rootId + " actual=" + newRoot.getId());

    return newRoot;
  }

  @NotNull
  private static String normalizeRootUrl(
      @NotNull String basePath, @NotNull NewVirtualFileSystem fs) {
    // need to protect against relative path of the form "/x/../y"
    return UriUtil.trimTrailingSlashes(
        fs.getProtocol()
            + URLUtil.SCHEME_SEPARATOR
            + VfsImplUtil.normalize(fs, FileUtil.toCanonicalPath(basePath)));
  }

  @Override
  public void clearIdCache() {
    myIdToDirCache.clear();
  }

  private static final int DEPTH_LIMIT = 75;

  @Override
  @Nullable
  public NewVirtualFile findFileById(final int id) {
    return findFileById(id, false, null, 0);
  }

  @Override
  public NewVirtualFile findFileByIdIfCached(final int id) {
    return findFileById(id, true, null, 0);
  }

  @Nullable
  private VirtualFileSystemEntry findFileById(
      int id, boolean cachedOnly, TIntArrayList visited, int mask) {
    VirtualFileSystemEntry cached = myIdToDirCache.get(id);
    if (cached != null) return cached;

    if (visited != null
        && (visited.size() >= DEPTH_LIMIT || (mask & id) == id && visited.contains(id))) {
      @NonNls
      String sb =
          "Dead loop detected in persistent FS (id=" + id + " cached-only=" + cachedOnly + "):";
      for (int i = 0; i < visited.size(); i++) {
        int _id = visited.get(i);
        sb +=
            "\n  "
                + _id
                + " '"
                + getName(_id)
                + "' "
                + String.format("%02x", getFileAttributes(_id))
                + ' '
                + myIdToDirCache.containsKey(_id);
      }
      LOG.error(sb);
      return null;
    }

    int parentId = getParent(id);
    if (parentId >= id) {
      if (visited == null) visited = new TIntArrayList(DEPTH_LIMIT);
    }
    if (visited != null) visited.add(id);

    VirtualFileSystemEntry result;
    if (parentId == 0) {
      myRootsLock.readLock().lock();
      try {
        result = myRootsById.get(id);
      } finally {
        myRootsLock.readLock().unlock();
      }
    } else {
      VirtualFileSystemEntry parentFile = findFileById(parentId, cachedOnly, visited, mask | id);
      if (parentFile instanceof VirtualDirectoryImpl) {
        result = ((VirtualDirectoryImpl) parentFile).findChildById(id, cachedOnly);
      } else {
        result = null;
      }
    }

    if (result != null && result.isDirectory()) {
      VirtualFileSystemEntry old = myIdToDirCache.put(id, result);
      if (old != null) result = old;
    }
    return result;
  }

  @Override
  @NotNull
  public VirtualFile[] getRoots() {
    myRootsLock.readLock().lock();
    try {
      Collection<VirtualFileSystemEntry> roots = myRoots.values();
      return VfsUtilCore.toVirtualFileArray(roots);
    } finally {
      myRootsLock.readLock().unlock();
    }
  }

  @Override
  @NotNull
  public VirtualFile[] getRoots(@NotNull final NewVirtualFileSystem fs) {
    final List<VirtualFile> roots = new ArrayList<VirtualFile>();

    myRootsLock.readLock().lock();
    try {
      for (NewVirtualFile root : myRoots.values()) {
        if (root.getFileSystem() == fs) {
          roots.add(root);
        }
      }
    } finally {
      myRootsLock.readLock().unlock();
    }

    return VfsUtilCore.toVirtualFileArray(roots);
  }

  @Override
  @NotNull
  public VirtualFile[] getLocalRoots() {
    List<VirtualFile> roots = ContainerUtil.newSmartList();

    myRootsLock.readLock().lock();
    try {
      for (NewVirtualFile root : myRoots.values()) {
        if (root.isInLocalFileSystem() && !(root.getFileSystem() instanceof TempFileSystem)) {
          roots.add(root);
        }
      }
    } finally {
      myRootsLock.readLock().unlock();
    }

    return VfsUtilCore.toVirtualFileArray(roots);
  }

  private VirtualFileSystemEntry applyEvent(@NotNull VFileEvent event) {
    try {
      if (event instanceof VFileCreateEvent) {
        final VFileCreateEvent createEvent = (VFileCreateEvent) event;
        return executeCreateChild(createEvent.getParent(), createEvent.getChildName());
      } else if (event instanceof VFileDeleteEvent) {
        final VFileDeleteEvent deleteEvent = (VFileDeleteEvent) event;
        executeDelete(deleteEvent.getFile());
      } else if (event instanceof VFileContentChangeEvent) {
        final VFileContentChangeEvent contentUpdateEvent = (VFileContentChangeEvent) event;
        executeTouch(
            contentUpdateEvent.getFile(),
            contentUpdateEvent.isFromRefresh(),
            contentUpdateEvent.getModificationStamp());
      } else if (event instanceof VFileCopyEvent) {
        final VFileCopyEvent copyEvent = (VFileCopyEvent) event;
        return executeCreateChild(copyEvent.getNewParent(), copyEvent.getNewChildName());
      } else if (event instanceof VFileMoveEvent) {
        final VFileMoveEvent moveEvent = (VFileMoveEvent) event;
        executeMove(moveEvent.getFile(), moveEvent.getNewParent());
      } else if (event instanceof VFilePropertyChangeEvent) {
        final VFilePropertyChangeEvent propertyChangeEvent = (VFilePropertyChangeEvent) event;
        if (VirtualFile.PROP_NAME.equals(propertyChangeEvent.getPropertyName())) {
          executeRename(propertyChangeEvent.getFile(), (String) propertyChangeEvent.getNewValue());
        } else if (VirtualFile.PROP_WRITABLE.equals(propertyChangeEvent.getPropertyName())) {
          executeSetWritable(
              propertyChangeEvent.getFile(),
              ((Boolean) propertyChangeEvent.getNewValue()).booleanValue());
        } else if (VirtualFile.PROP_HIDDEN.equals(propertyChangeEvent.getPropertyName())) {
          executeSetHidden(
              propertyChangeEvent.getFile(),
              ((Boolean) propertyChangeEvent.getNewValue()).booleanValue());
        } else if (VirtualFile.PROP_SYMLINK_TARGET.equals(propertyChangeEvent.getPropertyName())) {
          executeSetTarget(
              propertyChangeEvent.getFile(), (String) propertyChangeEvent.getNewValue());
        }
      }
    } catch (Exception e) {
      // Exception applying single event should not prevent other events from applying.
      LOG.error(e);
    }
    return null;
  }

  @NotNull
  @NonNls
  public String toString() {
    return "PersistentFS";
  }

  private static VirtualFileSystemEntry executeCreateChild(
      @NotNull VirtualFile parent, @NotNull String name) {
    final NewVirtualFileSystem delegate = getDelegate(parent);
    final VirtualFile fake = new FakeVirtualFile(parent, name);
    final FileAttributes attributes = delegate.getAttributes(fake);
    if (attributes != null) {
      final int parentId = getFileId(parent);
      final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
      appendIdToParentList(parentId, childId);
      assert parent instanceof VirtualDirectoryImpl : parent;
      final VirtualDirectoryImpl dir = (VirtualDirectoryImpl) parent;
      VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
      dir.addChild(child);
      return child;
    }
    return null;
  }

  private static int createAndFillRecord(
      @NotNull NewVirtualFileSystem delegateSystem,
      @NotNull VirtualFile delegateFile,
      int parentId,
      @NotNull FileAttributes attributes) {
    final int childId = FSRecords.createRecord();
    writeAttributesToRecord(childId, parentId, delegateFile, delegateSystem, attributes);
    return childId;
  }

  private static void appendIdToParentList(final int parentId, final int childId) {
    int[] childrenList = FSRecords.list(parentId);
    childrenList = ArrayUtil.append(childrenList, childId);
    FSRecords.updateList(parentId, childrenList);
  }

  private void executeDelete(@NotNull VirtualFile file) {
    if (!file.exists()) {
      LOG.error("Deleting a file, which does not exist: " + file.getPath());
      return;
    }
    clearIdCache();

    int id = getFileId(file);

    final VirtualFile parent = file.getParent();
    final int parentId = parent == null ? 0 : getFileId(parent);

    if (parentId == 0) {
      String rootUrl =
          normalizeRootUrl(file.getPath(), (NewVirtualFileSystem) file.getFileSystem());
      myRootsLock.writeLock().lock();
      try {
        myRoots.remove(rootUrl);
        myRootsById.remove(id);
        FSRecords.deleteRootRecord(id);
      } finally {
        myRootsLock.writeLock().unlock();
      }
    } else {
      removeIdFromParentList(parentId, id, parent, file);
      VirtualDirectoryImpl directory = (VirtualDirectoryImpl) file.getParent();
      assert directory != null : file;
      directory.removeChild(file);
    }

    FSRecords.deleteRecordRecursively(id);

    invalidateSubtree(file);
  }

  private static void invalidateSubtree(@NotNull VirtualFile file) {
    final VirtualFileSystemEntry impl = (VirtualFileSystemEntry) file;
    impl.invalidate();
    for (VirtualFile child : impl.getCachedChildren()) {
      invalidateSubtree(child);
    }
  }

  private static void removeIdFromParentList(
      final int parentId, final int id, @NotNull VirtualFile parent, VirtualFile file) {
    int[] childList = FSRecords.list(parentId);

    int index = ArrayUtil.indexOf(childList, id);
    if (index == -1) {
      throw new RuntimeException(
          "Cannot find child ("
              + id
              + ")"
              + file
              + "\n\tin ("
              + parentId
              + ")"
              + parent
              + "\n\tactual children:"
              + Arrays.toString(childList));
    }
    childList = ArrayUtil.remove(childList, index);
    FSRecords.updateList(parentId, childList);
  }

  private static void executeRename(@NotNull VirtualFile file, @NotNull final String newName) {
    final int id = getFileId(file);
    FSRecords.setName(id, newName);
    ((VirtualFileSystemEntry) file).setNewName(newName);
  }

  private static void executeSetWritable(@NotNull VirtualFile file, boolean writableFlag) {
    setFlag(file, IS_READ_ONLY, !writableFlag);
    ((VirtualFileSystemEntry) file).updateProperty(VirtualFile.PROP_WRITABLE, writableFlag);
  }

  private static void executeSetHidden(@NotNull VirtualFile file, boolean hiddenFlag) {
    setFlag(file, IS_HIDDEN, hiddenFlag);
    ((VirtualFileSystemEntry) file).updateProperty(VirtualFile.PROP_HIDDEN, hiddenFlag);
  }

  private static void executeSetTarget(@NotNull VirtualFile file, String target) {
    ((VirtualFileSystemEntry) file).setLinkTarget(target);
  }

  private static void setFlag(@NotNull VirtualFile file, int mask, boolean value) {
    setFlag(getFileId(file), mask, value);
  }

  private static void setFlag(final int id, final int mask, final boolean value) {
    int oldFlags = FSRecords.getFlags(id);
    int flags = value ? oldFlags | mask : oldFlags & ~mask;

    if (oldFlags != flags) {
      FSRecords.setFlags(id, flags, true);
    }
  }

  private static boolean checkFlag(int fileId, int mask) {
    return (FSRecords.getFlags(fileId) & mask) != 0;
  }

  private static void executeTouch(
      @NotNull VirtualFile file, boolean reloadContentFromDelegate, long newModificationStamp) {
    if (reloadContentFromDelegate) {
      setFlag(file, MUST_RELOAD_CONTENT, true);
    }

    final NewVirtualFileSystem delegate = getDelegate(file);
    final FileAttributes attributes = delegate.getAttributes(file);
    FSRecords.setLength(getFileId(file), attributes != null ? attributes.length : DEFAULT_LENGTH);
    FSRecords.setTimestamp(
        getFileId(file), attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP);

    ((VirtualFileSystemEntry) file).setModificationStamp(newModificationStamp);
  }

  private void executeMove(@NotNull VirtualFile file, @NotNull VirtualFile newParent) {
    clearIdCache();

    final int fileId = getFileId(file);
    final int newParentId = getFileId(newParent);
    final int oldParentId = getFileId(file.getParent());

    removeIdFromParentList(oldParentId, fileId, file.getParent(), file);
    FSRecords.setParent(fileId, newParentId);
    appendIdToParentList(newParentId, fileId);
    ((VirtualFileSystemEntry) file).setParent(newParent);
  }

  @Override
  public String getName(int id) {
    assert id > 0;
    return FSRecords.getName(id);
  }

  @TestOnly
  public void cleanPersistedContents() {
    final int[] roots = FSRecords.listRoots();
    for (int root : roots) {
      cleanPersistedContentsRecursively(root);
    }
  }

  @TestOnly
  private void cleanPersistedContentsRecursively(int id) {
    if (isDirectory(getFileAttributes(id))) {
      for (int child : FSRecords.list(id)) {
        cleanPersistedContentsRecursively(child);
      }
    } else {
      setFlag(id, MUST_RELOAD_CONTENT, true);
    }
  }

  private abstract static class AbstractRoot extends VirtualDirectoryImpl {
    public AbstractRoot(
        int id, VfsData.Segment segment, VfsData.DirectoryData data, NewVirtualFileSystem fs) {
      super(id, segment, data, null, fs);
    }

    @NotNull
    @Override
    public abstract CharSequence getNameSequence();

    @Override
    protected abstract char[] appendPathOnFileSystem(int accumulatedPathLength, int[] positionRef);

    @Override
    public void setNewName(@NotNull String newName) {
      throw new IncorrectOperationException();
    }

    @Override
    public final void setParent(@NotNull VirtualFile newParent) {
      throw new IncorrectOperationException();
    }
  }

  private static class ArchiveRoot extends AbstractRoot {
    private final VirtualFile myParentLocalFile;
    private final String myParentPath;

    private ArchiveRoot(
        @NotNull NewVirtualFileSystem fs,
        int id,
        VfsData.Segment segment,
        VfsData.DirectoryData data,
        VirtualFile parentLocalFile) {
      super(id, segment, data, fs);
      myParentLocalFile = parentLocalFile;
      myParentPath = myParentLocalFile.getPath();
    }

    @NotNull
    @Override
    public CharSequence getNameSequence() {
      return myParentLocalFile.getName();
    }

    @Override
    protected char[] appendPathOnFileSystem(int accumulatedPathLength, int[] positionRef) {
      char[] chars =
          new char
              [myParentPath.length()
                  + ArchiveFileSystem.ARCHIVE_SEPARATOR.length()
                  + accumulatedPathLength];
      positionRef[0] = copyString(chars, positionRef[0], myParentPath);
      positionRef[0] = copyString(chars, positionRef[0], ArchiveFileSystem.ARCHIVE_SEPARATOR);
      return chars;
    }
  }

  private static class FsRoot extends AbstractRoot {
    private final String myName;

    private FsRoot(
        @NotNull NewVirtualFileSystem fs,
        int id,
        VfsData.Segment segment,
        VfsData.DirectoryData data,
        @NotNull String basePath) {
      super(id, segment, data, fs);
      myName = FileUtil.toSystemIndependentName(basePath);
    }

    @NotNull
    @Override
    public CharSequence getNameSequence() {
      return myName;
    }

    @Override
    protected char[] appendPathOnFileSystem(int pathLength, int[] position) {
      String name = getName();
      int nameLength = name.length();
      int rootPathLength = pathLength + nameLength;

      // otherwise we called this as a part of longer file path calculation and slash will be added
      // anyway
      boolean appendSlash =
          SystemInfo.isWindows && nameLength == 2 && name.charAt(1) == ':' && pathLength == 0;

      if (appendSlash) ++rootPathLength;
      char[] chars = new char[rootPathLength];

      position[0] = copyString(chars, position[0], name);

      if (appendSlash) {
        chars[position[0]++] = '/';
      }

      return chars;
    }
  }
}
  private void processBatch(
      @NotNull final ProgressIndicator indicator, @NotNull Set<VirtualFile> files) {
    assert !myApplication.isDispatchThread();
    final int resolvedInPreviousBatch = this.resolvedInPreviousBatch;
    final int totalSize = files.size() + resolvedInPreviousBatch;
    final ConcurrentIntObjectMap<int[]> fileToForwardIds =
        ContainerUtil.createConcurrentIntObjectMap();
    final Set<VirtualFile> toProcess = Collections.synchronizedSet(files);
    indicator.setIndeterminate(false);
    ProgressIndicatorUtils.forceWriteActionPriority(indicator, (Disposable) indicator);
    long start = System.currentTimeMillis();
    Processor<VirtualFile> processor =
        file -> {
          double fraction = 1 - toProcess.size() * 1.0 / totalSize;
          indicator.setFraction(fraction);
          try {
            if (!file.isDirectory() && toResolve(file, myProject)) {
              int fileId = getAbsId(file);
              int i = totalSize - toProcess.size();
              indicator.setText(i + "/" + totalSize + ": Resolving " + file.getPresentableUrl());
              int[] forwardIds = processFile(file, fileId, indicator);
              if (forwardIds == null) {
                // queueUpdate(file);
                return false;
              }
              fileToForwardIds.put(fileId, forwardIds);
            }
            toProcess.remove(file);
            return true;
          } catch (RuntimeException e) {
            indicator.checkCanceled();
          }
          return true;
        };
    boolean success = true;
    try {
      success = processFilesConcurrently(files, indicator, processor);
    } finally {
      this.resolvedInPreviousBatch = toProcess.isEmpty() ? 0 : totalSize - toProcess.size();
      queue(toProcess, "re-added after fail. success=" + success);
      storeIds(fileToForwardIds);

      long end = System.currentTimeMillis();
      log(
          "Resolved batch of "
              + (totalSize - toProcess.size())
              + " from "
              + totalSize
              + " files in "
              + ((end - start) / 1000)
              + "sec. (Gap: "
              + storage.gap
              + ")");
      synchronized (filesToResolve) {
        upToDate = filesToResolve.isEmpty();
        log("upToDate = " + upToDate);
        if (upToDate) {
          for (Listener listener : myListeners) {
            listener.allFilesResolved();
          }
        }
      }
    }
  }
public abstract class Client extends UserDataHolderBase {
  static final RuntimeException REJECTED = Promise.createError("rejected");

  protected final Channel channel;

  final ConcurrentIntObjectMap<AsyncPromise<Object>> messageCallbackMap =
      ContainerUtil.createConcurrentIntObjectMap();

  protected Client(@NotNull Channel channel) {
    this.channel = channel;
  }

  @NotNull
  public final EventLoop getEventLoop() {
    return channel.eventLoop();
  }

  @NotNull
  public final ByteBufAllocator getByteBufAllocator() {
    return channel.alloc();
  }

  protected abstract ChannelFuture send(@NotNull ByteBuf message);

  public abstract void sendHeartbeat();

  @Nullable
  final <T> AsyncPromise<T> send(int messageId, @NotNull ByteBuf message) {
    ChannelFuture channelFuture = send(message);
    if (messageId == -1) {
      return null;
    }

    ChannelFutureAwarePromise<T> promise =
        new ChannelFutureAwarePromise<T>(messageId, messageCallbackMap);
    channelFuture.addListener(promise);
    //noinspection unchecked
    messageCallbackMap.put(messageId, (AsyncPromise<Object>) promise);
    return promise;
  }

  final void rejectAsyncResults(@NotNull ExceptionHandler exceptionHandler) {
    if (!messageCallbackMap.isEmpty()) {
      Enumeration<AsyncPromise<Object>> elements = messageCallbackMap.elements();
      while (elements.hasMoreElements()) {
        try {
          elements.nextElement().setError(REJECTED);
        } catch (Throwable e) {
          exceptionHandler.exceptionCaught(e);
        }
      }
    }
  }

  private static final class ChannelFutureAwarePromise<T> extends AsyncPromise<T>
      implements ChannelFutureListener {
    private final int messageId;
    private final ConcurrentIntObjectMap<?> messageCallbackMap;

    public ChannelFutureAwarePromise(int messageId, ConcurrentIntObjectMap<?> messageCallbackMap) {
      this.messageId = messageId;
      this.messageCallbackMap = messageCallbackMap;
    }

    @Override
    public boolean setError(@NotNull Throwable error) {
      boolean result = super.setError(error);
      messageCallbackMap.remove(messageId);
      return result;
    }

    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
      if (!future.isSuccess()) {
        Throwable cause = future.cause();
        setError(cause == null ? Promise.createError("No success") : cause);
      }
    }
  }
}
/** Author: dmitrylomov */
public class ModuleScopeProviderImpl implements ModuleScopeProvider {
  private final Module myModule;
  private final ConcurrentIntObjectMap<GlobalSearchScope> myScopeCache =
      ContainerUtil.createConcurrentIntObjectMap();
  private ModuleWithDependentsTestScope myModuleTestsWithDependentsScope;

  public ModuleScopeProviderImpl(@NotNull Module module) {
    myModule = module;
  }

  @NotNull
  private GlobalSearchScope getCachedScope(@ModuleWithDependenciesScope.ScopeConstant int options) {
    GlobalSearchScope scope = myScopeCache.get(options);
    if (scope == null) {
      scope = new ModuleWithDependenciesScope(myModule, options);
      myScopeCache.put(options, scope);
    }
    return scope;
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleScope() {
    return getCachedScope(ModuleWithDependenciesScope.COMPILE | ModuleWithDependenciesScope.TESTS);
  }

  @NotNull
  @Override
  public GlobalSearchScope getModuleScope(boolean includeTests) {
    return getCachedScope(
        ModuleWithDependenciesScope.COMPILE
            | (includeTests ? ModuleWithDependenciesScope.TESTS : 0));
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleWithLibrariesScope() {
    return getCachedScope(
        ModuleWithDependenciesScope.COMPILE
            | ModuleWithDependenciesScope.TESTS
            | ModuleWithDependenciesScope.LIBRARIES);
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleWithDependenciesScope() {
    return getCachedScope(
        ModuleWithDependenciesScope.COMPILE
            | ModuleWithDependenciesScope.TESTS
            | ModuleWithDependenciesScope.MODULES);
  }

  @NotNull
  @Override
  public GlobalSearchScope getModuleContentScope() {
    return getCachedScope(ModuleWithDependenciesScope.CONTENT);
  }

  @NotNull
  @Override
  public GlobalSearchScope getModuleContentWithDependenciesScope() {
    return getCachedScope(
        ModuleWithDependenciesScope.CONTENT | ModuleWithDependenciesScope.MODULES);
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleWithDependenciesAndLibrariesScope(boolean includeTests) {
    return getCachedScope(
        ModuleWithDependenciesScope.COMPILE
            | ModuleWithDependenciesScope.MODULES
            | ModuleWithDependenciesScope.LIBRARIES
            | (includeTests ? ModuleWithDependenciesScope.TESTS : 0));
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleWithDependentsScope() {
    return getModuleTestsWithDependentsScope().getBaseScope();
  }

  @Override
  @NotNull
  public ModuleWithDependentsTestScope getModuleTestsWithDependentsScope() {
    ModuleWithDependentsTestScope scope = myModuleTestsWithDependentsScope;
    if (scope == null) {
      myModuleTestsWithDependentsScope = scope = new ModuleWithDependentsTestScope(myModule);
    }
    return scope;
  }

  @Override
  @NotNull
  public GlobalSearchScope getModuleRuntimeScope(boolean includeTests) {
    return getCachedScope(
        ModuleWithDependenciesScope.MODULES
            | ModuleWithDependenciesScope.LIBRARIES
            | (includeTests ? ModuleWithDependenciesScope.TESTS : 0));
  }

  @Override
  public void clearCache() {
    myScopeCache.clear();
    myModuleTestsWithDependentsScope = null;
  }
}
// todo send FCGI_ABORT_REQUEST if client channel disconnected
public abstract class FastCgiService extends SingleConnectionNetService {
  protected static final Logger LOG = Logger.getInstance(FastCgiService.class);

  private final AtomicInteger requestIdCounter = new AtomicInteger();
  protected final ConcurrentIntObjectMap<Channel> requests =
      ContainerUtil.createConcurrentIntObjectMap();

  public FastCgiService(@NotNull Project project) {
    super(project);
  }

  @Override
  protected void closeProcessConnections() {
    try {
      super.closeProcessConnections();
    } finally {
      requestIdCounter.set(0);
      if (!requests.isEmpty()) {
        List<Channel> waitingClients = ContainerUtil.toList(requests.elements());
        requests.clear();
        for (Channel channel : waitingClients) {
          sendBadGateway(channel);
        }
      }
    }
  }

  private static void sendBadGateway(@NotNull Channel channel) {
    try {
      if (channel.isActive()) {
        Responses.sendStatus(HttpResponseStatus.BAD_GATEWAY, channel);
      }
    } catch (Throwable e) {
      NettyUtil.log(e, LOG);
    }
  }

  @Override
  protected void configureBootstrap(
      @NotNull Bootstrap bootstrap, @NotNull final Consumer<String> errorOutputConsumer) {
    bootstrap.handler(
        new ChannelInitializer() {
          @Override
          protected void initChannel(Channel channel) throws Exception {
            channel
                .pipeline()
                .addLast(
                    "fastCgiDecoder", new FastCgiDecoder(errorOutputConsumer, FastCgiService.this));
            channel.pipeline().addLast("exceptionHandler", ChannelExceptionHandler.getInstance());
          }
        });
  }

  public void send(@NotNull final FastCgiRequest fastCgiRequest, @NotNull ByteBuf content) {
    final ByteBuf notEmptyContent;
    if (content.isReadable()) {
      content.retain();
      notEmptyContent = content;
      notEmptyContent.touch();
    } else {
      notEmptyContent = null;
    }

    try {
      if (processHandler.has()) {
        fastCgiRequest.writeToServerChannel(notEmptyContent, processChannel);
      } else {
        processHandler
            .get()
            .done(
                new Consumer<OSProcessHandler>() {
                  @Override
                  public void consume(OSProcessHandler osProcessHandler) {
                    fastCgiRequest.writeToServerChannel(notEmptyContent, processChannel);
                  }
                })
            .rejected(
                new Consumer<Throwable>() {
                  @Override
                  public void consume(Throwable error) {
                    LOG.error(error);
                    handleError(fastCgiRequest, notEmptyContent);
                  }
                });
      }
    } catch (Throwable e) {
      LOG.error(e);
      handleError(fastCgiRequest, notEmptyContent);
    }
  }

  private void handleError(@NotNull FastCgiRequest fastCgiRequest, @Nullable ByteBuf content) {
    try {
      if (content != null && content.refCnt() != 0) {
        content.release();
      }
    } finally {
      Channel channel = requests.remove(fastCgiRequest.requestId);
      if (channel != null) {
        sendBadGateway(channel);
      }
    }
  }

  public int allocateRequestId(@NotNull Channel channel) {
    int requestId = requestIdCounter.getAndIncrement();
    if (requestId >= Short.MAX_VALUE) {
      requestIdCounter.set(0);
      requestId = requestIdCounter.getAndDecrement();
    }
    requests.put(requestId, channel);
    return requestId;
  }

  void responseReceived(int id, @Nullable ByteBuf buffer) {
    Channel channel = requests.remove(id);
    if (channel == null || !channel.isActive()) {
      if (buffer != null) {
        buffer.release();
      }
      return;
    }

    if (buffer == null) {
      Responses.sendStatus(HttpResponseStatus.BAD_GATEWAY, channel);
      return;
    }

    HttpResponse httpResponse =
        new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer);
    try {
      parseHeaders(httpResponse, buffer);
      Responses.addServer(httpResponse);
      if (!HttpHeaderUtil.isContentLengthSet(httpResponse)) {
        HttpHeaderUtil.setContentLength(httpResponse, buffer.readableBytes());
      }
    } catch (Throwable e) {
      buffer.release();
      try {
        LOG.error(e);
      } finally {
        Responses.sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, channel);
      }
      return;
    }
    channel.writeAndFlush(httpResponse);
  }

  private static void parseHeaders(@NotNull HttpResponse response, @NotNull ByteBuf buffer) {
    StringBuilder builder = new StringBuilder();
    while (buffer.isReadable()) {
      builder.setLength(0);

      String key = null;
      boolean valueExpected = true;
      while (true) {
        int b = buffer.readByte();
        if (b < 0 || b == '\n') {
          break;
        }

        if (b != '\r') {
          if (valueExpected && b == ':') {
            valueExpected = false;

            key = builder.toString();
            builder.setLength(0);
            MessageDecoder.skipWhitespace(buffer);
          } else {
            builder.append((char) b);
          }
        }
      }

      if (builder.length() == 0) {
        // end of headers
        return;
      }

      // skip standard headers
      if (StringUtil.isEmpty(key)
          || StringUtilRt.startsWithIgnoreCase(key, "http")
          || StringUtilRt.startsWithIgnoreCase(key, "X-Accel-")) {
        continue;
      }

      String value = builder.toString();
      if (key.equalsIgnoreCase("status")) {
        int index = value.indexOf(' ');
        if (index == -1) {
          LOG.warn("Cannot parse status: " + value);
          response.setStatus(HttpResponseStatus.OK);
        } else {
          response.setStatus(
              HttpResponseStatus.valueOf(Integer.parseInt(value.substring(0, index))));
        }
      } else if (!(key.startsWith("http") || key.startsWith("HTTP"))) {
        response.headers().add(key, value);
      }
    }
  }
}