diff --git a/platform/core-impl/src/com/intellij/idea/AppExitCodes.java b/platform/core-impl/src/com/intellij/idea/AppExitCodes.java index 297ab59a868d..c2ed00a5fc54 100644 --- a/platform/core-impl/src/com/intellij/idea/AppExitCodes.java +++ b/platform/core-impl/src/com/intellij/idea/AppExitCodes.java @@ -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; diff --git a/platform/platform-api/src/com/intellij/ide/SpecialConfigFiles.java b/platform/platform-api/src/com/intellij/ide/SpecialConfigFiles.java index 98b8bc54a4a7..4a4355ee31d6 100644 --- a/platform/platform-api/src/com/intellij/ide/SpecialConfigFiles.java +++ b/platform/platform-api/src/com/intellij/ide/SpecialConfigFiles.java @@ -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"; } diff --git a/platform/platform-impl/src/com/intellij/idea/DirectoryLock.java b/platform/platform-impl/src/com/intellij/idea/DirectoryLock.java index 09745fe165e0..943e1fdcfa9a 100644 --- a/platform/platform-impl/src/com/intellij/idea/DirectoryLock.java +++ b/platform/platform-impl/src/com/intellij/idea/DirectoryLock.java @@ -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) -> Deferred> = AtomicReference { - CompletableDeferred(CliResult(AppExitCodes.ACTIVATE_NOT_INITIALIZED, IdeBundle.message("activation.not.initialized"))) - } - - private val lockedFiles = ArrayList(4) - - @Volatile - var serverFuture: Deferred? = 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) -> Deferred) { - 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, CliResult> myProcessor; + + private volatile @Nullable ServerSocketChannel myServerChannel = null; + + DirectoryLock(@NotNull Path configPath, @NotNull Path systemPath, @NotNull Function, 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, mainScope: CoroutineScope): Pair { - log("enter: lock(config=%s system=%s)", configPath, systemPath) - lockPortFiles() - val portToPath = HashMap>() - 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(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 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, - args: List, - systemPath: Path): Pair { - 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 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(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>) { - 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 { - val result = ArrayList() - 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): 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) { - 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, - private val commandProcessor: () -> (List) -> Deferred, - 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() - 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("") - val pid = ph.pid() + private void handleConnection(SocketChannel socketChannel) { + try (socketChannel) { + var request = readLines(socketChannel); - "PID: $pid, Command: $command" - } catch (e: Throwable) { - "" + 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 ?: "" - - val stackTrace = try { - e.stackTraceToString() - } catch (e: Throwable) { - "" + catch (IOException e) { + LOG.warn(e); } - - return "Response from process: $processInfo, exception: $message\n$stackTrace" } + + // + private static void sendLines(SocketChannel socketChannel, List 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 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(); + while (buffer.hasRemaining()) { + length = buffer.getShort(); + var bytes = new byte[length]; + buffer.get(bytes); + lines.add(new String(bytes, StandardCharsets.UTF_8)); + } + return lines; + } + // } diff --git a/platform/platform-impl/src/com/intellij/idea/StartupUtil.kt b/platform/platform-impl/src/com/intellij/idea/StartupUtil.kt index c8e3b11fed6d..de13192c8af6 100644 --- a/platform/platform-impl/src/com/intellij/idea/StartupUtil.kt +++ b/platform/platform-impl/src/com/intellij/idea/StartupUtil.kt @@ -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) -> Deferred> = + 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? = null @@ -113,21 +114,16 @@ fun CoroutineScope.startApplication(args: List, // 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, 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) -> Deferred) { - 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 = socketLock?.serverFuture ?: CompletableDeferred(value = null) +fun addExternalInstanceListener(processor: (List) -> Deferred) = + commandProcessor.set(processor) private fun runPreAppClass(log: Logger, args: List) { val classBeforeAppProperty = System.getProperty(IDEA_CLASS_BEFORE_APPLICATION_PROPERTY) ?: return @@ -642,18 +628,17 @@ fun checkHiDPISettings() { } } -private fun CoroutineScope.checkSystemDirs(lockSystemDirJob: Job, pathDeferred: Deferred>): 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>, - args: List, - 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): 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 { 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) {} @@ -968,3 +927,13 @@ class Java11ShimImpl : Java11Shim() { override fun copyOfCollection(collection: Collection): List = java.util.List.copyOf(collection) } + +// +@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 +} +// diff --git a/platform/platform-tests/testSrc/com/intellij/idea/DirectoryLockTest.java b/platform/platform-tests/testSrc/com/intellij/idea/DirectoryLockTest.java index 1f38778a9438..f733d877967f 100644 --- a/platform/platform-tests/testSrc/com/intellij/idea/DirectoryLockTest.java +++ b/platform/platform-tests/testSrc/com/intellij/idea/DirectoryLockTest.java @@ -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 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(); - 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 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); } } diff --git a/platform/service-container/resources/messages/BootstrapBundle.properties b/platform/service-container/resources/messages/BootstrapBundle.properties index 1632670835f4..3282f3caf483 100644 --- a/platform/service-container/resources/messages/BootstrapBundle.properties +++ b/platform/service-container/resources/messages/BootstrapBundle.properties @@ -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