Skip to content

Commit

Permalink
Run an executable version of LemMinX
Browse files Browse the repository at this point in the history
Download a binary lemminx, check its integrity, then run it.

Includes:
 * Setting to specify a binary, `xml.server.binary.path`
 * Setting to specify args for the binary, `xml.server.binary.args`

Defaults to java server in cases such as:
 * The binary can't be downloaded
 * The file containing the expected hash of the binary is missing
 * The hash of the binary doesn't match the expected hash
 * Binary specified in setting can't be located and the above three fail

Signed-off-by: David Thompson <[email protected]>
  • Loading branch information
fbricon authored and datho7561 committed Oct 21, 2020
1 parent af429c2 commit d8c8903
Show file tree
Hide file tree
Showing 8 changed files with 489 additions and 259 deletions.
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*.ts]
[*.js]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = false
5 changes: 5 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.vscode
node_modules
src/
tsconfig.json
webpack.config.js
22 changes: 22 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,28 @@
"markdownDescription": "Set a custom folder path for cached XML Schemas. An absolute path is expected, although the `~` prefix (for the user home directory) is supported. Default is `~/.lemminx`. Please refer to the [cache documentation](command:xml.open.docs?%5B%7B%22page%22%3A%22Preferences%22%2C%22section%22%3A%22server-cache-path%22%7D%5D) for more information.",
"scope": "window"
},
"xml.server.preferBinary": {
"type": "boolean",
"default": false,
"description": "By default, vscode-xml tries to run the Java version of the XML Language Server. If no Java is detected, vscode-xml runs the binary XML language server. When this setting is enabled, the binary will also be used even if Java is installed. If there are additions to the XML Language Server provided by other extensions, Java will be used (if available) even if this settings is enabled.",
"scope": "window"
},
"xml.server.silenceExtensionWarning": {
"type": "boolean",
"default": false,
"markdownDescription": "The XML Language server allows other VSCode extensions to extend its functionality. It requires Java-specific features in order to do this. If extensions to the XML language server are detected, but a binary XML language server is run because Java is missing, a warning will appear. This setting can be used to disable this warning.",
"scope": "window"
},
"xml.server.binary.path": {
"type": "string",
"description": "The path to the server binary to run. Will be ignored if `xml.server.binary.enabled` is not set. A binary will be downloaded if this is not set.",
"scope": "machine"
},
"xml.server.binary.args": {
"type": "string",
"markdownDescription": "Command line arguments to supply to the server binary when the server binary is being used. Takes into effect after relaunching VSCode. Please refer to [this website for the available options](https://www.graalvm.org/reference-manual/native-image/HostedvsRuntimeOptions/). For example, you can increase the maximum memory that the server can use to 1 GB by adding `-Xmx1g`",
"scope": "machine"
},
"xml.trace.server": {
"type": "string",
"enum": [
Expand Down
150 changes: 150 additions & 0 deletions src/binaryServerStarter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { createHash, Hash } from 'crypto';
import * as fs from 'fs';
import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import { ExtensionContext, window, WorkspaceConfiguration } from "vscode";
import { Executable } from "vscode-languageclient";
import { getXMLConfiguration } from "./settings";
const glob = require('glob');

/**
* Returns the executable to launch LemMinX (the XML Language Server) as a binary
*
* @param context the extension context
* @throws if the binary doesn't exist and can't be downloaded, or if the binary is not trusted
* @returns Returns the executable to launch LemMinX (the XML Language Server) as a binary
*/
export async function prepareBinaryExecutable(context: ExtensionContext): Promise<Executable> {
const binaryOptions: string = getXMLConfiguration().get("server.binary.args");
let binaryExecutable: Executable;
return getServerBinaryPath()
.then((binaryPath: string) => {
binaryExecutable = {
args: [binaryOptions],
command: binaryPath
} as Executable;
return binaryPath;
})
.then(checkBinaryHash)
.then((hashOk: boolean) => {
if (hashOk) {
return binaryExecutable;
} else {
return new Promise((resolve, reject) => {
return window.showErrorMessage(`The server binary ${binaryExecutable.command} is not trusted. `
+ 'Running the file poses a threat to your system\'s security. '
+ 'Run anyways?', 'Yes', 'No')
.then((val: string) => {
if (val === 'Yes') {
resolve(binaryExecutable);
}
reject("The binary XML language server is not trusted");
});
});
}
});
}

/**
* Returns the path to the LemMinX binary
*
* Downloads it if it is missing
*
* @returns The path to the LemMinX binary
* @throws If the LemMinX binary can't be located or downloaded
*/
async function getServerBinaryPath(): Promise<string> {
const config: WorkspaceConfiguration = getXMLConfiguration();
let binaryPath: string = config.get("server.binary.path");
if (binaryPath) {
if (fs.existsSync(binaryPath)) {
return Promise.resolve(binaryPath);
}
window.showErrorMessage('The specified XML language server binary could not be found. Using the default binary...');
}
let server_home: string = path.resolve(__dirname, '../server');
let binaries: Array<string> = glob.sync(`**/lemminx-${os.platform()}*`, { cwd: server_home });
const JAR_AND_HASH_REJECTOR: RegExp = /(\.jar)|(hash)$/;
binaries = binaries.filter((path) => { return !JAR_AND_HASH_REJECTOR.test(path) });
if (binaries.length) {
return new Promise<string>((resolve, reject) => {
resolve(path.resolve(server_home, binaries[0]));
});
}
// Download it, then return the downloaded binary's location
return downloadBinary();
}

/**
* Downloads LemMinX binary under the `server` directory and returns the path to the binary as a Promise
*
* @returns The path to the LemMinX binary
* @throws If the LemMinX binary download fails
*/
async function downloadBinary(): Promise<string> {
window.setStatusBarMessage('Downloading XML language server binary...', 2000);
return new Promise((resolve, reject) => {
const handleResponse = (response: http.IncomingMessage) => {
const statusCode = response.statusCode;
if (statusCode === 303) {
http.get(response.headers.location, handleResponse);
} else if (statusCode === 200) {
// This is probably the problem in Theia TODO:
const serverBinaryPath = path.resolve(__dirname, '../server', getServerBinaryName());
const serverBinaryFileStream = fs.createWriteStream(serverBinaryPath);
response.pipe(serverBinaryFileStream);
serverBinaryFileStream.on('finish', () => {
serverBinaryFileStream.on('close', () => {
fs.chmodSync(serverBinaryPath, "766");
resolve(serverBinaryPath);
});
serverBinaryFileStream.close();
});
} else {
reject('Server binary download failed');
}
}
http.get(`http://localhost:8080/lemminx-redirect/${os.platform()}`, handleResponse)
.on('error', () => {
reject('Server binary download failed');
});
});
}

/**
* Returns true if the hash of the binary matches the expected hash and false otherwise
*
* @param binaryPath the path to the binary to check
* @returns true if the hash of the binary matches the expected hash and false otherwise
*/
async function checkBinaryHash(binaryPath: string): Promise<boolean> {
const hash: Hash = createHash('sha256');
return new Promise((resolve, reject) => {
fs.readFile(path.resolve(binaryPath), (err, fileContents) => {
if (err) {
reject(err)
};
resolve(fileContents);
});
})
.then((fileContents: string) => {
hash.update(fileContents);
const hashDigest: string = hash.digest('hex').toLowerCase();
const expectedHashPath: string = path.resolve(__dirname, '../server', `lemminx-${os.platform}-hash`);
const expectedHash = fs.readFileSync(expectedHashPath).toString('utf-8').toLowerCase().split(' ')[0];
return hashDigest === expectedHash;
})
.catch((err: any) => {
return false;
});
}

/**
* Returns the name of the LemMinX server binary executable file
*
* @return the name of the LemMinX server binary executable file
*/
function getServerBinaryName(): string {
return `lemminx-${os.platform()}${os.platform() === 'win32' ? ".exe" : ""}`;
}
Loading

0 comments on commit d8c8903

Please sign in to comment.