diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 399da1368..23c818481 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 # required by sonarqube - - name: Use Java 11 + - name: Use Java 17 uses: actions/setup-java@v1 with: - java-version: "11" + java-version: "17" architecture: x64 - name: Use Node 16.14.2 uses: actions/setup-node@v4 @@ -31,6 +31,14 @@ jobs: key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- + - name: Checkout microsoft/build-server-for-gradle + uses: actions/checkout@v2 + with: + repository: microsoft/build-server-for-gradle + path: extension/build-server-for-gradle + - name: Build Jars + run: ../gradlew buildJars + working-directory: extension/ - name: Lint uses: gradle/gradle-build-action@v2 with: @@ -61,7 +69,7 @@ jobs: fail-fast: false matrix: node-version: [16.14.2] - java-version: ["8", "11", "17", "21"] + java-version: ["17", "21"] os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v2 diff --git a/.vscode/launch.json b/.vscode/launch.json index 3c3f33f82..97080c446 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,19 +31,14 @@ }, { "type": "java", - "name": "Debug Server", - "request": "launch", - "mainClass": "com.github.badsyntax.gradle.GradleServer", - "projectName": "gradle-server", - "cwd": "${workspaceFolder}/gradle-server", - "presentation": { - "group": "debug", - "order": 2, - "hidden": true - } + "name": "Attach to Gradle Server", + "request": "attach", + "hostName": "localhost", + "port": "8089", + "projectName": "com.github.badsyntax.gradle" }, { - "name": "Debug Extension with Debug Server", + "name": "Debug Gradle Server & Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", @@ -54,12 +49,13 @@ "${workspaceFolder}/extension/dist/**/*.js" ], "preLaunchTask": "Gradle: Build", + "presentation": { + "group": "debug", + "order": 2 + }, "env": { - "VSCODE_DEBUG_SERVER": "true" + "GRADLE_SERVER_OPTS":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8089" }, - "presentation": { - "hidden": true - } }, { "name": "Debug Extension & Gradle Plugin", @@ -76,30 +72,10 @@ "env": { "VSCODE_DEBUG_PLUGIN": "true" }, - "presentation": { - "group": "debug", - "order": 2 - } - }, - { - "name": "Debug Extension & Build Server", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}/extension" - ], - "outFiles": [ - "${workspaceFolder}/extension/dist/**/*.js" - ], - "preLaunchTask": "Gradle: Build", "presentation": { "group": "debug", "order": 3 - }, - "env": { - "DEBUG_GRADLE_BUILD_SERVER":"true" - }, + } }, { "name": "Debug Language Server: Launch Extension", @@ -372,17 +348,4 @@ } }, ], - "compounds": [ - { - "name": "Debug Server & Extension", - "configurations": [ - "Debug Server", - "Debug Extension with Debug Server" - ], - "presentation": { - "group": "debug", - "order": 3 - } - } - ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ade961ce..ed6a0b136 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Start by opening an issue using one of the issue templates, or propose a change ### Build Gradle Server and Gradle Language Server. 1. Install [nvm](https://github.com/nvm-sh/nvm) -2. Install [Java version >= 8](https://adoptium.net/) +2. Install [Java version >= 17](https://adoptium.net/) 3. Change directory to the root of the project 4. Select Node version: `nvm use` 5. If using an Apple M1: @@ -40,34 +40,22 @@ The extension uses a Gradle plugin (`com.microsoft.gradle.GradlePlugin`) to get > Note: There is a known issue that when the Gradle project stores in a sub-folder of the root folder, the `Attach to Gradle Plugin` will fail to attach. See [#1237](https://github.com/microsoft/vscode-gradle/issues/1237). -## Debugging Gradle Build Server - -To debug the Extension with the [Gradle Build Server](https://github.com/microsoft/build-server-for-gradle), follow these steps: - -1. Open the `extension/build-server-for-gradle` directory, which you should have [imported previously](#build-gradle-project-importer) as a separate project. -2. In the `.vscode/launch.json` of the build-server-for-gradle project, ensure you have the following configuration to attach the debugger: - ```json - { - "type": "java", - "name": "Attach to Gradle Build Server", - "request": "attach", - "hostName": "localhost", - "port": "8989", - "projectName": "server" - } - ``` -3. In your main project (vscode-gradle), start the `Debug Extension & Build Server` launch configuration. -4. In the build-server-for-gradle project, start the `Attach to Gradle Build Server` launch configuration. +## Debugging Gradle Server + +1. Run vscode launch configuration `Debug Gradle Server & Extension`. +2. Run vscode launch configuration `Attach to Gradle Server` when you notice the `Gradle: Connecting...` message in the bottom status bar. + +> **Note:** If the "Java: Error" message appears in the bottom status bar and the following error is logged in the `.log` file: +> ```java +> java.lang.NullPointerException: Cannot invoke "ch.epfl.scala.bsp4j.WorkspaceBuildTargetsResult.getTargets()" +> ``` +> it indicates that the connection attempt to the Gradle Server was too slow. The [GradleBuildClient](/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/ImporterPlugin.java#L107) requires an active Gradle Server to successfully establish a connection. If you encounter this issue, please retry the connection promptly to avoid this error. ## Debugging Gradle Language Server (editing feature related) 1. Run vscode launch configuration `Debug Language Server: Launch Extension`. 2. Run vscode launch configuration `Debug Language Server: Launch Language Server`. -## Debugging Gradle Server (work with Gradle daemon) - -Run vscode launch configuration `Debug Server & Extension`. - ## Development Workflow Open the root of the project in VS Code. diff --git a/build.gradle b/build.gradle index d492b25ab..e976f182f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { project.ext.set('grpcVersion', '1.53.0') project.ext.set('protobufVersion', '3.12.0') project.ext.set('protocVersion', project.protobufVersion) -project.ext.set('toolingAPIVersion', '8.0.2') +project.ext.set('toolingAPIVersion', '8.8') allprojects { group = 'vscode-gradle' diff --git a/extension/.eslintignore b/extension/.eslintignore index 2e8035a6e..3e65bd4bc 100644 --- a/extension/.eslintignore +++ b/extension/.eslintignore @@ -9,3 +9,5 @@ src/proto/ build/ beta/ webpack.config.js +build-server-for-gradle/ +bin/ diff --git a/extension/.prettierignore b/extension/.prettierignore index 84d8e1e9c..290df17f4 100644 --- a/extension/.prettierignore +++ b/extension/.prettierignore @@ -9,3 +9,5 @@ lib/ src/proto/ build/ src/java-test-runner.api.ts +build-server-for-gradle/ +bin/ diff --git a/extension/build.gradle b/extension/build.gradle index 9b8dce80c..a2ada7e16 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -225,18 +225,28 @@ task buildBuildServer(type: CrossPlatformExec) { } } -task copyBuildServerJars(type: Copy) { +task copyBuildServerPluginJars(type: Copy) { dependsOn ':extension:buildBuildServer' from('./build-server-for-gradle/server/build/libs/') { - include '**/*.jar' - include '**/init.gradle' + include 'plugins/' } into 'server' mustRunAfter copyDocs } +// TODO: This task can be removed once the build server is published. +task copyBuildServerJarsToGradleServer(type: Copy) { + dependsOn ':extension:buildBuildServer' + from('./build-server-for-gradle/server/build/libs/') { + include 'runtime/' + } + from('./build-server-for-gradle/server/build/libs/server.jar') + into '../gradle-server/build/libs/' + mustRunAfter copyDocs +} + task buildJars() { - dependsOn copyJdtlsPluginJar, copyBuildServerJars + dependsOn copyJdtlsPluginJar, copyBuildServerPluginJars, copyBuildServerJarsToGradleServer } build.finalizedBy buildProd, buildTest diff --git a/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/GradleBuildServerProjectImporter.java b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/GradleBuildServerProjectImporter.java index 7ae70039b..4f19b409b 100644 --- a/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/GradleBuildServerProjectImporter.java +++ b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/GradleBuildServerProjectImporter.java @@ -61,6 +61,10 @@ public boolean applies(IProgressMonitor monitor) throws OperationCanceledExcepti return false; } + //TODO: support multi-root workspaces + if (getPreferences().getRootPaths().size() != 1) { + return false; + } if (!Utils.isBuildServerEnabled(getPreferences())) { return false; diff --git a/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/ImporterPlugin.java b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/ImporterPlugin.java index d780d9144..15f24fb50 100644 --- a/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/ImporterPlugin.java +++ b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/ImporterPlugin.java @@ -2,9 +2,6 @@ import java.io.File; import java.io.IOException; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -38,8 +35,6 @@ public class ImporterPlugin extends Plugin { */ private DigestStore digestStore; - private static String bundleDirectory; - private static String bundleVersion = ""; @Override @@ -52,7 +47,6 @@ public void start(BundleContext context) throws Exception { if (!bundleFile.isPresent()) { throw new IllegalStateException("Failed to get bundle location."); } - bundleDirectory = bundleFile.get().getParent(); } @Override @@ -102,31 +96,18 @@ public static BuildServerConnection getBuildServerConnection(IPath rootPath, boo return null; } - String javaExecutablePath = getJavaExecutablePath(); - String[] classpaths = getBuildServerClasspath(); - - String pluginPath = getBuildServerPluginPath(); - - List command = new ArrayList<>(); - command.add(javaExecutablePath); - if (Boolean.parseBoolean(System.getenv("DEBUG_GRADLE_BUILD_SERVER"))) { - command.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8989"); + if (instance.buildServers.size() > 0) { + throw new CoreException(new Status(IStatus.ERROR, PLUGIN_ID, + "Not support multiple workspaces.")); } - command.add("--add-opens=java.base/java.lang=ALL-UNNAMED"); - command.add("--add-opens=java.base/java.io=ALL-UNNAMED"); - command.add("--add-opens=java.base/java.util=ALL-UNNAMED"); - command.add("-Dplugin.dir=" + pluginPath); - command.add("-cp"); - command.add(String.join(getClasspathSeparator(), classpaths)); - command.add("com.microsoft.java.bs.core.Launcher"); - - ProcessBuilder build = new ProcessBuilder(command); + try { - Process process = build.start(); - BuildClient client = new GradleBuildClient(); + NamedPipeStream pipeStream = new NamedPipeStream(); + + GradleBuildClient client = new GradleBuildClient(); Launcher launcher = new Launcher.Builder() - .setOutput(process.getOutputStream()) - .setInput(process.getInputStream()) + .setOutput(pipeStream.getOutputStream()) + .setInput(pipeStream.getInputStream()) .setLocalService(client) .setExecutorService(Executors.newCachedThreadPool()) .setRemoteInterface(BuildServerConnection.class) @@ -142,37 +123,4 @@ public static BuildServerConnection getBuildServerConnection(IPath rootPath, boo "Failed to start build server.", e)); } } - - /** - * Get the Java executable used by JDT.LS, which will be higher than JDK 17. - */ - private static String getJavaExecutablePath() { - Optional command = ProcessHandle.current().info().command(); - if (command.isPresent()) { - return command.get(); - } - - throw new IllegalStateException("Failed to get Java executable path."); - } - - private static String[] getBuildServerClasspath() { - return new String[]{ - Paths.get(bundleDirectory, "server.jar").toString(), - Paths.get(bundleDirectory, "runtime").toString() + File.separatorChar + "*" - }; - } - - private static String getBuildServerPluginPath() { - return Paths.get(bundleDirectory, "plugins").toString(); - } - - private static String getClasspathSeparator() { - String os = System.getProperty("os.name").toLowerCase(); - - if (os.contains("win")) { - return ";"; - } - - return ":"; // Linux or Mac - } } diff --git a/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/NamedPipeStream.java b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/NamedPipeStream.java new file mode 100644 index 000000000..862de589e --- /dev/null +++ b/extension/jdtls.ext/com.microsoft.gradle.bs.importer/src/com/microsoft/gradle/bs/importer/NamedPipeStream.java @@ -0,0 +1,257 @@ +package com.microsoft.gradle.bs.importer; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; + +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.core.runtime.Platform; +import com.microsoft.gradle.bs.importer.model.Telemetry; + +/** + * A class to create a named pipe stream for the importer to communicate with the extension. + */ +public class NamedPipeStream { + + private StreamProvider provider; + + private final int MAX_ATTEMPTS = 5; + + interface StreamProvider { + InputStream getInputStream() throws IOException; + OutputStream getOutputStream() throws IOException; + } + + public StreamProvider getSelectedStream() { + if (provider == null) { + provider = createProvider(); + } + return provider; + } + + private StreamProvider createProvider() { + PipeStreamProvider pipeStreamProvider = new PipeStreamProvider(); + pipeStreamProvider.initializeNamedPipe(); + return pipeStreamProvider; + } + + public InputStream getInputStream() throws IOException { + return getSelectedStream().getInputStream(); + } + + public OutputStream getOutputStream() throws IOException { + return getSelectedStream().getOutputStream(); + } + protected final class PipeStreamProvider implements StreamProvider { + + private InputStream input; + private OutputStream output; + + @Override + public InputStream getInputStream() throws IOException { + return input; + } + + @Override + public OutputStream getOutputStream() throws IOException { + return output; + } + + private void initializeNamedPipe() { + String pathName = generateRandomPipeName(); + sendImporterPipeName(pathName); + File pipeFile = new File(pathName); + + int attempts = 0; + // Need to retry until the pipeName was sent and pipe is created by Extension side + while (attempts < MAX_ATTEMPTS) { + try { + attemptConnection(pipeFile); + break; + } catch (IOException e) { + sleep(e, attempts); + attempts++; + } + } + Telemetry telemetry = new Telemetry("importerConnectAttempts", attempts); + Utils.sendTelemetry(JavaLanguageServerPlugin.getProjectsManager().getConnection(), + telemetry); + if (attempts == MAX_ATTEMPTS) { + throw new RuntimeException("Failed to connect to the named pipe after " + MAX_ATTEMPTS + " attempts"); + } + } + + private static String generateRandomHex(int numBytes) { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[numBytes]; + random.nextBytes(bytes); + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + hexString.append(String.format("%02x", b)); + } + return hexString.toString(); + } + + private String generateRandomPipeName() { + if (System.getProperty("os.name").startsWith("Windows")) { + return Paths.get("\\\\.\\pipe\\", generateRandomHex(16) + "-sock").toString(); + } + String tmpDir = System.getenv("XDG_RUNTIME_DIR"); + if (tmpDir == null || tmpDir.isEmpty()) { + tmpDir = System.getProperty("java.io.tmpdir"); + } + int fixedLength = ".sock".length(); + int safeIpcPathLengths = 103; + int availableLength = safeIpcPathLengths - fixedLength - tmpDir.length(); + int randomLength = 32; + int bytesLength = Math.min(availableLength / 2, randomLength); + + if (bytesLength < 16) { + throw new IllegalArgumentException("Unable to generate a random pipe name with character length less than 16"); + } + return Paths.get(tmpDir, generateRandomHex(bytesLength) + ".sock").toString(); + } + + private void sendImporterPipeName(String pipeName) { + JavaLanguageServerPlugin.getInstance().getClientConnection() + .sendNotification("gradle.onWillImporterConnect", pipeName); + } + + private void attemptConnection(File pipeFile) throws IOException { + if (isWindows()) { + AsynchronousFileChannel channel = AsynchronousFileChannel.open(pipeFile.toPath(), + StandardOpenOption.READ, StandardOpenOption.WRITE); + input = new NamedPipeInputStream(channel); + output = new NamedPipeOutputStream(channel); + } else { + UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(pipeFile.toPath()); + SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX); + channel.connect(socketAddress); + input = new NamedPipeInputStream(channel); + output = new NamedPipeOutputStream(channel); + } + } + + private void sleep(IOException e, int attempts) { + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread interrupted while handling connection failure", ie); + } + } + + protected static boolean isWindows() { + return Platform.OS_WIN32.equals(Platform.getOS()); + } + } + + public class NamedPipeInputStream extends InputStream { + + private ReadableByteChannel unixChannel; + private AsynchronousFileChannel winChannel; + private ByteBuffer buffer = ByteBuffer.allocate(1024); + private int readyBytes = 0; + + public NamedPipeInputStream(ReadableByteChannel channel) { + this.unixChannel = channel; + } + + public NamedPipeInputStream(AsynchronousFileChannel channel) { + this.winChannel = channel; + } + + @Override + public int read() throws IOException { + if (buffer.position() < readyBytes) { + return buffer.get() & 0xFF; + } + try { + buffer.clear(); + if (winChannel != null) { + readyBytes = winChannel.read(buffer, 0).get(); + } else { + readyBytes = unixChannel.read(buffer); + } + if (readyBytes == -1) { + return -1; // EOF + } + buffer.flip(); + return buffer.get() & 0xFF; + } catch (InterruptedException | ExecutionException e) { + throw new IOException(e); + } + } + } + + public class NamedPipeOutputStream extends OutputStream { + + private WritableByteChannel unixChannel; + private AsynchronousFileChannel winChannel; + private ByteBuffer buffer = ByteBuffer.allocate(1); + + public NamedPipeOutputStream(WritableByteChannel channel) { + this.unixChannel = channel; + } + + public NamedPipeOutputStream(AsynchronousFileChannel channel) { + this.winChannel = channel; + } + + @Override + public void write(int b) throws IOException { + buffer.clear(); + buffer.put((byte) b); + buffer.position(0); + if (winChannel != null) { + Future result = winChannel.write(buffer, 0); + try { + result.get(); + } catch (Exception e) { + throw new IOException(e); + } + } else { + unixChannel.write(buffer); + } + } + + @Override + public void write(byte[] b) throws IOException { + final int BUFFER_SIZE = 1024; + int blocks = b.length / BUFFER_SIZE; + int writeBytes = 0; + for (int i = 0; i <= blocks; i++) { + int offset = i * BUFFER_SIZE; + int length = Math.min(b.length - writeBytes, BUFFER_SIZE); + if (length <= 0) { + break; + } + writeBytes += length; + ByteBuffer buffer = ByteBuffer.wrap(b, offset, length); + if (winChannel != null) { + Future result = winChannel.write(buffer, 0); + try { + result.get(); + } catch (Exception e) { + throw new IOException(e); + } + } else { + unixChannel.write(buffer); + } + } + } + } +} diff --git a/extension/src/Extension.ts b/extension/src/Extension.ts index 5dce78716..69c483b84 100644 --- a/extension/src/Extension.ts +++ b/extension/src/Extension.ts @@ -1,4 +1,5 @@ import * as vscode from "vscode"; +import { commands, window } from "vscode"; import { logger, LogVerbosity, Logger } from "./logger"; import { Api } from "./api"; import { GradleClient } from "./client"; @@ -31,13 +32,16 @@ import { GRADLE_COMPLETION, GRADLE_PROPERTIES_FILE_CHANGE, VSCODE_TRIGGER_COMPLETION, + OPT_RESTART, } from "./constant"; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; import { GradleBuildContentProvider } from "./client/GradleBuildContentProvider"; import { BuildServerController } from "./bs/BuildServerController"; import { GradleTestRunner } from "./bs/GradleTestRunner"; +import { BspProxy } from "./bs/BspProxy"; export class Extension { + private readonly bspProxy: BspProxy; private readonly client: GradleClient; private readonly server: GradleServer; private readonly pinnedTasksStore: PinnedTasksStore; @@ -78,12 +82,16 @@ export class Extension { const serverLogger = new Logger("gradle-server"); serverLogger.setLoggingChannel(loggingChannel); + const bspLogger = new Logger("bspProxy"); + bspLogger.setLoggingChannel(loggingChannel); + if (getConfigIsDebugEnabled()) { Logger.setLogVerbosity(LogVerbosity.DEBUG); } const statusBarItem = vscode.window.createStatusBarItem(); - this.server = new GradleServer({ host: "localhost" }, context, serverLogger); + this.bspProxy = new BspProxy(this.context, bspLogger); + this.server = new GradleServer({ host: "localhost" }, context, serverLogger, this.bspProxy); this.client = new GradleClient(this.server, statusBarItem, clientLogger); this.pinnedTasksStore = new PinnedTasksStore(context); this.recentTasksStore = new RecentTasksStore(); @@ -238,11 +246,13 @@ export class Extension { }); } const activated = !!(await this.rootProjectsStore.getProjectRoots()).length; + this.bspProxy.prepareToStart(); if (!this.server.isReady()) { await this.server.start(); } await vscode.commands.executeCommand("setContext", "gradle:activated", activated); await vscode.commands.executeCommand("setContext", "gradle:defaultView", true); + await this.bspProxy.start(); } private registerCommands(): void { @@ -301,7 +311,14 @@ export class Extension { this.gradleWrapperWatcher.onDidChange( instrumentOperation(GRADLE_PROPERTIES_FILE_CHANGE, async (_operationId: string, uri: vscode.Uri) => { logger.info("Gradle wrapper properties changed:", uri.fsPath); - await this.restartServer(); + const selection = await this.showRestartWindow(); + sendInfo("", { + kind: "wrapperPropertiesChangedReloadRequest", + data2: selection === OPT_RESTART ? "true" : "false", + }); + if (selection === OPT_RESTART) { + await this.restartServer(); + } if (isLanguageServerStarted) { void vscode.commands.executeCommand("gradle.distributionChanged"); } @@ -310,10 +327,14 @@ export class Extension { } private async restartServer(): Promise { - if (this.server.isReady()) { - await this.client.cancelBuilds(); - await this.server.restart(); - } + await this.client.cancelBuilds(); + await commands.executeCommand("workbench.action.restartExtensionHost"); + } + + private async showRestartWindow(): Promise { + const msg = "Please restart the extension to make the change take effect. Restart now?"; + const selection = await window.showWarningMessage(msg, OPT_RESTART); + return selection; } private refresh(): Thenable { @@ -328,7 +349,14 @@ export class Extension { event.affectsConfiguration("java.jdt.ls.java.home") || event.affectsConfiguration("java.import.gradle.java.home") ) { - await this.restartServer(); + const selection = await this.showRestartWindow(); + sendInfo("", { + kind: "javaHomeChangedReloadRequest", + data2: selection === OPT_RESTART ? "true" : "false", + }); + if (selection === OPT_RESTART) { + await this.restartServer(); + } } else if ( event.affectsConfiguration("gradle.javaDebug.cleanOutput") || event.affectsConfiguration("gradle.nestedProjects") diff --git a/extension/src/bs/BspProxy.ts b/extension/src/bs/BspProxy.ts new file mode 100644 index 000000000..4ab77080e --- /dev/null +++ b/extension/src/bs/BspProxy.ts @@ -0,0 +1,89 @@ +import { JdtlsImporterConnector } from "./JdtlsImporterConnector"; +import { BuildServerConnector } from "./BuildServerConnector"; +import * as vscode from "vscode"; +import * as rpc from "vscode-jsonrpc/node"; +import { Logger } from "../logger/index"; +import { sendInfo } from "vscode-extension-telemetry-wrapper"; + +/** + * Forwards JSON-RPC messages between the build server and the Java JDT LS importer. + * + * This layer is necessary because named pipes are not well supported by Java on Windows, + * but are well supported by Node.js. So Node.js is used to create two named pipe servers. + * + * During the named pipe connecting process, Both the build server and JDT LS importer act as clients connecting to BspProxy. + */ +export class BspProxy { + private buildServerConnector: BuildServerConnector; + private jdtlsImporterConnector: JdtlsImporterConnector; + + constructor(context: vscode.ExtensionContext, private readonly logger: Logger) { + this.buildServerConnector = new BuildServerConnector(); + this.jdtlsImporterConnector = new JdtlsImporterConnector(context); + } + /** + * This function needs to be called before we start Java Gradle Server. + */ + public prepareToStart(): void { + this.buildServerConnector.setupBuildServerPipeStream(); + } + + /** + * The order of the following start steps is important. + * + * We have to start listening after the message forwarding is setup, otherwise the Java importer + * will stop polling and start sending messages before the forwarding is setup and the messages will be lost. + */ + public async start(): Promise { + await this.jdtlsImporterConnector.waitForImporterPipePath(); + await this.jdtlsImporterConnector.setupImporterPipeStream(); + + this.setupMessageForwarding( + this.jdtlsImporterConnector.getImporterConnection(), + this.buildServerConnector.getServerConnection() + ); + this.jdtlsImporterConnector.startListening(); + } + + public getBuildServerPipeName(): string { + return this.buildServerConnector.getServerPipePath(); + } + + private setupMessageForwarding( + importerConnection: rpc.MessageConnection | null, + buildServerConnection: rpc.MessageConnection | null + ): void { + importerConnection?.onRequest((method, params) => { + if (params !== null) { + return buildServerConnection?.sendRequest(method, params); + } + return buildServerConnection?.sendRequest(method); + }); + + buildServerConnection?.onNotification((method, params) => { + if (params !== null) { + return importerConnection?.sendNotification(method, params); + } + importerConnection?.sendNotification(method); + }); + importerConnection?.onError(([error]) => { + this.logger.error(`Error on importerConnection: ${error.message}`); + sendInfo("", { + kind: "bspProxy-importerConnectionError", + message: error.message, + errorStack: error.stack ? error.stack.toString() : "", + }); + // TODO: Implement more specific error handling logic here + }); + + buildServerConnection?.onError(([error]) => { + this.logger.error(`Error on buildServerConnection: ${error.message}`); + sendInfo("", { + kind: "bspProxy-importerConnectionError", + message: error.message, + errorStack: error.stack ? error.stack.toString() : "", + }); + // TODO: Implement more specific error handling logic here + }); + } +} diff --git a/extension/src/bs/BuildServerConnector.ts b/extension/src/bs/BuildServerConnector.ts new file mode 100644 index 000000000..0c04a0f6b --- /dev/null +++ b/extension/src/bs/BuildServerConnector.ts @@ -0,0 +1,37 @@ +import * as net from "net"; +import * as rpc from "vscode-jsonrpc/node"; +import { generateRandomPipeName } from "../util/generateRandomPipeName"; + +/** + * Creates a named pipe file and sets up a pipe server + * for communication with the build server. + */ +export class BuildServerConnector { + private serverConnection: rpc.MessageConnection | null = null; + private serverPipeServer: net.Server; + private serverPipePath: string; + + /** + * Generates a random pipe name, creates a pipe server and + * waiting for the connection from the Java build server. + */ + public setupBuildServerPipeStream(): void { + this.serverPipePath = generateRandomPipeName(); + this.serverPipeServer = net.createServer((socket: net.Socket) => { + this.serverConnection = rpc.createMessageConnection( + new rpc.StreamMessageReader(socket), + new rpc.StreamMessageWriter(socket) + ); + this.serverConnection.listen(); + }); + this.serverPipeServer.listen(this.serverPipePath); + } + + public getServerConnection(): rpc.MessageConnection | null { + return this.serverConnection; + } + + public getServerPipePath(): string { + return this.serverPipePath; + } +} diff --git a/extension/src/bs/JdtlsImporterConnector.ts b/extension/src/bs/JdtlsImporterConnector.ts new file mode 100644 index 000000000..29b6f2fa4 --- /dev/null +++ b/extension/src/bs/JdtlsImporterConnector.ts @@ -0,0 +1,71 @@ +import * as net from "net"; +import * as rpc from "vscode-jsonrpc/node"; +import * as vscode from "vscode"; +import * as path from "path"; + +export const ON_WILL_IMPORTER_CONNECT = "gradle.onWillImporterConnect"; + +/** + * Receive the pipe name from Java jdt.ls importer, generate named pipe file and + * setting up a pipe server that will be used to communicate with the importer + */ +export class JdtlsImporterConnector { + private importerConnection: rpc.MessageConnection | null = null; + private importerPipeServer: net.Server; + private importerPipePath: string; + private readonly context: vscode.ExtensionContext; + private readonly _onImporterReady: vscode.EventEmitter = new vscode.EventEmitter(); + + constructor(context: vscode.ExtensionContext) { + this.context = context; + this.registerCommand(); + } + + /** + * Waits for the importer pipe path to be ready. + * It listens for the `_onImporterReady` event, and when the event is fired, + * it updates the `importerPipePath` with the resolved path and resolves the Promise. + * + * @returns Promise that resolves when the pipe path is ready + */ + public async waitForImporterPipePath(): Promise { + return new Promise((resolve) => { + this._onImporterReady.event((resolvedPath) => { + this.importerPipePath = resolvedPath; + resolve(); + }); + }); + } + + /** + * The `_onPipePathReady` event will be fired when the pipe path is received from Java jdt.ls importer + */ + private registerCommand(): void { + this.context.subscriptions.push( + vscode.commands.registerCommand(ON_WILL_IMPORTER_CONNECT, (pipeName: string) => { + this._onImporterReady.fire(path.resolve(pipeName)); + }) + ); + } + + public async setupImporterPipeStream(): Promise { + return new Promise((resolve) => { + this.importerPipeServer = net.createServer((socket: net.Socket) => { + this.importerConnection = rpc.createMessageConnection( + new rpc.StreamMessageReader(socket), + new rpc.StreamMessageWriter(socket) + ); + resolve(); + }); + this.importerPipeServer.listen(this.importerPipePath); + }); + } + + public startListening(): void { + this.importerConnection!.listen(); + } + + public getImporterConnection(): rpc.MessageConnection | null { + return this.importerConnection; + } +} diff --git a/extension/src/constant.ts b/extension/src/constant.ts index 2ff546bb7..1d01ed0d5 100644 --- a/extension/src/constant.ts +++ b/extension/src/constant.ts @@ -21,6 +21,8 @@ export const GRADLE_BUILD_FILE_NAMES = ["build.gradle", "settings.gradle", "buil export const NO_JAVA_EXECUTABLE = "No Java executable found, please consider to configure your 'java.jdt.ls.java.home' setting or set JAVA_HOME in your path or put a Java executable in your path."; +export const OPT_RESTART = "Restart"; + export enum CompletionKinds { DEPENDENCY_GROUP = "dependency_group", DEPENDENCY_ARTIFACT = "dependency_artifact", diff --git a/extension/src/languageServer/languageServer.ts b/extension/src/languageServer/languageServer.ts index 6f80ede5c..d40d2cbd7 100644 --- a/extension/src/languageServer/languageServer.ts +++ b/extension/src/languageServer/languageServer.ts @@ -15,7 +15,8 @@ import { getConfigJavaImportGradleUserHome, getConfigJavaImportGradleVersion, getConfigJavaImportGradleWrapperEnabled, - getSupportedJavaHome, + findValidJavaHome, + getJavaExecutablePathFromJavaHome, } from "../util/config"; import { prepareLanguageServerParams } from "./utils"; const CHANNEL_NAME = "Gradle for Java (Language Server)"; @@ -54,10 +55,10 @@ export async function startLanguageServer( serverOptions = awaitServerConnection.bind(null, port); } else { // keep consistent with gRPC server - const javaHome = await getSupportedJavaHome(); + const javaHome = await findValidJavaHome(); let javaCommand; if (javaHome) { - javaCommand = path.join(javaHome, "bin", "java"); + javaCommand = getJavaExecutablePathFromJavaHome(javaHome); } else { if (!checkEnvJavaExecutable()) { // we have already show error message in gRPC server for no java executable found, so here we will just reject and return diff --git a/extension/src/server/GradleServer.ts b/extension/src/server/GradleServer.ts index d0e5c8d4a..2b36d2054 100644 --- a/extension/src/server/GradleServer.ts +++ b/extension/src/server/GradleServer.ts @@ -3,10 +3,13 @@ import * as path from "path"; import * as cp from "child_process"; import * as getPort from "get-port"; import * as kill from "tree-kill"; +import { commands } from "vscode"; +import { sendInfo } from "vscode-extension-telemetry-wrapper"; import { getGradleServerCommand, getGradleServerEnv } from "./serverUtil"; -import { isDebuggingServer } from "../util"; import { Logger } from "../logger/index"; -import { NO_JAVA_EXECUTABLE } from "../constant"; +import { NO_JAVA_EXECUTABLE, OPT_RESTART } from "../constant"; +import { redHatJavaInstalled } from "../util/config"; +import { BspProxy } from "../bs/BspProxy"; const SERVER_LOGLEVEL_REGEX = /^\[([A-Z]+)\](.*)$/; const DOWNLOAD_PROGRESS_CHAR = "."; @@ -19,7 +22,7 @@ export class GradleServer { private readonly _onDidStart: vscode.EventEmitter = new vscode.EventEmitter(); private readonly _onDidStop: vscode.EventEmitter = new vscode.EventEmitter(); private ready = false; - private port: number | undefined; + private taskServerPort: number | undefined; private restarting = false; public readonly onDidStart: vscode.Event = this._onDidStart.event; @@ -29,51 +32,51 @@ export class GradleServer { constructor( private readonly opts: ServerOptions, private readonly context: vscode.ExtensionContext, - private readonly logger: Logger + private readonly logger: Logger, + private bspProxy: BspProxy ) {} public async start(): Promise { - if (isDebuggingServer()) { - this.port = 8887; - this.fireOnStart(); - } else { - this.port = await getPort(); - const cwd = this.context.asAbsolutePath("lib"); - const cmd = path.join(cwd, getGradleServerCommand()); - const env = await getGradleServerEnv(); - if (!env) { - await vscode.window.showErrorMessage(NO_JAVA_EXECUTABLE); - return; - } - const args = [String(this.port)]; - - this.logger.debug("Starting server"); - this.logger.debug(`Gradle Server cmd: ${cmd} ${args.join(" ")}`); - - this.process = cp.spawn(`"${cmd}"`, args, { - cwd, - env, - shell: true, - }); - this.process.stdout.on("data", this.logOutput); - this.process.stderr.on("data", this.logOutput); - this.process - .on("error", (err: Error) => this.logger.error(err.message)) - .on("exit", async (code) => { - this.logger.warn("Gradle server stopped"); - this._onDidStop.fire(null); - this.ready = false; - this.process?.removeAllListeners(); - if (this.restarting) { - this.restarting = false; - await this.start(); - } else if (code !== 0) { - await this.handleServerStartError(); - } - }); - - this.fireOnStart(); + this.taskServerPort = await getPort(); + const cwd = this.context.asAbsolutePath("lib"); + const cmd = path.join(cwd, getGradleServerCommand()); + const env = await getGradleServerEnv(); + const bundleDirectory = this.context.asAbsolutePath("server"); + if (!env) { + await vscode.window.showErrorMessage(NO_JAVA_EXECUTABLE); + return; + } + const startBuildServer = redHatJavaInstalled() ? "true" : "false"; + const args = [`--port=${this.taskServerPort}`, `--startBuildServer=${startBuildServer}`]; + if (startBuildServer === "true") { + const buildServerPipeName = this.bspProxy.getBuildServerPipeName(); + args.push(`--pipeName=${buildServerPipeName}`, `--bundleDir=${bundleDirectory}`); } + this.logger.debug(`Gradle Server cmd: ${cmd} ${args.join(" ")}`); + + this.process = cp.spawn(`"${cmd}"`, args, { + cwd, + env, + shell: true, + }); + this.process.stdout.on("data", this.logOutput); + this.process.stderr.on("data", this.logOutput); + this.process + .on("error", (err: Error) => this.logger.error(err.message)) + .on("exit", async (code) => { + this.logger.warn("Gradle server stopped"); + this._onDidStop.fire(null); + this.ready = false; + this.process?.removeAllListeners(); + if (this.restarting) { + this.restarting = false; + await this.start(); + } else if (code !== 0) { + await this.handleServerStartError(code); + } + }); + + this.fireOnStart(); } public isReady(): boolean { @@ -81,13 +84,16 @@ export class GradleServer { } public async showRestartMessage(): Promise { - const OPT_RESTART = "Restart Server"; - const input = await vscode.window.showErrorMessage( + const selection = await vscode.window.showErrorMessage( "No connection to gradle server. Try restarting the server.", OPT_RESTART ); - if (input === OPT_RESTART) { - await this.start(); + sendInfo("", { + kind: "serverProcessExitRestart", + data2: selection === OPT_RESTART ? "true" : "false", + }); + if (selection === OPT_RESTART) { + await commands.executeCommand("workbench.action.restartExtensionHost"); } } @@ -123,7 +129,11 @@ export class GradleServer { } } - private async handleServerStartError(): Promise { + private async handleServerStartError(code: number | null): Promise { + sendInfo("", { + kind: "serverProcessExit", + data2: code ? code.toString() : "", + }); await this.showRestartMessage(); } @@ -141,7 +151,7 @@ export class GradleServer { } public getPort(): number | undefined { - return this.port; + return this.taskServerPort; } public getOpts(): ServerOptions { diff --git a/extension/src/server/serverUtil.ts b/extension/src/server/serverUtil.ts index 4462b5727..6034627ab 100644 --- a/extension/src/server/serverUtil.ts +++ b/extension/src/server/serverUtil.ts @@ -1,4 +1,4 @@ -import { checkEnvJavaExecutable, getSupportedJavaHome } from "../util/config"; +import { checkEnvJavaExecutable, findValidJavaHome, getRedHatJavaEmbeddedJRE } from "../util/config"; export function getGradleServerCommand(): string { const platform = process.platform; @@ -16,7 +16,7 @@ export interface ProcessEnv { } export async function getGradleServerEnv(): Promise { - const javaHome = await getSupportedJavaHome(); + const javaHome = getRedHatJavaEmbeddedJRE() || (await findValidJavaHome()); const env = { ...process.env }; if (javaHome) { Object.assign(env, { diff --git a/extension/src/util/config.ts b/extension/src/util/config.ts index 68d5a31aa..f71cd6412 100644 --- a/extension/src/util/config.ts +++ b/extension/src/util/config.ts @@ -1,10 +1,13 @@ import { execSync } from "child_process"; -import { getRuntime } from "jdk-utils"; +import { JAVA_FILENAME } from "jdk-utils"; import * as vscode from "vscode"; import { GradleConfig } from "../proto/gradle_pb"; import { RootProject } from "../rootProject/RootProject"; - +import * as fse from "fs-extra"; +import * as path from "path"; +import { findDefaultRuntimeFromSettings, getMajorVersion, listJdks } from "./jdkUtils"; type AutoDetect = "on" | "off"; +const REQUIRED_JDK_VERSION = 17; export function getConfigIsAutoDetectionEnabled(rootProject: RootProject): boolean { return ( @@ -26,17 +29,58 @@ export function getConfigJavaImportGradleJavaHome(): string | null { return vscode.workspace.getConfiguration("java").get("import.gradle.java.home", null); } -export function getConfigGradleJavaHome(): string | null { - return getConfigJavaImportGradleJavaHome() || getJdtlsConfigJavaHome() || getConfigJavaHome(); +export function getJavaExecutablePathFromJavaHome(javaHome: string): string { + return path.join(javaHome, "bin", JAVA_FILENAME); +} + +export async function findValidJavaHome(): Promise { + const javaHomeGetters = [getJdtlsConfigJavaHome, getConfigJavaHome, getConfigJavaImportGradleJavaHome]; + let javaHome: string | undefined = undefined; + let javaVersion = 0; + + for (const getJavaHome of javaHomeGetters) { + javaHome = getJavaHome() || undefined; + if (javaHome) { + javaVersion = await getMajorVersion(javaHome); + if (javaVersion >= REQUIRED_JDK_VERSION) { + return javaHome; + } + } + } + + // Search valid JDKs from env.JAVA_HOME, env.PATH, SDKMAN, jEnv, jabba, common directories + const javaRuntimes = await listJdks(); + const validJdks = javaRuntimes.find((r) => r.version!.major >= REQUIRED_JDK_VERSION); + if (validJdks !== undefined) { + return validJdks.homedir; + } + + // Search java.configuration.runtimes if still not found + javaHome = await findDefaultRuntimeFromSettings(); + javaVersion = await getMajorVersion(javaHome); + if (javaVersion >= REQUIRED_JDK_VERSION) { + return javaHome; + } + + return undefined; +} + +export function redHatJavaInstalled(): boolean { + return !!vscode.extensions.getExtension("redhat.java"); } -export async function getSupportedJavaHome(): Promise { - const javaHome = getConfigGradleJavaHome() || process.env.JAVA_HOME; - if (javaHome) { - const runtime = await getRuntime(javaHome, { withVersion: true }); - if (runtime?.version) { - // check the JDK version of given java home is supported, otherwise return undefined - return runtime.version.major >= 8 && runtime.version.major <= 21 ? javaHome : undefined; +export function getRedHatJavaEmbeddedJRE(): string | undefined { + if (!redHatJavaInstalled()) { + return undefined; + } + + const jreHome = path.join(vscode.extensions.getExtension("redhat.java")!.extensionPath, "jre"); + if (fse.existsSync(jreHome) && fse.statSync(jreHome).isDirectory()) { + const candidates = fse.readdirSync(jreHome); + for (const candidate of candidates) { + if (fse.existsSync(path.join(jreHome, candidate, "bin", JAVA_FILENAME))) { + return path.join(jreHome, candidate); + } } } return undefined; diff --git a/extension/src/util/generateRandomPipeName.ts b/extension/src/util/generateRandomPipeName.ts new file mode 100644 index 000000000..f4d052504 --- /dev/null +++ b/extension/src/util/generateRandomPipeName.ts @@ -0,0 +1,32 @@ +// See: https://github.com/microsoft/vscode-languageserver-node/blob/6d0454dca7fba8529ba3fc6d930642f134291d3d/jsonrpc/src/node/main.ts#L176 +import { randomBytes } from "crypto"; +import * as os from "os"; +import * as path from "path"; +import * as fs from "fs"; + +const XDG_RUNTIME_DIR = process.env["XDG_RUNTIME_DIR"]; +const safeIpcPathLengths: Map = new Map([ + ["linux", 107], + ["darwin", 103], +]); + +// TODO: remove this function after upgrading vscode-languageclient +export function generateRandomPipeName(): string { + if (process.platform === "win32") { + return `\\\\.\\pipe\\${randomBytes(16).toString("hex")}-sock`; + } + + let randomLength = 32; + const fixedLength = ".sock".length; + const tmpDir: string = fs.realpathSync(XDG_RUNTIME_DIR ?? os.tmpdir()); + const limit = safeIpcPathLengths.get(process.platform); + if (limit !== undefined) { + randomLength = Math.min(limit - tmpDir.length - fixedLength, randomLength); + } + if (randomLength < 16) { + throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`); + } + + const randomSuffix = randomBytes(Math.floor(randomLength / 2)).toString("hex"); + return path.join(tmpDir, `${randomSuffix}.sock`); +} diff --git a/extension/src/util/index.ts b/extension/src/util/index.ts index f95b5c2e3..27d9ba0ef 100644 --- a/extension/src/util/index.ts +++ b/extension/src/util/index.ts @@ -6,8 +6,6 @@ import { RootProject } from "../rootProject"; export const isTest = (): boolean => process.env.VSCODE_TEST?.toLowerCase() === "true"; -export const isDebuggingServer = (): boolean => process.env.VSCODE_DEBUG_SERVER?.toLowerCase() === "true"; - // some run application tasks require a lot of time to start. So we should set a loose timeout. const maximumTimeout = 60000; // ms const tcpTimeout = 300; // ms diff --git a/extension/src/util/jdkUtils.ts b/extension/src/util/jdkUtils.ts new file mode 100644 index 000000000..fed3af316 --- /dev/null +++ b/extension/src/util/jdkUtils.ts @@ -0,0 +1,52 @@ +// See: https://github.com/redhat-developer/vscode-java/blob/2015139c5773c0107f75d2289e3656f45cb38c98/src/jdkUtils.ts +import { getRuntime, findRuntimes, IJavaRuntime } from "jdk-utils"; +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; + +let cachedJdks: IJavaRuntime[]; + +export async function getMajorVersion(javaHome: string | undefined): Promise { + if (!javaHome) { + return 0; + } + const runtime = await getRuntime(javaHome, { withVersion: true }); + return runtime?.version?.major || 0; +} + +export async function findDefaultRuntimeFromSettings(): Promise { + const runtimes = vscode.workspace.getConfiguration().get("java.configuration.runtimes"); + if (Array.isArray(runtimes) && runtimes.length) { + let candidate: string | undefined; + for (const runtime of runtimes) { + if (!runtime || typeof runtime !== "object" || !runtime.path) { + continue; + } + const jr = await getRuntime(runtime.path); + if (jr) { + candidate = jr.homedir; + } + if (runtime.default) { + break; + } + } + return candidate; + } + return undefined; +} + +export async function listJdks(force?: boolean): Promise { + if (force || !cachedJdks) { + cachedJdks = await findRuntimes({ checkJavac: true, withVersion: true, withTags: true }).then((jdks) => + jdks.filter((jdk) => { + // Validate if it's a real Java Home + return ( + fs.existsSync(path.join(jdk.homedir, "lib", "rt.jar")) || + fs.existsSync(path.join(jdk.homedir, "jre", "lib", "rt.jar")) || // Java 8 + fs.existsSync(path.join(jdk.homedir, "lib", "jrt-fs.jar")) // Java 9+ + ); + }) + ); + } + return cachedJdks; +} diff --git a/gradle-server/build.gradle b/gradle-server/build.gradle index ce5aedabd..62720a014 100644 --- a/gradle-server/build.gradle +++ b/gradle-server/build.gradle @@ -6,13 +6,15 @@ plugins { description = 'vscode-gradle :: gradle-server' java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } dependencies { implementation project(":gradle-plugin-api") - implementation "org.gradle:gradle-tooling-api:${toolingAPIVersion}" + implementation files('build/libs/server.jar') + implementation fileTree(dir: 'build/libs/runtime', include: ['*.jar'], exclude: ['gradle-tooling-api-*.jar', 'slf4j-api-*.jar']) + implementation("org.gradle:gradle-tooling-api:${toolingAPIVersion}") implementation 'javax.annotation:javax.annotation-api:1.3.2' implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" @@ -124,6 +126,7 @@ task serverStartScripts(type: CreateStartScripts) { task copyRuntimeLibs(type: Copy) { into "../extension/lib" from configurations.runtimeClasspath + duplicatesStrategy = 'exclude' } project.tasks.named("processResources") { @@ -131,5 +134,15 @@ project.tasks.named("processResources") { duplicatesStrategy = 'include' } +test { + jvmArgs '--add-opens=java.base/java.lang=ALL-UNNAMED', + '--add-opens=java.base/java.util=ALL-UNNAMED', + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED', + '--add-opens=java.base/java.nio.file=ALL-UNNAMED', + '--add-opens=java.base/java.io=ALL-UNNAMED', + '--add-opens=java.base/sun.nio.fs=ALL-UNNAMED' +} + + compileJava.dependsOn 'generateProto', 'spotlessCheck' assemble.dependsOn serverStartScripts diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/BuildServerThread.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/BuildServerThread.java new file mode 100644 index 000000000..88c072958 --- /dev/null +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/BuildServerThread.java @@ -0,0 +1,26 @@ +package com.github.badsyntax.gradle; + +import com.microsoft.java.bs.core.Launcher; +import java.nio.file.Paths; +public class BuildServerThread implements Runnable { + + private String bundleDirectory; + + private final String pipeName; + + public BuildServerThread(String pipeName, String bundleDirectory) { + this.pipeName = pipeName; + this.bundleDirectory = bundleDirectory; + } + + @Override + public void run() { + System.setProperty("plugin.dir", getBuildServerPluginPath()); + String[] args = {"--pipe=" + this.pipeName}; + Launcher.main(args); + } + + private String getBuildServerPluginPath() { + return Paths.get(bundleDirectory, "plugins").toString(); + } +} diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleServer.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleServer.java index 9b897e7dc..bdf611ecd 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleServer.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleServer.java @@ -1,8 +1,10 @@ package com.github.badsyntax.gradle; +import com.github.badsyntax.gradle.utils.Utils; import io.grpc.Server; import io.grpc.ServerBuilder; import java.io.IOException; +import java.util.Map; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,7 +13,7 @@ public class GradleServer { private static final Logger logger = LoggerFactory.getLogger(GradleServer.class.getName()); private final int port; - private final Server server; + private final Server taskServer; public GradleServer(int port) { this(ServerBuilder.forPort(port), port); @@ -19,13 +21,13 @@ public GradleServer(int port) { public GradleServer(ServerBuilder serverBuilder, int port) { this.port = port; - server = serverBuilder.addService(new GradleService()).build(); + taskServer = serverBuilder.addService(new TaskService()).build(); } @SuppressWarnings("java:S106") public void start() throws IOException { - server.start(); - logger.info("Server started, listening on {}", port); + taskServer.start(); + logger.info("Gradle Server started, listening on {}", port); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { @@ -42,24 +44,47 @@ public void run() { } public void stop() throws InterruptedException { - if (server != null) { - server.shutdown().awaitTermination(30, TimeUnit.SECONDS); + if (taskServer != null) { + taskServer.shutdown().awaitTermination(30, TimeUnit.SECONDS); } } private void blockUntilShutdown() throws InterruptedException { - if (server != null) { - server.awaitTermination(); + if (taskServer != null) { + taskServer.awaitTermination(); } } public static void main(String[] args) throws Exception { - int port = 8887; - if (args.length > 0) { - port = Integer.parseInt(args[0]); + Map params = Utils.parseArgs(args); + + int taskServerPort = Integer.parseInt(Utils.validateRequiredParam(params, "port")); + startTaskServerThread(taskServerPort); + + boolean startBuildServer = Boolean.parseBoolean(Utils.validateRequiredParam(params, "startBuildServer")); + if (startBuildServer) { + String buildServerPipeName = Utils.validateRequiredParam(params, "pipeName"); + String bundleDirectory = Utils.validateRequiredParam(params, "bundleDir"); + startBuildServerThread(buildServerPipeName, bundleDirectory); } + } + + private static void startTaskServerThread(int port) { GradleServer server = new GradleServer(port); - server.start(); - server.blockUntilShutdown(); + Thread serverThread = new Thread(() -> { + try { + server.start(); + server.blockUntilShutdown(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + }); + serverThread.start(); + } + + private static void startBuildServerThread(String pipeName, String directory) { + BuildServerThread buildServerConnectionThread = new BuildServerThread(pipeName, directory); + Thread buildServerThread = new Thread(buildServerConnectionThread); + buildServerThread.start(); } } diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleService.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/TaskService.java similarity index 97% rename from gradle-server/src/main/java/com/github/badsyntax/gradle/GradleService.java rename to gradle-server/src/main/java/com/github/badsyntax/gradle/TaskService.java index 0cb85b29e..135be3e14 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/GradleService.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/TaskService.java @@ -10,7 +10,7 @@ import com.github.badsyntax.gradle.handlers.StopDaemonsHandler; import io.grpc.stub.StreamObserver; -public class GradleService extends GradleGrpc.GradleImplBase { +public class TaskService extends GradleGrpc.GradleImplBase { @Override public void getBuild(GetBuildRequest req, StreamObserver responseObserver) { diff --git a/gradle-server/src/main/java/com/github/badsyntax/gradle/utils/Utils.java b/gradle-server/src/main/java/com/github/badsyntax/gradle/utils/Utils.java index e213544c5..827f123f6 100644 --- a/gradle-server/src/main/java/com/github/badsyntax/gradle/utils/Utils.java +++ b/gradle-server/src/main/java/com/github/badsyntax/gradle/utils/Utils.java @@ -5,7 +5,9 @@ import io.github.g00fy2.versioncompare.Version; import java.io.File; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; public class Utils { public static boolean isValidFile(File file) { @@ -74,4 +76,26 @@ private static String toPackageName(String name) { } return result.toString(); } + public static Map parseArgs(String[] args) { + Map paramMap = new HashMap<>(); + for (String arg : args) { + if (arg.startsWith("--")) { + int index = arg.indexOf('='); + if (index != -1) { + String key = arg.substring(2, index); + String value = arg.substring(index + 1); + paramMap.put(key, value); + } + } + } + return paramMap; + } + + public static String validateRequiredParam(Map params, String key) throws IllegalArgumentException { + String value = params.get(key); + if (value == null || value.isEmpty()) { + throw new IllegalArgumentException(key + " is required and can not be empty"); + } + return value; + } }