From 71600cf3666922c0cd6d828420b13f2357f697e1 Mon Sep 17 00:00:00 2001 From: Dave Holoway Date: Sat, 27 Jun 2020 18:33:13 +0100 Subject: [PATCH] cache decoded android library in globalStoragePath --- extension.js | 9 +- langserver/java/java-libraries.js | 174 ++++++++++++++++++++++++++++++ langserver/server.js | 21 ++-- 3 files changed, 194 insertions(+), 10 deletions(-) create mode 100644 langserver/java/java-libraries.js diff --git a/extension.js b/extension.js index bba2bcf..4b7d0ab 100644 --- a/extension.js +++ b/extension.js @@ -11,6 +11,9 @@ const { selectTargetDevice } = require('./src/utils/device'); /** @type {LanguageClient} */ let client; +/** + * @param {vscode.ExtensionContext} context + */ function activateLanguageClient(context) { // The server is implemented in node let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js')); @@ -38,10 +41,14 @@ function activateLanguageClient(context) { let clientOptions = { // Register the server for plain text documents documentSelector: [{ scheme: 'file', language: 'java' }], + initializationOptions: { + // globalStoragePath is used to cache decoded jar files + globalStoragePath: context.globalStoragePath, + }, synchronize: { // Notify the server about file changes to '.java files contained in the workspace fileEvents: vscode.workspace.createFileSystemWatcher('**/.java') - } + }, }; // Create the language client and start the client. diff --git a/langserver/java/java-libraries.js b/langserver/java/java-libraries.js new file mode 100644 index 0000000..11f3272 --- /dev/null +++ b/langserver/java/java-libraries.js @@ -0,0 +1,174 @@ +const fs = require('fs'); +const path = require('path'); +const { loadAndroidLibrary, CEIType } = require('java-mti'); + +const android_system_library_cache_filename = { + regex: /^sdk-platforms-android-(.+?)-(\d+)\.json$/, + /** + * @param {string} version + * @param {fs.Stats} stat + */ + build(version, stat) { + return `sdk-platforms-${version}-${Math.trunc(stat.mtime.getTime())}.json`; + } +} + +/** + * @param {string} cache_dir directory used to store decoded jar libraries + */ +function ensureFolderExists(cache_dir) { + if (!cache_dir) { + throw new Error('missing cache dir value'); + } + try { + const stat = fs.statSync(cache_dir); + if (!stat.isDirectory()) { + throw new Error(`cache dir '${cache_dir}' is not a directory`); + } + } catch (err) { + if (err.code !== 'ENOENT') { + throw new Error(`cache dir '${cache_dir}' check failed: ${err.message}`); + } + fs.mkdirSync(cache_dir); + } +} + +/** + * + * @param {string} cache_dir + * @param {RegExp} filter + * @returns {fs.Dirent[]} + */ +function findLibraryFiles(cache_dir, filter) { + const files = fs.readdirSync(cache_dir, { withFileTypes: true }); + const valid_files = files.filter(file => filter.test(file.name) && file.isFile()); + return valid_files; +} + +/** + * + * @param {fs.Dirent[]} files + */ +function chooseAndroidSystemLibrary(files) { + let chosen = { + api: 0, + /** @type {fs.Dirent} */ + file: null, + }; + files.forEach(file => { + const m = file.name.match(android_system_library_cache_filename.regex); + if (!m) return; + if (/^\d+$/.test(m[1])) { + const api = parseInt(m[1]); + if (api > chosen.api) { + chosen.api = api; + chosen.file = file; + } + } + }) + return chosen.file; +} + +function findHighestAPISystemLibrary(android_sdk_platforms_root) { + let platform_folders = [], android_platform_jars = []; + try { + platform_folders = fs.readdirSync(android_sdk_platforms_root, { withFileTypes: true }); + } catch {}; + platform_folders.forEach(folder => { + if (!folder.isDirectory()) return; + // we assume stable SDK platform folders are named 'android-' + if (!/^android-\d+$/.test(folder.name)) return; + // the platform folder must contain an android.jar file + let stat, filepath = path.join(android_sdk_platforms_root, folder.name, 'android.jar'); + try { stat = fs.statSync(filepath) } + catch { return } + if (!stat.isFile()) return; + // add it to the list + android_platform_jars.push({ + folder: folder.name, + api: parseInt(folder.name.split('-')[1], 10), + filepath, + stat, + }) + }); + if (android_platform_jars.length === 0) { + return null; + } + // choose the folder with the highest API number + return android_platform_jars.sort((a,b) => b.api - a.api)[0].folder; +} + +/** + * @param {string} cache_dir + * @param {string|':latest'} [version] + */ +async function buildAndroidSystemLibrary(cache_dir, version) { + const android_sdk_root = process.env['ANDROID_SDK'] || process.env['ANDROID_HOME']; + if (!android_sdk_root) { + throw new Error('Cannot locate Android SDK folder - ANDROID_SDK env variable is not defined'); + } + const android_sdk_platforms_root = path.join(android_sdk_root, 'platforms'); + + if (!version) { + // choose the folder with the highest API number + version = findHighestAPISystemLibrary(android_sdk_platforms_root); + if (!version) { + throw new Error(`Cannot build Android library: No supported system libraries found in ${android_sdk_platforms_root}`); + } + } + + let stat, filepath = path.join(android_sdk_platforms_root, version, 'android.jar'); + try { stat = fs.statSync(filepath) } + catch { } + if (!stat || !stat.isFile()) { + throw new Error(`Cannot build Android library: '${filepath}' is not a valid android system library file`); + } + + console.log(`Building ${version} library cache for code completion support. This might take a few minutes...`); + const cache_filename = path.join(cache_dir, android_system_library_cache_filename.build(version, stat)); + try { + const library = await loadAndroidLibrary(cache_filename, { api: version, sdk_root: android_sdk_root }); + console.log(`${version} library cache built.`); + return library; + } catch(err) { + throw new Error(`Cannot build Android library: ${err.message}`); + } +} + +/** + * @param {string} cache_dir directory used to store decoded jar libraries + * @returns {Promise>} + */ +async function loadAndroidSystemLibrary(cache_dir) { + console.time('android-library-load'); + // for (let x;;) { + // console.log('waiting'); + // if (x) break; + // await new Promise(res => setTimeout(res, 1000)); + // } + let library; + try { + ensureFolderExists(cache_dir); + const library_files = findLibraryFiles(cache_dir, android_system_library_cache_filename.regex); + if (!library_files.length) { + return buildAndroidSystemLibrary(cache_dir); + } + // search for the highest android API number in the list of cached files + const library_file = chooseAndroidSystemLibrary(library_files); + if (!library_file) { + return buildAndroidSystemLibrary(cache_dir); + } + // load the library + const library_path_name = path.join(cache_dir, library_file.name); + console.log(`Loading android system library: ${library_path_name}`); + library = await loadAndroidLibrary(library_path_name, null); + + } catch (err) { + console.error(`android library load failed: ${err.message}`); + library = new Map(); + } + console.timeEnd('android-library-load'); + return library; +} + +exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary; diff --git a/langserver/server.js b/langserver/server.js index 522a81b..7a87a65 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -18,7 +18,8 @@ const { const { TextDocument } = require('vscode-languageserver-textdocument'); -const { loadAndroidLibrary, JavaType, CEIType, ArrayType, PrimitiveType, Method } = require('java-mti'); +const { loadAndroidSystemLibrary } = require('./java/java-libraries'); +const { JavaType, CEIType, ArrayType, PrimitiveType, Method } = require('java-mti'); const { ParseProblem } = require('./java/parser'); const { parse } = require('./java/body-parser3'); @@ -289,14 +290,16 @@ let hasWorkspaceFolderCapability = false; let hasDiagnosticRelatedInformationCapability = false; connection.onInitialize((params) => { - console.time('android-library-load') - androidLibrary = loadAndroidLibrary('android-25').then(lib => { - console.timeEnd('android-library-load') - return androidLibrary = lib; - }, err => { - trace(`android library load failed: ${err.message}`); - return androidLibrary = new Map(); - }); + + // the storage path is passed to us by the client side of the extension + const library_cache_path = (params.initializationOptions || {}).globalStoragePath; + trace(`library_cache_path: ${library_cache_path}`); + + // the android library is loaded asynchronously, with the global `androidLibrary` variable + // set to the promise while it is loading. + androidLibrary = loadAndroidSystemLibrary(library_cache_path) + .then(library => androidLibrary = library); + let capabilities = params.capabilities; // Does the client support the `workspace/configuration` request?