blob: ceddbd18d97f4253658d2f0567b967d78b734eb9 [file] [log] [blame]
/*
* Copyright 2000-2013 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.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.Consumer;
import com.intellij.util.net.NetUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
/**
* @author mike
*/
public class SocketLock {
private static final Logger LOG = Logger.getInstance("#com.intellij.idea.SocketLock");
public static final int SOCKET_NUMBER_START = 6942;
public static final int SOCKET_NUMBER_END = SOCKET_NUMBER_START + 50;
// IMPORTANT: Some antiviral software detect viruses by the fact of accessing these ports so we should not touch them to appear innocent.
private static final int[] FORBIDDEN_PORTS = {6953, 6969, 6970};
private ServerSocket mySocket;
private final List<String> myLockedPaths = new ArrayList<String>();
private boolean myIsDialogShown = false;
@NonNls private static final String LOCK_THREAD_NAME = "Lock thread";
@NonNls private static final String ACTIVATE_COMMAND = "activate ";
@Nullable
private Consumer<List<String>> myActivateListener;
public static enum ActivateStatus { ACTIVATED, NO_INSTANCE, CANNOT_ACTIVATE }
public SocketLock() {
}
public void setActivateListener(@Nullable Consumer<List<String>> consumer) {
myActivateListener = consumer;
}
public synchronized void dispose() {
if (LOG.isDebugEnabled()) {
LOG.debug("enter: destroyProcess()");
}
try {
mySocket.close();
mySocket = null;
}
catch (IOException e) {
LOG.debug(e);
}
}
private volatile int acquiredPort = -1;
public synchronized int getAcquiredPort () {
return acquiredPort;
}
public synchronized ActivateStatus lock(String path, boolean markPort, String... args) {
if (LOG.isDebugEnabled()) {
LOG.debug("enter: lock(path='" + path + "')");
}
ActivateStatus status = ActivateStatus.NO_INSTANCE;
int port = acquireSocket();
if (mySocket == null) {
if (!myIsDialogShown) {
final String productName = ApplicationNamesInfo.getInstance().getProductName();
if (Main.isHeadless()) { //team server inspections
throw new RuntimeException("Only one instance of " + productName + " can be run at a time.");
}
@NonNls final String pathToLogFile = PathManager.getLogPath() + "/idea.log file".replace('/', File.separatorChar);
JOptionPane.showMessageDialog(
JOptionPane.getRootFrame(),
CommonBundle.message("cannot.start.other.instance.is.running.error.message", productName, pathToLogFile),
CommonBundle.message("title.warning"),
JOptionPane.WARNING_MESSAGE
);
myIsDialogShown = true;
}
return status;
}
if (markPort && port != -1) {
File portMarker = new File(path, "port");
try {
FileUtil.writeToFile(portMarker, Integer.toString(port).getBytes());
}
catch (IOException ignored) {
FileUtil.asyncDelete(portMarker);
}
}
for (int i = SOCKET_NUMBER_START; i < SOCKET_NUMBER_END; i++) {
if (isPortForbidden(i) || i == mySocket.getLocalPort()) continue;
status = tryActivate(i, path, args);
if (status != ActivateStatus.NO_INSTANCE) {
return status;
}
}
myLockedPaths.add(path);
return status;
}
public static boolean isPortForbidden(int port) {
for (int forbiddenPort : FORBIDDEN_PORTS) {
if (port == forbiddenPort) return true;
}
return false;
}
private static ActivateStatus tryActivate(int portNumber, String path, String[] args) {
List<String> result = new ArrayList<String>();
try {
try {
ServerSocket serverSocket = new ServerSocket(portNumber, 50, NetUtils.getLoopbackAddress());
serverSocket.close();
return ActivateStatus.NO_INSTANCE;
}
catch (IOException ignored) {
}
Socket socket = new Socket(NetUtils.getLoopbackAddress(), portNumber);
socket.setSoTimeout(300);
DataInputStream in = new DataInputStream(socket.getInputStream());
while (true) {
try {
result.add(in.readUTF());
}
catch (IOException ignored) {
break;
}
}
if (result.contains(path)) {
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 ignored) {
}
return ActivateStatus.CANNOT_ACTIVATE;
}
in.close();
}
catch (IOException e) {
LOG.debug(e);
}
return ActivateStatus.NO_INSTANCE;
}
private int acquireSocket() {
if (mySocket != null) return -1;
int port = -1;
for (int i = SOCKET_NUMBER_START; i < SOCKET_NUMBER_END; i++) {
try {
if (isPortForbidden(i)) continue;
mySocket = new ServerSocket(i, 50, InetAddress.getByName("127.0.0.1"));
port = i;
acquiredPort = port;
break;
}
catch (IOException e) {
LOG.info(e);
}
}
final Thread thread = new Thread(new MyRunnable(), LOCK_THREAD_NAME);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
return port;
}
private class MyRunnable implements Runnable {
@Override
public void run() {
try {
while (true) {
try {
final Socket socket = mySocket.accept();
socket.setSoTimeout(800);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
synchronized (SocketLock.this) {
for (String path: myLockedPaths) {
out.writeUTF(path);
}
}
DataInputStream stream = new DataInputStream(socket.getInputStream());
final String command = stream.readUTF();
if (command.startsWith(ACTIVATE_COMMAND)) {
List<String> args = StringUtil.split(command.substring(ACTIVATE_COMMAND.length()), "\0");
if (myActivateListener != null) {
myActivateListener.consume(args);
}
out.writeUTF("ok");
}
out.close();
}
catch (IOException e) {
LOG.debug(e);
}
}
}
catch (Throwable ignored) {
}
}
}
}