diff --git a/vscode-extensions/commons-vscode/src/index.ts b/vscode-extensions/commons-vscode/src/index.ts index daebf3a56..f2676f6f3 100644 --- a/vscode-extensions/commons-vscode/src/index.ts +++ b/vscode-extensions/commons-vscode/src/index.ts @@ -1,3 +1,4 @@ -import {activate, findJvmFile, ActivatorOptions} from './launch-util'; +import {activate, ActivatorOptions} from './launch-util'; +import {JVM} from './jvm-util'; -export {activate, findJvmFile, ActivatorOptions}; +export {activate, JVM, ActivatorOptions}; diff --git a/vscode-extensions/commons-vscode/src/jvm-util.ts b/vscode-extensions/commons-vscode/src/jvm-util.ts new file mode 100644 index 000000000..153f6240d --- /dev/null +++ b/vscode-extensions/commons-vscode/src/jvm-util.ts @@ -0,0 +1,186 @@ +import * as FS from 'fs'; +import * as Path from 'path'; +import * as ChildProcess from 'child_process'; + +'use strict'; + +export interface JVM { + /** + * 8 = Java 1.8.x, 9 = Java 9.x, etc + */ + getMajorVersion() : number + + /** + * Path to the Java executable + */ + getJavaExecutable() : string + + /** + * Detect whether this JVM is a JDK + */ + isJdk() : boolean + + /** + * Find tools.jar for this JVM. + * + * Note that if the JVM is a JRE; or a Java 9 or above JDK; + * then this will return null. + */ + getToolsJar() : string | null +} + +/** + * Find a JVM by looking in the JAVA_HOME and PATH environment variables. + * + * Optionally, a specific javaHome can be passed in. This shortcuts the + * search logic and uses that javaHome as is. + * + * The returned JVM may or may not be a JDK. Methods are provided to obtain corresponding + * toolsjar and to check whether the JVM is a JDK. + */ +export function findJvm(javaHome?: string) : Promise { + let javaExe = findJavaExe(javaHome); + if (javaExe) { + return determineJavaVersion(javaExe).then(version => new JavaExecutable(javaExe, version)); + } + return Promise.resolve(null); +} + +/** + * Find a 'java' exe by looking in the JAVA_HOME and PATH environment variables. + *

+ * Optionally, a specific javaHome can be passed in. This shortcuts the + * search logic and uses that javaHome as is, not looking anywhere else. + */ +function findJavaExe(javaHome?: string) : string | null { + if (!javaHome) { + javaHome = process.env["JAVA_HOME"]; + } + if (javaHome) { + //Resolve symlinks + javaHome = FS.realpathSync(javaHome); + } + let binName = correctBinname("java"); + if (javaHome) { + return Path.resolve(javaHome, "bin", binName); + } else { + for (var searchPath of process.env['PATH'].split(Path.delimiter)) { + let javaExe = Path.resolve(searchPath, binName); + if (FS.existsSync(javaExe)) { + //Resolve symlinks + return FS.realpathSync(javaExe); + } + } + } + return null; +} + +type Getter = () => T; + +function memoize(getter : Getter) : Getter { + let computed : boolean = false; + let value : T | null = null; + return () => { + if (!computed) { + value = getter(); + computed = true; + } + return value; + }; +} + +const TOOLS_JAR_PATHS : string[][] = [ + ["lib", "tools.jar"], + ["..", "lib", "tools.jar"] +]; + +class JavaExecutable implements JVM { + javaExe : string + version : number + toolsJar: () => string | null; + constructor(javaExe : string, version : number) { + this.javaExe = javaExe; + this.version = version; + this.toolsJar = memoize(() => this.findToolsJar()); + } + + getJavaHome() : string { + return Path.resolve(this.javaExe, "..", ".."); + } + + findToolsJar() : string | null { + if (this.version>=9) { + return null; + } + let javaHome = this.getJavaHome(); + for (var tjp of TOOLS_JAR_PATHS) { + let toolsJar = Path.resolve(javaHome, ...tjp); + if (FS.existsSync(toolsJar)) { + return toolsJar; + } + } + //Not found. + return null; + } + + getMajorVersion() { + return this.version; + } + getJavaExecutable(): string { + return this.javaExe; + } + isJdk(): boolean { + //Consider memoizing? + if (this.version<9) { + return this.getToolsJar()!=null; + } else { + return FS.existsSync(Path.resolve(this.getJavaHome(), "jmods", "jdk.management.jmod")); + } + } + getToolsJar(): string { + return this.toolsJar(); + } +} + +function determineJavaVersion(javaExecutablePath : string) : Promise { +//Examples of the 'java -version' command output: +// +// For Java 9: +/* +java version "9.0.4" +Java(TM) SE Runtime Environment (build 9.0.4+11) +Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode) +*/ +// For Java 8: +/* +java version "1.8.0_161" +Java(TM) SE Runtime Environment (build 1.8.0_161-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.161-b12, mixed mode) +*/ + return new Promise((resolve, reject) => { + ChildProcess.execFile(javaExecutablePath, ['-version'], {}, (error, stdout, stderr) => { + let versionStart = stderr.indexOf('"'); + if (versionStart>=0) { + versionStart = versionStart + 1; + let versionEnd = stderr.indexOf('"', versionStart); + if (versionEnd>=0) { + let versionString = stderr.substring(versionStart, versionEnd); + let pieces = versionString.split("."); + let major = parseInt(pieces[0]); + major = major==1 ? parseInt(pieces[1]) : major + return resolve(major); + } + } + //Unexpected output... + return resolve(0); + }); + }); + +} + +function correctBinname(binname: string) { + if (process.platform === 'win32') + return binname + '.exe'; + else + return binname; +} \ No newline at end of file diff --git a/vscode-extensions/commons-vscode/src/launch-util.ts b/vscode-extensions/commons-vscode/src/launch-util.ts index 9011067ab..fbb22b597 100644 --- a/vscode-extensions/commons-vscode/src/launch-util.ts +++ b/vscode-extensions/commons-vscode/src/launch-util.ts @@ -18,6 +18,7 @@ import {WorkspaceEdit, Position} from 'vscode-languageserver-types'; import {HighlightService, HighlightParams} from './highlight-service'; import { log } from 'util'; import { tmpdir } from 'os'; +import { JVM, findJvm } from './jvm-util'; let p2c = P2C.createConverter(); @@ -34,7 +35,7 @@ export interface ActivatorOptions { launcher: (context: VSCode.ExtensionContext) => string; jvmHeap?: string; workspaceOptions?: VSCode.WorkspaceConfiguration; - classpath?: (context: VSCode.ExtensionContext, javaVersion: number) => string[]; + classpath?: (context: VSCode.ExtensionContext, jvm: JVM) => string[]; } type JavaOptions = { @@ -72,22 +73,21 @@ export function activate(options: ActivatorOptions, context: VSCode.ExtensionCon } } - let javaExecutablePath = findJvmFile('bin', correctBinname('java')); - - if (javaExecutablePath == null) { - VSCode.window.showErrorMessage("Couldn't locate java in $JAVA_HOME or $PATH"); - return; - } - log("Found java exe: " + javaExecutablePath); - - - return javaVersion(javaExecutablePath).then(version => { - if (!version) { + return findJvm().then(jvm => { + if (!jvm) { + VSCode.window.showErrorMessage("Couldn't locate java in $JAVA_HOME or $PATH"); + return; + } + let javaExecutablePath = jvm.getJavaExecutable(); + log("Found java exe: " + javaExecutablePath); + + let version = jvm.getMajorVersion(); + if (version<8) { VSCode.window.showErrorMessage('Java-based Language Server requires Java 8 or higher (using ' + javaExecutablePath + ')'); return; } log("isJavaEightOrHigher => true"); - + function createServer(): Promise { return new Promise((resolve, reject) => { PortFinder.getPort((err, port) => { @@ -112,7 +112,7 @@ export function activate(options: ActivatorOptions, context: VSCode.ExtensionCon '-Dsts.log.file=' + logfile ]; if (options.classpath) { - const classpath = options.classpath(context, version); + const classpath = options.classpath(context, jvm); if (classpath) { args.push('-cp'); args.push(classpath.join(Path.delimiter)); @@ -143,7 +143,6 @@ export function activate(options: ActivatorOptions, context: VSCode.ExtensionCon }); }); } - return setupLanguageClient(context, createServer, options); }); } @@ -210,44 +209,6 @@ function setupLanguageClient(context: VSCode.ExtensionContext, createServer: Ser }); } -function javaVersion(javaExecutablePath: string): Promise { - return new Promise((resolve, reject) => { - ChildProcess.execFile(javaExecutablePath, ['-version'], {}, (error, stdout, stderr) => { - if (stderr.indexOf('1.8') >= 0) { - resolve(8); - } else if (stderr.indexOf('java version "9') >= 0) { - resolve(9); - } else { - resolve(0); - } - }); - }); -} - -export function findJvmFile(folderPath: string, file: string): string { - // First search each JAVA_HOME bin folder - if (process.env['JAVA_HOME']) { - let workspaces = process.env['JAVA_HOME'].split(Path.delimiter); - for (let i = 0; i < workspaces.length; i++) { - let filePath = Path.join(workspaces[i], folderPath, file); - if (FS.existsSync(filePath)) { - return filePath; - } - } - } - - // Then search PATH parts - if (process.env['PATH']) { - let pathparts = process.env['PATH'].split(Path.delimiter); - for (let i = 0; i < pathparts.length; i++) { - let filePath = Path.join(pathparts[i], file); - if (FS.existsSync(filePath)) { - return filePath; - } - } - } -} - function correctBinname(binname: string) { if (process.platform === 'win32') return binname + '.exe'; diff --git a/vscode-extensions/commons-vscode/test.txt b/vscode-extensions/commons-vscode/test.txt new file mode 100644 index 000000000..e69de29bb diff --git a/vscode-extensions/vscode-boot-java/.vscode/launch.json b/vscode-extensions/vscode-boot-java/.vscode/launch.json index 1c135881c..50d3b7d61 100644 --- a/vscode-extensions/vscode-boot-java/.vscode/launch.json +++ b/vscode-extensions/vscode-boot-java/.vscode/launch.json @@ -1,31 +1,17 @@ // A launch configuration that compiles the extension and then opens it inside a new window { - "version": "0.1.0", - "configurations": [ - { - "type": "extensionHost", - "request": "launch", - "name": "Launch Extension", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceRoot}" - ], - "sourceMaps": true, - "outFiles": [ - "${workspaceRoot}/out/**/*.js" - ], - "preLaunchTask": "npm" - }, - { - "name": "Launch Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], - "stopOnEntry": false, - "sourceMaps": true, - "outFiles": ["${workspaceRoot}/out/test"], - "preLaunchTask": "npm" - } - ] -} + "version": "0.1.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": true, + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/out/**/*.js" ], + "preLaunchTask": "npm: watch" + } + ] +} \ No newline at end of file diff --git a/vscode-extensions/vscode-boot-java/.vscode/tasks.json b/vscode-extensions/vscode-boot-java/.vscode/tasks.json index 1cbb9fd45..604e38f5a 100644 --- a/vscode-extensions/vscode-boot-java/.vscode/tasks.json +++ b/vscode-extensions/vscode-boot-java/.vscode/tasks.json @@ -1,30 +1,20 @@ -// Available variables which can be used inside of strings. -// ${workspaceRoot}: the root folder of the team -// ${file}: the current opened file -// ${fileBasename}: the current opened file's basename -// ${fileDirname}: the current opened file's dirname -// ${fileExtname}: the current opened file's extension -// ${cwd}: the current working directory of the spawned process - -// A task runner that calls a custom npm script that compiles the extension. +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format { - "version": "0.1.0", - - // we want to run npm - "command": "npm", - - // the command is a shell script - "isShellCommand": true, - - // show the output window only if unrecognized errors occur. - "showOutput": "silent", - - // we run the custom script "compile" as defined in package.json - "args": ["run", "compile"], //, "--loglevel", "silent"], - - // The tsc compiler is started in watching mode - "isWatching": true, - - // use the standard tsc in watch mode problem matcher to find compile problems in the output. - "problemMatcher": "$tsc-watch" + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] } \ No newline at end of file diff --git a/vscode-extensions/vscode-boot-java/lib/Main.ts b/vscode-extensions/vscode-boot-java/lib/Main.ts index 2b6e1225a..f8de40d35 100644 --- a/vscode-extensions/vscode-boot-java/lib/Main.ts +++ b/vscode-extensions/vscode-boot-java/lib/Main.ts @@ -1,6 +1,4 @@ 'use strict'; -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below import * as net from 'net'; @@ -14,28 +12,25 @@ import { workspace, TextDocument } from 'vscode'; import { Trace } from 'vscode-jsonrpc'; import * as commons from 'commons-vscode'; +import { connect } from 'tls'; export function activate(context: VSCode.ExtensionContext) { - let options: commons.ActivatorOptions = { DEBUG: false, CONNECT_TO_LS: false, extensionId: 'boot-java', launcher: (context: VSCode.ExtensionContext) => 'org.springframework.boot.loader.JarLauncher', - classpath: (context: VSCode.ExtensionContext, javaVersion: number) => { + classpath: (context: VSCode.ExtensionContext, jvm: commons.JVM) => { const classpath = [ Path.resolve(context.extensionPath, 'jars/language-server.jar') ]; - - if (javaVersion < 9) { - const toolsJar = commons.findJvmFile('lib', 'tools.jar'); - if (toolsJar) { - classpath.push(toolsJar); - } else { - VSCode.window.showWarningMessage('JAVA_HOME environment variable points either to JRE or JDK missing "lib/tools.jar" hence Boot Hints are unavailable'); - } + if (!jvm.isJdk()) { + VSCode.window.showWarningMessage('JAVA_HOME or PATH environment variable seems to point to a JRE. A JDK is required, hence Boot Hints are unavailable.'); + } + const toolsJar = jvm.getToolsJar(); + if (toolsJar) { + classpath.unshift(toolsJar); } - return classpath; }, clientOptions: { diff --git a/vscode-extensions/vscode-boot-java/scripts/preinstall.sh b/vscode-extensions/vscode-boot-java/scripts/preinstall.sh index 63f3a81ca..1b3d8ba09 100755 --- a/vscode-extensions/vscode-boot-java/scripts/preinstall.sh +++ b/vscode-extensions/vscode-boot-java/scripts/preinstall.sh @@ -8,9 +8,9 @@ workdir=`pwd` npm install ../commons-vscode/commons-vscode-*.tgz # Use maven to build fat jar of the language server -cd ../../headless-services/boot-java-language-server -./build.sh +#cd ../../headless-services/boot-java-language-server +#./build.sh -mkdir -p ${workdir}/jars -cp target/*.jar ${workdir}/jars/language-server.jar +# mkdir -p ${workdir}/jars +# cp target/*.jar ${workdir}/jars/language-server.jar