mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[platform] switching IDE directory locking/instance activation code to UDS-based implementation (IDEA-238995)
GitOrigin-RevId: c5175ab1ab98e2f864eef8c80d64dbed81585594
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1e93cefc23
commit
436a9c577c
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.idea;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
@@ -17,7 +17,7 @@ public final class AppExitCodes {
|
||||
// reserved (permanently if launchers will perform the check): public static final int UNSUPPORTED_JAVA_VERSION = 10;
|
||||
public static final int PRIVACY_POLICY_REJECTION = 11;
|
||||
public static final int INSTALLATION_CORRUPTED = 12;
|
||||
public static final int ACTIVATE_WRONG_TOKEN_CODE = 13;
|
||||
// reserved: public static final int ACTIVATE_WRONG_TOKEN_CODE = 13;
|
||||
public static final int ACTIVATE_NOT_INITIALIZED = 14;
|
||||
public static final int ACTIVATE_ERROR = 15;
|
||||
public static final int ACTIVATE_DISPOSING = 16;
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ide;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@ApiStatus.Internal
|
||||
public final class SpecialConfigFiles {
|
||||
public static final String PORT_FILE = "port";
|
||||
public static final String PORT_LOCK_FILE = "port.lock";
|
||||
public static final String TOKEN_FILE = "token";
|
||||
public static final String PORT_FILE = ".port";
|
||||
public static final String LOCK_FILE = ".lock";
|
||||
|
||||
public static final String USER_WEB_TOKEN = "user.web.token";
|
||||
public static final String CHROME_USER_DATA = "chrome-user-data";
|
||||
|
||||
// used in pre-231 directory locking
|
||||
public static final String PORT_LOCK_FILE = "port.lock";
|
||||
public static final String TOKEN_FILE = "token";
|
||||
}
|
||||
|
||||
@@ -1,407 +1,333 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("ReplaceGetOrSet")
|
||||
package com.intellij.idea;
|
||||
|
||||
package com.intellij.idea
|
||||
import com.intellij.execution.process.ProcessIOExecutorService;
|
||||
import com.intellij.ide.BootstrapBundle;
|
||||
import com.intellij.ide.CliResult;
|
||||
import com.intellij.ide.SpecialConfigFiles;
|
||||
import com.intellij.jna.JnaLoader;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.SystemInfoRt;
|
||||
import com.intellij.openapi.util.io.NioFiles;
|
||||
import com.intellij.util.Suppressions;
|
||||
import com.intellij.util.User32Ex;
|
||||
import com.sun.jna.platform.win32.WinDef;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import com.intellij.diagnostic.ActivityCategory
|
||||
import com.intellij.diagnostic.StartUpMeasurer
|
||||
import com.intellij.ide.CliResult
|
||||
import com.intellij.ide.IdeBundle
|
||||
import com.intellij.ide.SpecialConfigFiles
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.SystemInfoRt
|
||||
import com.intellij.openapi.util.io.NioFiles
|
||||
import com.intellij.util.User32Ex
|
||||
import com.sun.jna.platform.win32.WinDef
|
||||
import io.netty.buffer.ByteBuf
|
||||
import io.netty.buffer.ByteBufOutputStream
|
||||
import io.netty.channel.ChannelHandlerContext
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.future.asCompletableFuture
|
||||
import org.jetbrains.io.BuiltInServer
|
||||
import org.jetbrains.io.MessageDecoder
|
||||
import java.io.DataInput
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.ConnectException
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.*
|
||||
import java.nio.file.attribute.PosixFileAttributeView
|
||||
import java.nio.file.attribute.PosixFilePermission
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.StreamCorruptedException;
|
||||
import java.net.BindException;
|
||||
import java.net.SocketException;
|
||||
import java.net.StandardProtocolFamily;
|
||||
import java.net.UnixDomainSocketAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
|
||||
private const val PATHS_EOT_RESPONSE = "---"
|
||||
private const val ACTIVATE_COMMAND = "activate "
|
||||
private const val OK_RESPONSE = "ok"
|
||||
import static java.util.Objects.requireNonNullElse;
|
||||
|
||||
//todo rename into DirectoryLock, then replace with the new one
|
||||
class SocketLock(@JvmField val configPath: Path, @JvmField val systemPath: Path) {
|
||||
enum class ActivationStatus {
|
||||
ACTIVATED, NO_INSTANCE, CANNOT_ACTIVATE
|
||||
}
|
||||
/**
|
||||
* The class ensures that only one IDE instance is running on the given pair of configuration/cache directories,
|
||||
* and participates in the CLI by passing arguments and relaying back exit codes and error messages.
|
||||
*/
|
||||
final class DirectoryLock {
|
||||
static final class CannotActivateException extends Exception {
|
||||
private CannotActivateException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
private val commandProcessorRef: AtomicReference<(List<String>) -> Deferred<CliResult>> = AtomicReference {
|
||||
CompletableDeferred(CliResult(AppExitCodes.ACTIVATE_NOT_INITIALIZED, IdeBundle.message("activation.not.initialized")))
|
||||
}
|
||||
|
||||
private val lockedFiles = ArrayList<AutoCloseable>(4)
|
||||
|
||||
@Volatile
|
||||
var serverFuture: Deferred<BuiltInServer>? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
if (configPath == systemPath) {
|
||||
throw IllegalArgumentException("'config' and 'system' paths should point to different directories")
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fun setCommandProcessor(processor: (List<String>) -> Deferred<CliResult>) {
|
||||
commandProcessorRef.set(processor)
|
||||
}
|
||||
private static final int UDS_PATH_LENGTH_LIMIT = 100;
|
||||
private static final int LOCK_RETRIES = 3;
|
||||
private static final int BUFFER_LENGTH = 16_384;
|
||||
private static final int MARKER = 0xFACADE;
|
||||
private static final int HEADER_LENGTH = 6; // the marker (4 bytes) + a packet length (2 bytes)
|
||||
private static final String SERVER_THREAD_NAME = "External Command Listener";
|
||||
private static final String INTERNAL_DIAGNOSTIC_COMMAND = "ij-activation-diagnostic";
|
||||
|
||||
fun dispose() {
|
||||
log("enter: dispose()")
|
||||
val server: BuiltInServer? = runCatching { getServer() }.getOrNull()
|
||||
try {
|
||||
if (lockedFiles.isEmpty()) {
|
||||
lockPortFiles()
|
||||
}
|
||||
server?.let {
|
||||
Disposer.dispose(it)
|
||||
}
|
||||
Files.deleteIfExists(configPath.resolve(SpecialConfigFiles.PORT_FILE))
|
||||
Files.deleteIfExists(systemPath.resolve(SpecialConfigFiles.PORT_FILE))
|
||||
Files.deleteIfExists(systemPath.resolve(SpecialConfigFiles.TOKEN_FILE))
|
||||
unlockPortFiles()
|
||||
private static final Logger LOG = Logger.getInstance(DirectoryLock.class);
|
||||
private static final AtomicInteger COUNT = new AtomicInteger(); // to ensure redirected port file uniqueness in tests
|
||||
|
||||
private final String myPid = String.valueOf(ProcessHandle.current().pid());
|
||||
private final Path myPortFile;
|
||||
private final Path myLockFile;
|
||||
private final @Nullable Path myRedirectedPortFile;
|
||||
private final Function<List<String>, CliResult> myProcessor;
|
||||
|
||||
private volatile @Nullable ServerSocketChannel myServerChannel = null;
|
||||
|
||||
DirectoryLock(@NotNull Path configPath, @NotNull Path systemPath, @NotNull Function<List<String>, CliResult> processor) {
|
||||
myPortFile = systemPath.resolve(SpecialConfigFiles.PORT_FILE);
|
||||
myLockFile = configPath.resolve(SpecialConfigFiles.LOCK_FILE);
|
||||
|
||||
if (myPortFile.toString().length() > UDS_PATH_LENGTH_LIMIT) {
|
||||
var baseDir = SystemInfoRt.isWindows ? Path.of(System.getenv("SystemRoot"), "Temp") : Path.of("/tmp");
|
||||
myRedirectedPortFile = baseDir.resolve(".ij_redirected_port_" + myPid + "_" + COUNT.incrementAndGet());
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getServer(): BuiltInServer? = serverFuture?.asCompletableFuture()?.join()
|
||||
|
||||
fun lockAndTryActivate(args: List<String>, mainScope: CoroutineScope): Pair<ActivationStatus, CliResult?> {
|
||||
log("enter: lock(config=%s system=%s)", configPath, systemPath)
|
||||
lockPortFiles()
|
||||
val portToPath = HashMap<Int, MutableList<String>>()
|
||||
readPort(configPath, portToPath)
|
||||
readPort(systemPath, portToPath)
|
||||
if (!portToPath.isEmpty()) {
|
||||
for ((key, value) in portToPath) {
|
||||
val status = tryActivate(portNumber = key, paths = value, args = args, systemPath = systemPath)
|
||||
if (status.first != ActivationStatus.NO_INSTANCE) {
|
||||
log("exit: lock(): " + status.second)
|
||||
unlockPortFiles()
|
||||
return status
|
||||
}
|
||||
}
|
||||
else {
|
||||
myRedirectedPortFile = null;
|
||||
}
|
||||
|
||||
serverFuture = mainScope.async(Dispatchers.IO) {
|
||||
val activity = StartUpMeasurer.startActivity("built-in server launch", ActivityCategory.DEFAULT)
|
||||
val token = UUID.randomUUID().toString()
|
||||
val lockedPaths = arrayOf(configPath, systemPath)
|
||||
val server = BuiltInServer.start(firstPort = 6942, portsCount = 50, tryAnyPort = false) {
|
||||
MyChannelInboundHandler(lockedPaths = lockedPaths, commandProcessor = { commandProcessorRef.get() }, token = token)
|
||||
}
|
||||
val portBytes = server.port.toString().toByteArray(StandardCharsets.UTF_8)
|
||||
Files.write(configPath.resolve(SpecialConfigFiles.PORT_FILE), portBytes)
|
||||
Files.write(systemPath.resolve(SpecialConfigFiles.PORT_FILE), portBytes)
|
||||
val tokenFile = systemPath.resolve(SpecialConfigFiles.TOKEN_FILE)
|
||||
Files.write(tokenFile, token.toByteArray(StandardCharsets.UTF_8))
|
||||
val view = Files.getFileAttributeView(tokenFile, PosixFileAttributeView::class.java)
|
||||
if (view != null) {
|
||||
try {
|
||||
view.setPermissions(EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE))
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log(e)
|
||||
}
|
||||
}
|
||||
unlockPortFiles()
|
||||
|
||||
activity.end()
|
||||
server
|
||||
}
|
||||
log("exit: lock(): succeed")
|
||||
return ActivationStatus.NO_INSTANCE to null
|
||||
myProcessor = processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* According to [Stack Overflow](https://stackoverflow.com/a/12652718/3463676), file locks should be removed on process segfault.
|
||||
* According to [FileLock] documentation, file locks are marked as invalid on JVM termination.
|
||||
*
|
||||
* Unlocking of port files (via [.unlockPortFiles]) happens either after builtin server init, or on app termination.
|
||||
*
|
||||
* Because of that, we do not care about non-starting Netty leading to infinite lock handling, as the IDE is not ready to
|
||||
* accept connections anyway; on app termination, the locks will be released.
|
||||
* Tries to grab a port file and start listening for incoming requests.
|
||||
* Failing that, attempts to connect via the existing port file to an already running instance.
|
||||
* Returns {@code null} on successfully locking the directories, a non-null value on successfully activating another instance,
|
||||
* or throws a {@link CannotActivateException}.
|
||||
*/
|
||||
@Suppress("KDocUnresolvedReference")
|
||||
@Synchronized
|
||||
private fun lockPortFiles() {
|
||||
check(lockedFiles.isEmpty()) { "File locking must not be called twice" }
|
||||
val options = arrayOf<OpenOption>(StandardOpenOption.CREATE, StandardOpenOption.APPEND)
|
||||
NioFiles.createDirectories(configPath)
|
||||
val cc = FileChannel.open(configPath.resolve(SpecialConfigFiles.PORT_LOCK_FILE), *options)
|
||||
lockedFiles.add(cc)
|
||||
lockedFiles.add(cc.lock())
|
||||
NioFiles.createDirectories(systemPath)
|
||||
val sc = FileChannel.open(systemPath.resolve(SpecialConfigFiles.PORT_LOCK_FILE), *options)
|
||||
lockedFiles.add(sc)
|
||||
lockedFiles.add(sc.lock())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun unlockPortFiles() {
|
||||
check(!lockedFiles.isEmpty()) { "File unlocking must not be called twice" }
|
||||
for (i in lockedFiles.indices.reversed()) {
|
||||
lockedFiles.get(i).close()
|
||||
}
|
||||
lockedFiles.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun allowActivation(input: DataInputStream) {
|
||||
if (SystemInfoRt.isWindows) {
|
||||
@Nullable CliResult lockOrActivate(@NotNull Path currentDirectory, @NotNull List<String> args) throws CannotActivateException {
|
||||
try {
|
||||
User32Ex.INSTANCE.AllowSetForegroundWindow(WinDef.DWORD(input.readLong()))
|
||||
var configDir = NioFiles.createDirectories(myLockFile.getParent());
|
||||
var systemDir = NioFiles.createDirectories(myPortFile.getParent());
|
||||
if (Files.isSameFile(systemDir, configDir)) {
|
||||
throw new IllegalArgumentException(BootstrapBundle.message("bootstrap.error.same.directories"));
|
||||
}
|
||||
|
||||
try {
|
||||
return tryListen();
|
||||
}
|
||||
catch (BindException | FileAlreadyExistsException e) {
|
||||
LOG.debug(e);
|
||||
}
|
||||
|
||||
try {
|
||||
return tryConnect(args, currentDirectory);
|
||||
}
|
||||
catch (SocketException e) {
|
||||
LOG.debug(e);
|
||||
}
|
||||
|
||||
Files.deleteIfExists(myPortFile);
|
||||
return tryListen();
|
||||
}
|
||||
catch (e: Exception) {
|
||||
log(e)
|
||||
catch (IOException e) {
|
||||
LOG.debug(e);
|
||||
throw new CannotActivateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryActivate(portNumber: Int,
|
||||
paths: List<String>,
|
||||
args: List<String>,
|
||||
systemPath: Path): Pair<SocketLock.ActivationStatus, CliResult?> {
|
||||
log("trying: port=%s", portNumber)
|
||||
try {
|
||||
Socket(InetAddress.getByName("127.0.0.1"), portNumber).use { socket ->
|
||||
socket.soTimeout = 5000
|
||||
val input = DataInputStream(socket.getInputStream())
|
||||
val stringList = readStringSequence(input)
|
||||
// backward compatibility: requires at least one path to match
|
||||
val result = paths.any(stringList::contains)
|
||||
if (result) {
|
||||
void dispose() {
|
||||
var serverChannel = myServerChannel;
|
||||
myServerChannel = null;
|
||||
if (serverChannel != null) {
|
||||
Suppressions.runSuppressing(
|
||||
() -> serverChannel.close(),
|
||||
() -> {
|
||||
if (myRedirectedPortFile != null) {
|
||||
Files.deleteIfExists(myRedirectedPortFile);
|
||||
}
|
||||
},
|
||||
() -> Files.deleteIfExists(myPortFile),
|
||||
() -> Files.deleteIfExists(myLockFile));
|
||||
}
|
||||
}
|
||||
|
||||
private CliResult tryConnect(List<String> args, Path currentDirectory) throws IOException {
|
||||
try (var socketChannel = SocketChannel.open(StandardProtocolFamily.UNIX)) {
|
||||
UnixDomainSocketAddress address;
|
||||
if (myRedirectedPortFile != null) {
|
||||
address = UnixDomainSocketAddress.of(Files.readString(myPortFile));
|
||||
}
|
||||
else {
|
||||
address = UnixDomainSocketAddress.of(myPortFile);
|
||||
}
|
||||
|
||||
socketChannel.connect(address);
|
||||
|
||||
allowActivation();
|
||||
|
||||
var request = new ArrayList<String>(args.size() + 1);
|
||||
request.add(currentDirectory.toString());
|
||||
request.addAll(args);
|
||||
sendLines(socketChannel, request);
|
||||
|
||||
var response = readLines(socketChannel);
|
||||
if (response.size() != 2) throw new IOException(BootstrapBundle.message("bootstrap.error.malformed.response", response));
|
||||
var exitCode = Integer.parseInt(response.get(0));
|
||||
var message = response.get(1);
|
||||
return new CliResult(exitCode, message.isEmpty() ? null : message);
|
||||
}
|
||||
}
|
||||
|
||||
private void allowActivation() {
|
||||
if (SystemInfoRt.isWindows && JnaLoader.isLoaded()) {
|
||||
try {
|
||||
var remotePID = Long.parseLong(Files.readString(myLockFile));
|
||||
User32Ex.INSTANCE.AllowSetForegroundWindow(new WinDef.DWORD(remotePID));
|
||||
}
|
||||
catch (Throwable t) {
|
||||
LOG.debug(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable CliResult tryListen() throws IOException, CannotActivateException {
|
||||
var serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
|
||||
|
||||
UnixDomainSocketAddress address;
|
||||
if (myRedirectedPortFile != null) {
|
||||
Files.writeString(myPortFile, myRedirectedPortFile.toString(), StandardOpenOption.CREATE_NEW);
|
||||
address = UnixDomainSocketAddress.of(myRedirectedPortFile);
|
||||
}
|
||||
else {
|
||||
address = UnixDomainSocketAddress.of(myPortFile);
|
||||
}
|
||||
|
||||
serverChannel.bind(address);
|
||||
myServerChannel = serverChannel;
|
||||
|
||||
try {
|
||||
lockDirectory(myPid);
|
||||
}
|
||||
catch (Exception e) {
|
||||
LOG.debug(e);
|
||||
dispose();
|
||||
throw new CannotActivateException(e);
|
||||
}
|
||||
|
||||
new Thread(this::acceptConnections, SERVER_THREAD_NAME).start();
|
||||
return null;
|
||||
}
|
||||
|
||||
private void lockDirectory(String pid) throws Exception {
|
||||
IOException first = null;
|
||||
|
||||
for (var i = 0; i < LOCK_RETRIES; i++) {
|
||||
try {
|
||||
Files.writeString(myLockFile, pid, StandardOpenOption.CREATE_NEW);
|
||||
return;
|
||||
}
|
||||
catch (FileAlreadyExistsException e) {
|
||||
first = Suppressions.addSuppressed(first, e);
|
||||
try {
|
||||
allowActivation(input)
|
||||
val token = readOneLine(systemPath.resolve(SpecialConfigFiles.TOKEN_FILE))
|
||||
val out = DataOutputStream(socket.getOutputStream())
|
||||
var currentDirectory = System.getenv(LAUNCHER_INITIAL_DIRECTORY_ENV_VAR)
|
||||
if (currentDirectory == null) {
|
||||
currentDirectory = "."
|
||||
}
|
||||
out.writeUTF(
|
||||
ACTIVATE_COMMAND + token + '\u0000' + Path.of(currentDirectory).toAbsolutePath() + '\u0000' + args.joinToString(separator = "\u0000"))
|
||||
out.flush()
|
||||
socket.soTimeout = 0
|
||||
val response = readStringSequence(input)
|
||||
log("read: response=%s", java.lang.String.join(";", response))
|
||||
if (!response.isEmpty() && OK_RESPONSE == response[0]) {
|
||||
return SocketLock.ActivationStatus.ACTIVATED to mapResponseToCliResult(response)
|
||||
try {
|
||||
var otherPid = Long.parseLong(Files.readString(myLockFile));
|
||||
if (ProcessHandle.of(otherPid).isPresent()) {
|
||||
throw new Exception(BootstrapBundle.message("bootstrap.error.still.running", otherPid), e);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ignored) { }
|
||||
Files.deleteIfExists(myLockFile);
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log(e)
|
||||
catch (IOException ex) {
|
||||
first = Suppressions.addSuppressed(first, ex);
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
log(e)
|
||||
}
|
||||
return SocketLock.ActivationStatus.CANNOT_ACTIVATE to null
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: ConnectException) {
|
||||
log("%s (stale port file?)", e.message!!)
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log(e)
|
||||
}
|
||||
return SocketLock.ActivationStatus.NO_INSTANCE to null
|
||||
}
|
||||
|
||||
private fun readOneLine(file: Path) = Files.newBufferedReader(file).use { it.readLine().trim() }
|
||||
|
||||
private fun readPort(dir: Path, portToPath: MutableMap<Int, MutableList<String>>) {
|
||||
try {
|
||||
portToPath.computeIfAbsent(readOneLine(dir.resolve(SpecialConfigFiles.PORT_FILE)).toInt()) { ArrayList() }.add(dir.toString())
|
||||
}
|
||||
catch (ignore: NoSuchFileException) {
|
||||
}
|
||||
catch (e: Exception) {
|
||||
// no need to delete a file, it will be overwritten
|
||||
log(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readStringSequence(input: DataInput): List<String> {
|
||||
val result = ArrayList<String>()
|
||||
while (true) {
|
||||
try {
|
||||
val string = input.readUTF()
|
||||
log("read: path=%s", string)
|
||||
if (PATHS_EOT_RESPONSE == string) {
|
||||
break
|
||||
}
|
||||
result.add(string)
|
||||
}
|
||||
catch (e: IOException) {
|
||||
log("read: %s", e.message!!)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun mapResponseToCliResult(responseParts: List<String>): CliResult {
|
||||
if (responseParts.size > 3 || responseParts.size < 2) {
|
||||
throw IllegalArgumentException("bad response: " + java.lang.String.join(";", responseParts))
|
||||
}
|
||||
val code = try {
|
||||
responseParts[1].toInt()
|
||||
}
|
||||
catch (e: NumberFormatException) {
|
||||
throw IllegalArgumentException("Second part is not a parsable return code", e)
|
||||
}
|
||||
return CliResult(code, if (responseParts.size == 3) responseParts[2] else null)
|
||||
}
|
||||
|
||||
private fun log(format: String, vararg args: Any) {
|
||||
val logger = Logger.getInstance(SocketLock::class.java)
|
||||
if (logger.isDebugEnabled) {
|
||||
logger.debug(String.format(format, *args))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendStringSequence(context: ChannelHandlerContext, strings: List<String>) {
|
||||
val buffer = context.alloc().ioBuffer(1024)
|
||||
var success = false
|
||||
try {
|
||||
ByteBufOutputStream(buffer).use { out ->
|
||||
for (s in strings) {
|
||||
out.writeUTF(s)
|
||||
}
|
||||
out.writeUTF(PATHS_EOT_RESPONSE)
|
||||
if (SystemInfoRt.isWindows) {
|
||||
// see 'allowActivation' function
|
||||
out.writeLong(ProcessHandle.current().pid())
|
||||
}
|
||||
success = true
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (!success) {
|
||||
buffer.release()
|
||||
}
|
||||
}
|
||||
context.writeAndFlush(buffer)
|
||||
}
|
||||
|
||||
private fun log(e: Exception) {
|
||||
Logger.getInstance(SocketLock::class.java).warn(e)
|
||||
}
|
||||
|
||||
private class MyChannelInboundHandler(lockedPaths: Array<Path>,
|
||||
private val commandProcessor: () -> (List<String>) -> Deferred<CliResult>,
|
||||
private val token: String) : MessageDecoder() {
|
||||
private enum class State {
|
||||
HEADER, CONTENT
|
||||
throw first;
|
||||
}
|
||||
|
||||
private val lockedPaths = lockedPaths.map(Path::toString)
|
||||
private var state = State.HEADER
|
||||
|
||||
override fun channelActive(context: ChannelHandlerContext) {
|
||||
sendStringSequence(context, lockedPaths)
|
||||
}
|
||||
|
||||
override fun messageReceived(context: ChannelHandlerContext, input: ByteBuf) {
|
||||
private void acceptConnections() {
|
||||
var serverChannel = myServerChannel;
|
||||
if (serverChannel == null) return;
|
||||
while (true) {
|
||||
when (state) {
|
||||
State.HEADER -> {
|
||||
val buffer = getBufferIfSufficient(input, 2, context) ?: return
|
||||
contentLength = buffer.readUnsignedShort()
|
||||
if (contentLength > 8192) {
|
||||
context.close()
|
||||
return
|
||||
}
|
||||
state = State.CONTENT
|
||||
}
|
||||
State.CONTENT -> {
|
||||
val command = readChars(input) ?: return
|
||||
if (command.startsWith(ACTIVATE_COMMAND)) {
|
||||
val data = command.subSequence(ACTIVATE_COMMAND.length, command.length).toString()
|
||||
val tokenizer = StringTokenizer(data, if (data.contains("\u0000")) "\u0000" else "\uFFFD")
|
||||
val tokenOK = tokenizer.hasMoreTokens() && token == tokenizer.nextToken()
|
||||
val result = if (tokenOK) {
|
||||
val list = ArrayList<String>()
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
list.add(tokenizer.nextToken())
|
||||
}
|
||||
|
||||
try {
|
||||
@Suppress("RAW_RUN_BLOCKING")
|
||||
runBlocking {
|
||||
commandProcessor()(list).await()
|
||||
}
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
val message = getDiagnosticErrorMessage(e)
|
||||
CliResult(AppExitCodes.ACTIVATE_ERROR, message)
|
||||
}
|
||||
}
|
||||
else {
|
||||
log(UnsupportedOperationException("unauthorized request: $command"))
|
||||
CliResult(AppExitCodes.ACTIVATE_WRONG_TOKEN_CODE, IdeBundle.message("activation.auth.message"))
|
||||
}
|
||||
|
||||
val exitCode = result.exitCode.toString()
|
||||
val message = result.message
|
||||
@Suppress("ReplaceJavaStaticMethodWithKotlinAnalog")
|
||||
val response = if (message == null) {
|
||||
java.util.List.of(OK_RESPONSE, exitCode)
|
||||
}
|
||||
else {
|
||||
java.util.List.of(OK_RESPONSE, exitCode, message)
|
||||
}
|
||||
sendStringSequence(context, response)
|
||||
}
|
||||
context.close()
|
||||
}
|
||||
try {
|
||||
var socketChannel = serverChannel.accept();
|
||||
ProcessIOExecutorService.INSTANCE.execute(() -> handleConnection(socketChannel));
|
||||
}
|
||||
catch (ClosedChannelException e) { break; }
|
||||
catch (IOException e) {
|
||||
LOG.warn(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDiagnosticErrorMessage(e: Throwable): @NlsSafe String {
|
||||
val processInfo = try {
|
||||
val ph = ProcessHandle.current()
|
||||
val command = ph.info().command().orElse("<N/A>")
|
||||
val pid = ph.pid()
|
||||
private void handleConnection(SocketChannel socketChannel) {
|
||||
try (socketChannel) {
|
||||
var request = readLines(socketChannel);
|
||||
|
||||
"PID: $pid, Command: $command"
|
||||
} catch (e: Throwable) {
|
||||
"<Unable to determine process info: ${e.message}>"
|
||||
CliResult result;
|
||||
try {
|
||||
if (request.size() == 2 && INTERNAL_DIAGNOSTIC_COMMAND.equals(request.get(1))) {
|
||||
@SuppressWarnings("HardCodedStringLiteral") var message = "PID=" + myPid + " thread=" + Thread.currentThread();
|
||||
result = new CliResult(0, message);
|
||||
}
|
||||
else {
|
||||
result = myProcessor.apply(request);
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
LOG.error(t);
|
||||
var error = requireNonNullElse(t.getMessage(), "Unknown error");
|
||||
var message = BootstrapBundle.message("bootstrap.error.request.failed", myPid, t.getClass(), error, LoggerFactory.getLogFilePath());
|
||||
result = new CliResult(AppExitCodes.ACTIVATE_ERROR, message);
|
||||
}
|
||||
|
||||
sendLines(socketChannel, List.of(String.valueOf(result.getExitCode()), requireNonNullElse(result.getMessage(), "")));
|
||||
}
|
||||
|
||||
val message = e.message ?: "<null>"
|
||||
|
||||
val stackTrace = try {
|
||||
e.stackTraceToString()
|
||||
} catch (e: Throwable) {
|
||||
"<Unable to get stack trace: ${e.message}>"
|
||||
catch (IOException e) {
|
||||
LOG.warn(e);
|
||||
}
|
||||
|
||||
return "Response from process: $processInfo, exception: $message\n$stackTrace"
|
||||
}
|
||||
|
||||
//<editor-fold desc="I/O helpers">
|
||||
private static void sendLines(SocketChannel socketChannel, List<String> lines) throws IOException {
|
||||
var buffer = ByteBuffer.allocate(BUFFER_LENGTH);
|
||||
buffer.putInt(MARKER).putShort((short)0);
|
||||
|
||||
for (var line : lines) {
|
||||
var bytes = line.getBytes(StandardCharsets.UTF_8);
|
||||
buffer.putShort((short)bytes.length);
|
||||
buffer.put(bytes);
|
||||
}
|
||||
|
||||
buffer.putShort(4, (short)buffer.position());
|
||||
|
||||
buffer.flip();
|
||||
while (buffer.hasRemaining()) {
|
||||
socketChannel.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> readLines(SocketChannel socketChannel) throws IOException {
|
||||
var buffer = ByteBuffer.allocate(BUFFER_LENGTH);
|
||||
|
||||
while (buffer.position() < HEADER_LENGTH) {
|
||||
if (socketChannel.read(buffer) < 0) {
|
||||
throw new EOFException("Expected " + HEADER_LENGTH + " bytes, got " + buffer.position());
|
||||
}
|
||||
}
|
||||
var length = buffer.getShort(4);
|
||||
while (buffer.position() < length) {
|
||||
if (socketChannel.read(buffer) < 0) {
|
||||
throw new EOFException("Expected " + length + " bytes, got " + buffer.position());
|
||||
}
|
||||
}
|
||||
|
||||
buffer.flip();
|
||||
var marker = buffer.getInt();
|
||||
if (marker != MARKER) throw new StreamCorruptedException("Invalid marker: 0x" + Integer.toHexString(marker));
|
||||
buffer.getShort();
|
||||
|
||||
var lines = new ArrayList<String>();
|
||||
while (buffer.hasRemaining()) {
|
||||
length = buffer.getShort();
|
||||
var bytes = new byte[length];
|
||||
buffer.get(bytes);
|
||||
lines.add(new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.intellij.ide.ui.html.GlobalStyleSheetHolder
|
||||
import com.intellij.ide.ui.laf.IdeaLaf
|
||||
import com.intellij.ide.ui.laf.IntelliJLaf
|
||||
import com.intellij.ide.ui.laf.darcula.DarculaLaf
|
||||
import com.intellij.idea.SocketLock.ActivationStatus
|
||||
import com.intellij.idea.DirectoryLock.CannotActivateException
|
||||
import com.intellij.jna.JnaLoader
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.ApplicationNamesInfo
|
||||
@@ -49,14 +49,13 @@ import com.intellij.util.ui.accessibility.ScreenReader
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import org.jetbrains.ide.BuiltInServerManager
|
||||
import org.jetbrains.io.BuiltInServer
|
||||
import sun.awt.AWTAutoShutdown
|
||||
import java.awt.Font
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.awt.Toolkit
|
||||
import java.awt.dnd.DragSource
|
||||
import java.io.File
|
||||
import java.io.IOError
|
||||
import java.io.IOException
|
||||
import java.lang.invoke.MethodHandles
|
||||
import java.lang.invoke.MethodType
|
||||
@@ -71,6 +70,7 @@ import java.nio.file.attribute.PosixFilePermission
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.BiFunction
|
||||
import java.util.logging.ConsoleHandler
|
||||
@@ -98,7 +98,8 @@ private const val IDEA_CLASS_BEFORE_APPLICATION_PROPERTY = "idea.class.before.ap
|
||||
private const val DISABLE_IMPLICIT_READ_ON_EDT_PROPERTY = "idea.disable.implicit.read.on.edt"
|
||||
private const val MAGIC_MAC_PATH = "/AppTranslocation/"
|
||||
|
||||
private var socketLock: SocketLock? = null
|
||||
private val commandProcessor: AtomicReference<(List<String>) -> Deferred<CliResult>> =
|
||||
AtomicReference { CompletableDeferred(CliResult(AppExitCodes.ACTIVATE_NOT_INITIALIZED, IdeBundle.message("activation.not.initialized"))) }
|
||||
|
||||
// checked - using a Deferred type doesn't lead to loading this class on StartupUtil init
|
||||
internal var shellEnvDeferred: Deferred<Boolean?>? = null
|
||||
@@ -113,21 +114,16 @@ fun CoroutineScope.startApplication(args: List<String>,
|
||||
// required for DisabledPluginsState and EUA
|
||||
ApplicationInfoImpl.getShadowInstance()
|
||||
}
|
||||
val pathDeferred = async(CoroutineName("config path computing") + Dispatchers.IO) {
|
||||
Pair(canonicalPath(PathManager.getConfigPath()), canonicalPath(PathManager.getSystemPath()))
|
||||
}
|
||||
|
||||
val isHeadless = AppMode.isHeadless()
|
||||
|
||||
val configImportNeededDeferred = async {
|
||||
val (configPath, _) = pathDeferred.await()
|
||||
!isHeadless && (!Files.exists(configPath) || Files.exists(configPath.resolve(ConfigImportHelper.CUSTOM_MARKER_FILE_NAME)))
|
||||
val configImportNeededDeferred = if (isHeadless) CompletableDeferred(false) else async {
|
||||
val configPath = PathManager.getConfigDir()
|
||||
!Files.exists(configPath) || Files.exists(configPath.resolve(ConfigImportHelper.CUSTOM_MARKER_FILE_NAME))
|
||||
}
|
||||
|
||||
val lockSystemDirsJob = lockSystemDirs(configImportNeededDeferred = configImportNeededDeferred,
|
||||
pathDeferred = pathDeferred,
|
||||
args = args,
|
||||
mainScope = mainScope)
|
||||
val lockSystemDirsJob = lockSystemDirs(configImportNeededDeferred, args)
|
||||
|
||||
val consoleLoggerJob = configureJavaUtilLogging()
|
||||
|
||||
launch {
|
||||
@@ -175,7 +171,7 @@ fun CoroutineScope.startApplication(args: List<String>,
|
||||
val initLafJob = initUi(initAwtToolkitAndEventQueueJob, preloadLafClassesJob)
|
||||
|
||||
// system dirs checking must happen after locking system dirs
|
||||
val checkSystemDirJob = checkSystemDirs(lockSystemDirsJob, pathDeferred)
|
||||
val checkSystemDirJob = checkSystemDirs(lockSystemDirsJob)
|
||||
|
||||
// log initialization must happen only after locking the system directory
|
||||
val logDeferred = setupLogger(consoleLoggerJob, checkSystemDirJob)
|
||||
@@ -384,18 +380,8 @@ internal val isImplicitReadOnEDTDisabled: Boolean
|
||||
get() = java.lang.Boolean.getBoolean(DISABLE_IMPLICIT_READ_ON_EDT_PROPERTY)
|
||||
|
||||
// called by the app after startup
|
||||
@Synchronized
|
||||
fun addExternalInstanceListener(processor: (List<String>) -> Deferred<CliResult>) {
|
||||
requireNotNull(socketLock) { "Not initialized yet" }.setCommandProcessor(processor)
|
||||
}
|
||||
|
||||
// used externally by TeamCity plugin (as TeamCity cannot use modern API to support old IDE versions)
|
||||
@Synchronized
|
||||
@Deprecated("")
|
||||
fun getServer(): BuiltInServer? = socketLock?.getServer()
|
||||
|
||||
@Synchronized
|
||||
fun getServerFutureAsync(): Deferred<BuiltInServer?> = socketLock?.serverFuture ?: CompletableDeferred(value = null)
|
||||
fun addExternalInstanceListener(processor: (List<String>) -> Deferred<CliResult>) =
|
||||
commandProcessor.set(processor)
|
||||
|
||||
private fun runPreAppClass(log: Logger, args: List<String>) {
|
||||
val classBeforeAppProperty = System.getProperty(IDEA_CLASS_BEFORE_APPLICATION_PROPERTY) ?: return
|
||||
@@ -642,18 +628,17 @@ fun checkHiDPISettings() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.checkSystemDirs(lockSystemDirJob: Job, pathDeferred: Deferred<Pair<Path, Path>>): Job {
|
||||
return launch {
|
||||
private fun CoroutineScope.checkSystemDirs(lockSystemDirJob: Job): Job =
|
||||
launch {
|
||||
lockSystemDirJob.join()
|
||||
|
||||
val (configPath, systemPath) = pathDeferred.await()
|
||||
val (configPath, systemPath) = PathManager.getConfigDir() to PathManager.getSystemDir()
|
||||
runActivity("system dirs checking") {
|
||||
if (!doCheckSystemDirs(configPath, systemPath)) {
|
||||
exitProcess(AppExitCodes.DIR_CHECK_FAILED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doCheckSystemDirs(configPath: Path, systemPath: Path): Boolean {
|
||||
if (configPath == systemPath) {
|
||||
@@ -776,53 +761,43 @@ private fun checkDirectory(directory: Path,
|
||||
}
|
||||
}
|
||||
|
||||
// returns `true` when `checkConfig` is requested and config import is needed
|
||||
private fun CoroutineScope.lockSystemDirs(configImportNeededDeferred: Job,
|
||||
pathDeferred: Deferred<Pair<Path, Path>>,
|
||||
args: List<String>,
|
||||
mainScope: CoroutineScope): Job {
|
||||
if (socketLock != null) {
|
||||
throw AssertionError("Already initialized")
|
||||
}
|
||||
|
||||
return launch(Dispatchers.IO) {
|
||||
val (configPath, systemPath) = pathDeferred.await()
|
||||
private fun CoroutineScope.lockSystemDirs(configImportNeededDeferred: Job, args: List<String>): Job =
|
||||
launch(Dispatchers.IO) {
|
||||
// the "import-needed" check must be performed strictly before IDE directories are locked
|
||||
configImportNeededDeferred.join()
|
||||
runActivity("system dirs locking") {
|
||||
// this check must be performed before system directories are locked
|
||||
socketLock = SocketLock(configPath, systemPath)
|
||||
val status = socketLock!!.lockAndTryActivate(args = args, mainScope = mainScope)
|
||||
when (status.first) {
|
||||
ActivationStatus.NO_INSTANCE -> {
|
||||
ShutDownTracker.getInstance().registerShutdownTask {
|
||||
synchronized(AppStarter::class.java) {
|
||||
socketLock!!.dispose()
|
||||
socketLock = null
|
||||
|
||||
// Temporary hack to debug "Zombie" process issue. See CWM-7058
|
||||
// TL;DR ShutDownTracker gets called but application still exists
|
||||
if (AppMode.isRemoteDevHost()) {
|
||||
val stacktrace = Thread.currentThread().stackTrace.joinToString("\n")
|
||||
println("ShutDownTracker stacktrace:\n$stacktrace")
|
||||
}
|
||||
}
|
||||
runActivity("system dirs locking") {
|
||||
val directoryLock = DirectoryLock(PathManager.getConfigDir(), PathManager.getSystemDir()) { args ->
|
||||
@Suppress("RAW_RUN_BLOCKING")
|
||||
runBlocking {
|
||||
commandProcessor.get()(args).await()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val currentDir = Path.of(System.getenv(LAUNCHER_INITIAL_DIRECTORY_ENV_VAR) ?: "").toAbsolutePath()
|
||||
when (val result = directoryLock.lockOrActivate(currentDir, args)) {
|
||||
null -> ShutDownTracker.getInstance().registerShutdownTask {
|
||||
directoryLock.dispose()
|
||||
}
|
||||
else -> {
|
||||
result.message?.let { println(it) }
|
||||
exitProcess(result.exitCode)
|
||||
}
|
||||
}
|
||||
ActivationStatus.ACTIVATED -> {
|
||||
val result = status.second!!
|
||||
println(result.message ?: "Already running")
|
||||
exitProcess(result.exitCode)
|
||||
}
|
||||
catch (e: CannotActivateException) {
|
||||
val message = BootstrapBundle.message("bootstrap.error.cannot.activate.message", e.javaClass.simpleName, e.message)
|
||||
if (args.isEmpty()) {
|
||||
StartupErrorReporter.showMessage(BootstrapBundle.message("bootstrap.error.cannot.activate.title"), message, true)
|
||||
}
|
||||
ActivationStatus.CANNOT_ACTIVATE -> {
|
||||
val message = BootstrapBundle.message("bootstrap.error.message.only.one.instance.of.0.can.be.run.at.a.time",
|
||||
ApplicationNamesInfo.getInstance().productName)
|
||||
StartupErrorReporter.showMessage(BootstrapBundle.message("bootstrap.error.title.too.many.instances"), message, true)
|
||||
exitProcess(AppExitCodes.INSTANCE_CHECK_FAILED)
|
||||
else {
|
||||
println(message)
|
||||
}
|
||||
exitProcess(AppExitCodes.INSTANCE_CHECK_FAILED)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.setupLogger(consoleLoggerJob: Job, checkSystemDirJob: Job): Deferred<Logger> {
|
||||
return async {
|
||||
@@ -933,22 +908,6 @@ private fun patchSystem(isHeadless: Boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
fun canonicalPath(path: String): Path {
|
||||
return try {
|
||||
// `toRealPath` doesn't restore a canonical file name on case-insensitive UNIX filesystems
|
||||
Path.of(File(path).canonicalPath)
|
||||
}
|
||||
catch (ignore: IOException) {
|
||||
val file = Path.of(path)
|
||||
try {
|
||||
file.toAbsolutePath()
|
||||
}
|
||||
catch (ignored: IOError) {
|
||||
}
|
||||
file.normalize()
|
||||
}
|
||||
}
|
||||
|
||||
interface AppStarter {
|
||||
fun prepareStart(args: List<String>) {}
|
||||
|
||||
@@ -968,3 +927,13 @@ class Java11ShimImpl : Java11Shim() {
|
||||
|
||||
override fun <E : Any> copyOfCollection(collection: Collection<E>): List<E> = java.util.List.copyOf(collection)
|
||||
}
|
||||
|
||||
//<editor-fold desc="Deprecated stuff.">
|
||||
@Deprecated("Please use BuiltInServerManager instead")
|
||||
fun getServer(): BuiltInServer? {
|
||||
val instance = BuiltInServerManager.getInstance()
|
||||
instance.waitForStart()
|
||||
val candidate = instance.serverDisposable
|
||||
return if (candidate is BuiltInServer) candidate else null
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
@@ -1,94 +1,202 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.idea;
|
||||
|
||||
import com.intellij.ide.CliResult;
|
||||
import com.intellij.ide.SpecialConfigFiles;
|
||||
import com.intellij.idea.DirectoryLock.CannotActivateException;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.SystemInfo;
|
||||
import com.intellij.openapi.util.io.NioFiles;
|
||||
import com.intellij.testFramework.TestLoggerFactory;
|
||||
import com.intellij.testFramework.rules.TempDirectory;
|
||||
import kotlinx.coroutines.GlobalScope;
|
||||
import com.intellij.util.Suppressions;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.rules.Timeout;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
public class LockSupportTest {
|
||||
public abstract sealed class DirectoryLockTest {
|
||||
static {
|
||||
Logger.setFactory(TestLoggerFactory.class);
|
||||
}
|
||||
|
||||
public static final class DirectModeTest extends DirectoryLockTest {
|
||||
@Override
|
||||
protected Path getTestDir() throws IOException {
|
||||
var testDir = tempDir.getRootPath();
|
||||
if (testDir.toString().length() > 90) {
|
||||
var dir = SystemInfo.isWindows ? Path.of(System.getenv("SystemRoot"), "Temp") : Path.of("/tmp");
|
||||
testDir = Files.createTempDirectory(dir, testDir.getFileName().toString());
|
||||
}
|
||||
return testDir;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class IndirectModeTest extends DirectoryLockTest {
|
||||
@Override
|
||||
protected Path getTestDir() throws IOException {
|
||||
var testDir = tempDir.getRootPath();
|
||||
if (testDir.toString().length() < 100) {
|
||||
var padding = "length_padding_" + "x".repeat(100 - testDir.toString().length());
|
||||
testDir = Files.createDirectories(testDir.resolve(padding));
|
||||
}
|
||||
return testDir;
|
||||
}
|
||||
}
|
||||
|
||||
@Rule public final TestRule watcher = TestLoggerFactory.createTestWatcher();
|
||||
@Rule public final Timeout timeout = Timeout.seconds(30);
|
||||
@Rule public final TempDirectory tempDir = new TempDirectory();
|
||||
|
||||
@Test(timeout = 30000)
|
||||
public void useCanonicalPathLock() {
|
||||
private Path testDir;
|
||||
private final List<DirectoryLock> activeLocks = new ArrayList<>();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
testDir = getTestDir();
|
||||
}
|
||||
|
||||
protected abstract Path getTestDir() throws IOException;
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
Suppressions.runSuppressing(
|
||||
() -> activeLocks.forEach(DirectoryLock::dispose),
|
||||
() -> activeLocks.clear(),
|
||||
() -> {
|
||||
if (testDir != tempDir.getRootPath()) {
|
||||
NioFiles.deleteRecursively(testDir);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private DirectoryLock createLock(Path configPath, Path systemPath) {
|
||||
var lock = new DirectoryLock(configPath, systemPath, args -> CliResult.OK);
|
||||
activeLocks.add(lock);
|
||||
return lock;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathCollision() {
|
||||
var path = testDir.resolve("same");
|
||||
var lock = createLock(path, path);
|
||||
assertThatThrownBy(() -> lock.lockOrActivate(Path.of(""), List.of())).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockingNonExistingDirectories() throws Exception {
|
||||
var lock = createLock(testDir.resolve("c"), testDir.resolve("s"));
|
||||
assertNull(lock.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockingVacantDirectories() throws Exception {
|
||||
var lock = createLock(Files.createDirectories(testDir.resolve("c")), Files.createDirectories(testDir.resolve("s")));
|
||||
assertNull(lock.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lockIndependence() throws Exception {
|
||||
var lock1 = createLock(testDir.resolve("c1"), testDir.resolve("s1"));
|
||||
assertNull(lock1.lockOrActivate(Path.of(""), List.of()));
|
||||
var lock2 = createLock(testDir.resolve("c2"), testDir.resolve("s2"));
|
||||
assertNull(lock2.lockOrActivate(Path.of(""), List.of()));
|
||||
var lock1copy = createLock(testDir.resolve("c1"), testDir.resolve("s1"));
|
||||
assertNotNull(lock1copy.lockOrActivate(Path.of(""), List.of()));
|
||||
var lock2copy = createLock(testDir.resolve("c2"), testDir.resolve("s2"));
|
||||
assertNotNull(lock2copy.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void releasingDirectories() throws Exception {
|
||||
var configDir = testDir.resolve("c");
|
||||
var systemDir = testDir.resolve("s");
|
||||
var lock1 = createLock(configDir, systemDir);
|
||||
var lock2 = createLock(configDir, systemDir);
|
||||
assertNull(lock1.lockOrActivate(Path.of(""), List.of()));
|
||||
assertNotNull(lock2.lockOrActivate(Path.of(""), List.of()));
|
||||
lock1.dispose();
|
||||
assertNull(lock2.lockOrActivate(Path.of(""), List.of()));
|
||||
lock2.dispose();
|
||||
assertThat(configDir).isEmptyDirectory();
|
||||
assertThat(systemDir).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activatingViaCaseMismatchingPath() throws Exception {
|
||||
assumeTrue("case-insensitive system-only", !SystemInfo.isFileSystemCaseSensitive);
|
||||
|
||||
var path1 = tempDir.getRootPath().toString();
|
||||
var path2 = path1.toUpperCase(Locale.ENGLISH);
|
||||
var lock1 = new SocketLock(StartupUtil.canonicalPath(path1 + "/c"), StartupUtil.canonicalPath(path1 + "/s"));
|
||||
var lock2 = new SocketLock(StartupUtil.canonicalPath(path2 + "/c"), StartupUtil.canonicalPath(path2 + "/s"));
|
||||
try {
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, tryActivate(lock1));
|
||||
assertEquals(SocketLock.ActivationStatus.ACTIVATED, tryActivate(lock2));
|
||||
}
|
||||
finally {
|
||||
lock1.dispose();
|
||||
lock2.dispose();
|
||||
}
|
||||
var lock1 = createLock(testDir.resolve("c"), testDir.resolve("s"));
|
||||
var lock2 = createLock(testDir.resolve("C"), testDir.resolve("S"));
|
||||
assertNull(lock1.lockOrActivate(Path.of(""), List.of()));
|
||||
assertNotNull(lock2.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
@Test(timeout = 30000)
|
||||
public void lock() {
|
||||
var lock = new SocketLock(tempDir.getRootPath().resolve("c"), tempDir.getRootPath().resolve("s"));
|
||||
try {
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, lock.lockAndTryActivate(List.of(), GlobalScope.INSTANCE).getFirst());
|
||||
}
|
||||
finally {
|
||||
lock.dispose();
|
||||
}
|
||||
@Test
|
||||
public void activatingViaSymlinkedPath() throws Exception {
|
||||
var dir = Files.createDirectories(testDir.resolve("dir"));
|
||||
var link = Files.createSymbolicLink(testDir.resolve("link"), dir);
|
||||
var lock1 = createLock(dir.resolve("c"), dir.resolve("s"));
|
||||
var lock2 = createLock(link.resolve("c"), link.resolve("s"));
|
||||
assertNull(lock1.lockOrActivate(Path.of(""), List.of()));
|
||||
assertNotNull(lock2.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
@Test(timeout = 30000)
|
||||
public void twoLocks() {
|
||||
var toClose = new ArrayList<SocketLock>();
|
||||
try {
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c1", "s1"));
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c2", "s2"));
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c3", "s3"));
|
||||
assertEquals(SocketLock.ActivationStatus.ACTIVATED, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c1", "s1"));
|
||||
assertEquals(SocketLock.ActivationStatus.ACTIVATED, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c2", "s2"));
|
||||
assertEquals(SocketLock.ActivationStatus.ACTIVATED, createLockAndTryActivate(toClose, tempDir.getRootPath(), "c3", "s3"));
|
||||
}
|
||||
finally {
|
||||
toClose.forEach(SocketLock::dispose);
|
||||
}
|
||||
@Test
|
||||
public void symlinkPathCollision() throws Exception {
|
||||
var dir = Files.createDirectories(testDir.resolve("dir"));
|
||||
var configLink = Files.createSymbolicLink(testDir.resolve("c"), dir);
|
||||
var systemLink = Files.createSymbolicLink(testDir.resolve("s"), dir);
|
||||
var lock = createLock(configLink, systemLink);
|
||||
assertThatThrownBy(() -> lock.lockOrActivate(Path.of(""), List.of())).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test(timeout = 30000)
|
||||
public void dispose() {
|
||||
var lock1 = new SocketLock(tempDir.getRootPath().resolve("c"), tempDir.getRootPath().resolve("s"));
|
||||
var lock2 = new SocketLock(tempDir.getRootPath().resolve("c"), tempDir.getRootPath().resolve("s"));
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, tryActivate(lock1));
|
||||
assertEquals(SocketLock.ActivationStatus.ACTIVATED, tryActivate(lock2));
|
||||
lock1.dispose();
|
||||
assertEquals(SocketLock.ActivationStatus.NO_INSTANCE, tryActivate(lock2));
|
||||
lock2.dispose();
|
||||
@Test
|
||||
public void configDirectoryCannotBeShared() throws Exception {
|
||||
var configDir = Files.createDirectories(testDir.resolve("c"));
|
||||
var systemDir1 = Files.createDirectories(testDir.resolve("s1"));
|
||||
var systemDir2 = Files.createDirectories(testDir.resolve("s2"));
|
||||
var lock1 = createLock(configDir, systemDir1);
|
||||
var lock2 = createLock(configDir, systemDir2);
|
||||
assertNull(lock1.lockOrActivate(Path.of(""), List.of()));
|
||||
assertThatThrownBy(() -> lock2.lockOrActivate(Path.of(""), List.of())).isInstanceOf(CannotActivateException.class);
|
||||
}
|
||||
|
||||
@Test(timeout = 30000, expected = IllegalArgumentException.class)
|
||||
public void pathCollision() {
|
||||
var path = tempDir.getRootPath().resolve("d");
|
||||
new SocketLock(path, path);
|
||||
@Test
|
||||
public void deletingStalePortFile() throws Exception {
|
||||
var systemDir = Files.createDirectories(testDir.resolve("s"));
|
||||
Files.createFile(systemDir.resolve(SpecialConfigFiles.PORT_FILE));
|
||||
var lock = createLock(testDir.resolve("c"), systemDir);
|
||||
assertNull(lock.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
private static SocketLock.ActivationStatus createLockAndTryActivate(List<SocketLock> toClose, Path dir, String cfg, String sys) {
|
||||
var lock = new SocketLock(dir.resolve(cfg), dir.resolve(sys));
|
||||
toClose.add(lock);
|
||||
return tryActivate(lock);
|
||||
@Test
|
||||
public void deletingStaleLockFile() throws Exception {
|
||||
var configDir = Files.createDirectories(testDir.resolve("c"));
|
||||
Files.writeString(configDir.resolve(SpecialConfigFiles.LOCK_FILE), "---");
|
||||
var lock = createLock(configDir, testDir.resolve("s"));
|
||||
assertNull(lock.lockOrActivate(Path.of(""), List.of()));
|
||||
}
|
||||
|
||||
private static SocketLock.ActivationStatus tryActivate(SocketLock lock) {
|
||||
var statusAndResult = lock.lockAndTryActivate(List.of(), GlobalScope.INSTANCE);
|
||||
lock.getServer();
|
||||
return statusAndResult.getFirst();
|
||||
@Test
|
||||
public void preservingActiveLockFile() throws Exception {
|
||||
var configDir = Files.createDirectories(testDir.resolve("c"));
|
||||
Files.writeString(configDir.resolve(SpecialConfigFiles.LOCK_FILE), String.valueOf(ProcessHandle.current().pid()));
|
||||
var lock = createLock(configDir, testDir.resolve("s"));
|
||||
assertThatThrownBy(() -> lock.lockOrActivate(Path.of(""), List.of())).isInstanceOf(CannotActivateException.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,12 @@ bootstrap.error.message.invalid.ide.directory.trans.located.macos.directory.advi
|
||||
bootstrap.error.message.invalid.ide.directory.ensure.the.modified.property.0.is.correct=If you have modified the ''{0}'' property, please make sure it is correct,\notherwise, please re-install the IDE.
|
||||
bootstrap.error.message.invalid.ide.directory.problem.0.possible.reason.1.advice.2.location.3.exception.class.4.exception.message.5={0}\n{1}\n\n{2}\n\n-----\nLocation: {3}\n{4}: {5}
|
||||
|
||||
bootstrap.error.title.too.many.instances=Too Many Instances
|
||||
bootstrap.error.message.only.one.instance.of.0.can.be.run.at.a.time=Only one instance of {0} can be run at a time.
|
||||
bootstrap.error.cannot.activate.title=IDE Already Running
|
||||
bootstrap.error.cannot.activate.message=Cannot connect to already running IDE instance.\n{0}: {1}
|
||||
bootstrap.error.same.directories='config' and 'system' paths should point to different directories
|
||||
bootstrap.error.malformed.response=Malformed response: {0}
|
||||
bootstrap.error.still.running=Process {0} is still running
|
||||
bootstrap.error.request.failed=Process {0} failed to handle the command\n{1} : {2}\nSee {3} for more details
|
||||
|
||||
bootstrap.error.title.configuration.wizard.failed=Configuration Wizard Failed
|
||||
|
||||
|
||||
Reference in New Issue
Block a user