All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.intellij.idea.SocketLock Maven / Gradle / Ivy

/*
 * Copyright 2000-2015 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.idea;

import com.intellij.CommonBundle;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.NotNullProducer;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.net.NetUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.io.BuiltInServer;
import org.jetbrains.io.MessageDecoder;

import javax.swing.*;
import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author mike
 */
public final class SocketLock {
  private static final Logger LOG = Logger.getInstance(SocketLock.class);

  @NonNls private static final String ACTIVATE_COMMAND = "activate ";

  private final String configPath;
  private final String systemPath;

  public enum ActivateStatus {ACTIVATED, NO_INSTANCE, CANNOT_ACTIVATE}

  private final AtomicReference>> activateListener = new AtomicReference>>();

  private BuiltInServer server;

  public SocketLock(@NotNull String configPath, @NotNull String systemPath) {
    this.configPath = configPath;
    this.systemPath = systemPath;
  }

  public void setExternalInstanceListener(@Nullable Consumer> consumer) {
    activateListener.set(consumer);
  }

  public void dispose() {
    if (LOG.isDebugEnabled()) {
      LOG.debug("enter: destroyProcess()");
    }

    BuiltInServer server = this.server;
    boolean doRemovePortMarker = server != null;
    try {
      if (server != null) {
        Disposer.dispose(server);
      }
    }
    finally {
      if (doRemovePortMarker) {
        try {
          executeAndClose(new Executor() {
            @Override
            public Void execute(@NotNull List closeables) throws IOException {
              File config = new File(configPath);
              File system = new File(systemPath);
              lockPortMarker(config, closeables);
              lockPortMarker(system, closeables);
              FileUtil.delete(new File(config, "port"));
              FileUtil.delete(new File(system, "port"));
              return null;
            }
          });
        }
        catch (Throwable e) {
          LOG.error(e);
        }
      }
    }
  }

  @Nullable
  public BuiltInServer getServer() {
    return server;
  }

  private static void lockPortMarker(@NotNull File parent, @NotNull List list) throws IOException {
    FileUtilRt.createDirectory(parent);
    FileOutputStream stream = new FileOutputStream(new File(parent, "port.lock"), true);
    list.add(stream);
  }

  private static void addExistingPort(@NotNull File portMarker, @NotNull String path, @NotNull MultiMap portToPath) {
    if (portMarker.exists()) {
      try {
        portToPath.putValue(Integer.parseInt(FileUtilRt.loadFile(portMarker)), path);
      }
      catch (Throwable e) {
        LOG.debug(e);
        // don't delete - we overwrite it on write in any case
      }
    }
  }

  @Nullable
  public ActivateStatus lock() {
    return lock(ArrayUtil.EMPTY_STRING_ARRAY);
  }

  @Nullable
  public ActivateStatus lock(@NotNull final String[] args) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("enter: lock(configPath='" + configPath + "', systemPath='" + systemPath + "')");
    }

    try {
      final File config = new File(configPath);
      final File system = new File(systemPath);
      final File portMarkerC = new File(config, "port");
      final File portMarkerS = new File(system, "port");
      return executeAndClose(new Executor() {
        @Override
        public ActivateStatus execute(@NotNull List closeables) throws Throwable {
          lockPortMarker(config, closeables);
          lockPortMarker(system, closeables);
          MultiMap portToPath = MultiMap.createSmart();
          addExistingPort(portMarkerC, configPath, portToPath);
          addExistingPort(portMarkerS, systemPath, portToPath);
          if (!portToPath.isEmpty()) {
            for (Map.Entry> entry : portToPath.entrySet()) {
              ActivateStatus status = tryActivate(entry.getKey(), entry.getValue(), args);
              if (status != ActivateStatus.NO_INSTANCE) {
                return status;
              }
            }
          }

          final String[] lockedPaths = {configPath, systemPath};
          server = BuiltInServer.start(1, 6942, 50, false, new NotNullProducer() {
            @NotNull
            @Override
            public ChannelHandler produce() {
              return new MyChannelInboundHandler(lockedPaths, activateListener);
            }
          });

          byte[] portBytes = Integer.toString(server.getPort()).getBytes(CharsetToolkit.UTF8_CHARSET);
          FileUtil.writeToFile(portMarkerC, portBytes);
          FileUtil.writeToFile(portMarkerS, portBytes);
          return ActivateStatus.NO_INSTANCE;
        }
      });
    }
    catch (Throwable e) {
      LOG.error(e);

      if (Main.isHeadless()) {
        Main.showMessage("Cannot lock system folders", e);
      }
      else {
        String pathToLogFile = PathManager.getLogPath() + "/idea.log file".replace('/', File.separatorChar);
        JOptionPane.showMessageDialog(
          JOptionPane.getRootFrame(),
          CommonBundle.message("cannot.start.other.instance.is.running.error.message", ApplicationNamesInfo.getInstance().getProductName(),
                               pathToLogFile),
          CommonBundle.message("title.warning"),
          JOptionPane.WARNING_MESSAGE
        );
      }
      return null;
    }
  }

  private interface Executor {
    T execute(@NotNull List closeables) throws Throwable;
  }

  private static  T executeAndClose(@NotNull Executor executor) throws Throwable {
    List closeables = new ArrayList();
    try {
      return executor.execute(closeables);
    }
    finally {
      for (Closeable closeable : closeables) {
        try {
          closeable.close();
        }
        catch (Throwable e) {
          LOG.error(e);
        }
      }
    }
  }

  @SuppressWarnings({"SocketOpenedButNotSafelyClosed", "IOResourceOpenedButNotSafelyClosed"})
  @NotNull
  private static ActivateStatus tryActivate(int portNumber, @NotNull Collection paths, @NotNull String[] args) {
    Socket socket = null;
    try {
      socket = new Socket(NetUtils.getLoopbackAddress(), portNumber);
      socket.setSoTimeout(300);

      boolean result = false;
      DataInputStream in = new DataInputStream(socket.getInputStream());
      while (true) {
        try {
          String path = in.readUTF();
          if (paths.contains(path)) {
            result = true;
            // don't break - read all input
          }
        }
        catch (IOException ignored) {
          break;
        }
      }

      if (result) {
        try {
          DataOutputStream out = new DataOutputStream(socket.getOutputStream());
          out.writeUTF(ACTIVATE_COMMAND + new File(".").getAbsolutePath() + "\0" + StringUtil.join(args, "\0"));
          out.flush();
          String response = in.readUTF();
          if (response.equals("ok")) {
            return ActivateStatus.ACTIVATED;
          }
        }
        catch (IOException e) {
          LOG.info(e);
        }
        return ActivateStatus.CANNOT_ACTIVATE;
      }
    }
    catch (IOException e) {
      LOG.debug(e);
    }
    finally {
      if (socket != null) {
        try {
          socket.close();
        }
        catch (IOException e) {
          LOG.debug(e);
        }
      }
    }

    return ActivateStatus.NO_INSTANCE;
  }

  private static class MyChannelInboundHandler extends MessageDecoder {
    private final String[] lockedPaths;
    private State state = State.HEADER;
    private final AtomicReference>> activateListener;

    public MyChannelInboundHandler(@NotNull String[] lockedPaths, @NotNull AtomicReference>> activateListener) {
      this.lockedPaths = lockedPaths;
      this.activateListener = activateListener;
    }

    private enum State {HEADER, CONTENT}

    @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
    @Override
    public void channelActive(ChannelHandlerContext context) throws Exception {
      ByteBuf buffer = context.alloc().ioBuffer(1024);
      boolean success = false;
      try {
        ByteBufOutputStream out = new ByteBufOutputStream(buffer);
        for (String path : lockedPaths) {
          if (path != null) {
            out.writeUTF(path);
          }
        }
        out.close();
        success = true;
      }
      finally {
        if (!success) {
          buffer.release();
        }
      }
      context.writeAndFlush(buffer);
    }

    @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
    @Override
    protected void messageReceived(@NotNull ChannelHandlerContext context, @NotNull ByteBuf input) throws Exception {
      while (true) {
        switch (state) {
          case HEADER: {
            ByteBuf buffer = getBufferIfSufficient(input, 2, context);
            if (buffer == null) {
              return;
            }

            contentLength = buffer.readUnsignedShort();
            state = State.CONTENT;
          }
          break;

          case CONTENT: {
            CharSequence command = readChars(input);
            if (command == null) {
              return;
            }

            if (StringUtil.startsWith(command, ACTIVATE_COMMAND)) {
              List args = StringUtil.split(command.subSequence(ACTIVATE_COMMAND.length(), command.length()).toString(), "\0");
              Consumer> listener = activateListener.get();
              if (listener != null) {
                listener.consume(args);
              }

              ByteBuf buffer = context.alloc().ioBuffer(4);
              ByteBufOutputStream out = new ByteBufOutputStream(buffer);
              out.writeUTF("ok");
              out.close();
              context.writeAndFlush(buffer);
            }
            context.close();
          }
          break;
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy