const cp = require('child_process'); const fs = require('fs'); const path = require('path'); const url = require('url'); const remote = require('remote-file-size'); const PortFinder = require('portfinder'); const net = require('net'); const rpc = require('vscode-jsonrpc'); const {AutoLanguageClient, DownloadFile} = require('atom-languageclient'); const { Disposable } = require('atom'); import { StsAdapter } from './sts-adapter'; export class JavaProcessLanguageClient extends AutoLanguageClient { DEBUG = false; constructor(serverDownloadUrl, serverHome, serverLauncherJar) { super(); this.serverHome = serverHome; this.serverDownloadUrl = serverDownloadUrl; this.serverLauncherJar = serverLauncherJar; } getInitializeParams(projectPath, process) { const initParams = super.getInitializeParams(projectPath, process); initParams.capabilities = { workspace: { executeCommand: { } } }; return initParams; } startServerProcess () { // //TODO: Remove when debugging is over atom.config.set('core.debugLSP', true); let childProcess; if (this.DEBUG) { return this.connectToLS(); } return new Promise((resolve, reject) => { let basePort = Math.floor(Math.random() * 10000) + 40000; PortFinder.getPort({port: basePort}, (err, port) => { this.server = net.createServer(socket => { this.socket = socket; resolve(childProcess); }); this.server.listen(port, 'localhost', () => { this.launchProcess(port).then(p => childProcess = p); }); }); }); } connectToLS() { return new Promise(resolve => { this.socket = net.connect({ port: 5007 }); resolve({ pid: -1, kill: function() { console.log('fake shutdown'); } }) }); } // Start adapters that are not shared between servers startExclusiveAdapters(server) { super.startExclusiveAdapters(server); const stsAdapter = this.createStsAdapter() || new StsAdapter(); server.connection._onRequest({method: 'sts/moveCursor'}, params => stsAdapter.onMoveCursor(params)); server.connection._onNotification({method: 'sts/progress'}, params => stsAdapter.onProgress(params)); server.connection._onNotification({method: 'sts/highlight'}, params => stsAdapter.onHighlight(params)); } launchProcess(port) { const command = this.findJavaFile('bin', this.correctBinname('java')); return this.javaVesrion(command).then(version => { if (version) { return this.launchVmArgs(version).then(args => { args.push(`-Dserver.port=${port}`); return this.getOrInstallLauncher().then(launcher => this.doLaunchProcess(command, launcher, port, args)); }); } else { this.logger.error('Java executable is not Java 8 or higher'); const notification = atom.notifications.addError('Cannot start Language Server', { dismissable: true, detail: 'No compatible Java Runtime Environment found', description: 'The Java Runtime Environment is either below version "1.8" or is missing from the system', buttons: [{ text: 'OK', onDidClick: () => { notification.dismiss() }, }], }); } }); } launchVmArgs(version) { return Promise.resolve([]); } doLaunchProcess(javaExecutable, launcher, port, args=[]) { let vmArgs = args.concat([ // Atom doesn't have lazy completion proposals support - completionItem/resolve message. Disable lazy completions '-Dlsp.lazy.completions.disable=true', '-Dlsp.completions.indentation.enable=true', '-Dlsp.yaml.completions.errors.disable=true', ]); if (launcher.endsWith('.jar')) { vmArgs.push('-jar'); } vmArgs.push(launcher); this.logger.debug(`starting "${javaExecutable} ${vmArgs.join('\n')}"`); return cp.spawn(javaExecutable, vmArgs, { cwd: this.serverHome }) } getOrInstallLauncher() { const fullLauncherJar = path.join(this.serverHome, this.serverLauncherJar); return this.fileExists(fullLauncherJar).then(doesExist => doesExist ? fullLauncherJar : this.installServer().then(() => fullLauncherJar) ); } installServer () { const localFileName = path.join(this.serverHome, this.serverLauncherJar); this.logger.log(`Downloading ${this.serverDownloadUrl} to ${localFileName}`); return this.fileExists(this.serverHome) .then(doesExist => { if (!doesExist) fs.mkdir(this.serverHome) }) .then(() => this.remoteFileSize(this.serverDownloadUrl)) .then((size) => DownloadFile(this.serverDownloadUrl, localFileName, (bytesDone, percent) => this.handleDownlaodPercentChange(bytesDone, size, percent), size)) .then(() => this.fileExists(path.join(this.serverHome, this.serverLauncherJar))) .then(doesExist => { if (!doesExist) throw Error(`Failed to install the ${this.getServerName()} language server`) }) .then(() => this.handleServerInstalled()) .then(() => Promise.resolve(true)); } handleDownlaodPercentChange(bytesDone, size, percent) { } handleServerInstalled() { } preInitialization(connection) { connection.onCustom('language/status', (e) => this.updateStatusBar(`${e.type.replace(/^Started$/, '')} ${e.message}`)); } remoteFileSize(url) { return new Promise((resolve, reject) => { remote(url, (e,s) => { if (e) { reject(e); } else { resolve(s); } }); }); } fileExists (path) { return new Promise((resolve, reject) => { fs.access(path, fs.R_OK, error => { resolve(!error || error.code !== 'ENOENT'); }) }) } findJavaFile(folders, file) { // First search each JAVA_HOME 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], folders, 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; } } } // Else return the binary name directly (this will likely always fail downstream) return null; } correctBinname(binname) { if (process.platform === 'win32') return binname + '.exe'; else return binname; } javaVesrion(javaExecutablePath) { return new Promise((resolve, reject) => { cp.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); } }); }); } // Late wire-up of listeners after initialize method has been sent postInitialization(server) { server.disposable.add(new Disposable(() => { if (this.server) { this.server.close() } })); } createStsAdapter() { } }