Samuel Shuert | 274a4d6 | 2023-12-01 15:04:55 -0500 | [diff] [blame] | 1 | 'use strict'; |
| 2 | |
| 3 | const Module = require('module'); |
| 4 | const crypto = require('crypto'); |
| 5 | const fs = require('fs'); |
| 6 | const path = require('path'); |
| 7 | const vm = require('vm'); |
| 8 | const os = require('os'); |
| 9 | |
| 10 | const hasOwnProperty = Object.prototype.hasOwnProperty; |
| 11 | |
| 12 | //------------------------------------------------------------------------------ |
| 13 | // FileSystemBlobStore |
| 14 | //------------------------------------------------------------------------------ |
| 15 | |
| 16 | class FileSystemBlobStore { |
| 17 | constructor(directory, prefix) { |
| 18 | const name = prefix ? slashEscape(prefix + '.') : ''; |
| 19 | this._blobFilename = path.join(directory, name + 'BLOB'); |
| 20 | this._mapFilename = path.join(directory, name + 'MAP'); |
| 21 | this._lockFilename = path.join(directory, name + 'LOCK'); |
| 22 | this._directory = directory; |
| 23 | this._load(); |
| 24 | } |
| 25 | |
| 26 | has(key, invalidationKey) { |
| 27 | if (hasOwnProperty.call(this._memoryBlobs, key)) { |
| 28 | return this._invalidationKeys[key] === invalidationKey; |
| 29 | } else if (hasOwnProperty.call(this._storedMap, key)) { |
| 30 | return this._storedMap[key][0] === invalidationKey; |
| 31 | } |
| 32 | return false; |
| 33 | } |
| 34 | |
| 35 | get(key, invalidationKey) { |
| 36 | if (hasOwnProperty.call(this._memoryBlobs, key)) { |
| 37 | if (this._invalidationKeys[key] === invalidationKey) { |
| 38 | return this._memoryBlobs[key]; |
| 39 | } |
| 40 | } else if (hasOwnProperty.call(this._storedMap, key)) { |
| 41 | const mapping = this._storedMap[key]; |
| 42 | if (mapping[0] === invalidationKey) { |
| 43 | return this._storedBlob.slice(mapping[1], mapping[2]); |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | set(key, invalidationKey, buffer) { |
| 49 | this._invalidationKeys[key] = invalidationKey; |
| 50 | this._memoryBlobs[key] = buffer; |
| 51 | this._dirty = true; |
| 52 | } |
| 53 | |
| 54 | delete(key) { |
| 55 | if (hasOwnProperty.call(this._memoryBlobs, key)) { |
| 56 | this._dirty = true; |
| 57 | delete this._memoryBlobs[key]; |
| 58 | } |
| 59 | if (hasOwnProperty.call(this._invalidationKeys, key)) { |
| 60 | this._dirty = true; |
| 61 | delete this._invalidationKeys[key]; |
| 62 | } |
| 63 | if (hasOwnProperty.call(this._storedMap, key)) { |
| 64 | this._dirty = true; |
| 65 | delete this._storedMap[key]; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | isDirty() { |
| 70 | return this._dirty; |
| 71 | } |
| 72 | |
| 73 | save() { |
| 74 | const dump = this._getDump(); |
| 75 | const blobToStore = Buffer.concat(dump[0]); |
| 76 | const mapToStore = JSON.stringify(dump[1]); |
| 77 | |
| 78 | try { |
| 79 | mkdirpSync(this._directory); |
| 80 | fs.writeFileSync(this._lockFilename, 'LOCK', {flag: 'wx'}); |
| 81 | } catch (error) { |
| 82 | // Swallow the exception if we fail to acquire the lock. |
| 83 | return false; |
| 84 | } |
| 85 | |
| 86 | try { |
| 87 | fs.writeFileSync(this._blobFilename, blobToStore); |
| 88 | fs.writeFileSync(this._mapFilename, mapToStore); |
| 89 | } finally { |
| 90 | fs.unlinkSync(this._lockFilename); |
| 91 | } |
| 92 | |
| 93 | return true; |
| 94 | } |
| 95 | |
| 96 | _load() { |
| 97 | try { |
| 98 | this._storedBlob = fs.readFileSync(this._blobFilename); |
| 99 | this._storedMap = JSON.parse(fs.readFileSync(this._mapFilename)); |
| 100 | } catch (e) { |
| 101 | this._storedBlob = Buffer.alloc(0); |
| 102 | this._storedMap = {}; |
| 103 | } |
| 104 | this._dirty = false; |
| 105 | this._memoryBlobs = {}; |
| 106 | this._invalidationKeys = {}; |
| 107 | } |
| 108 | |
| 109 | _getDump() { |
| 110 | const buffers = []; |
| 111 | const newMap = {}; |
| 112 | let offset = 0; |
| 113 | |
| 114 | function push(key, invalidationKey, buffer) { |
| 115 | buffers.push(buffer); |
| 116 | newMap[key] = [invalidationKey, offset, offset + buffer.length]; |
| 117 | offset += buffer.length; |
| 118 | } |
| 119 | |
| 120 | for (const key of Object.keys(this._memoryBlobs)) { |
| 121 | const buffer = this._memoryBlobs[key]; |
| 122 | const invalidationKey = this._invalidationKeys[key]; |
| 123 | push(key, invalidationKey, buffer); |
| 124 | } |
| 125 | |
| 126 | for (const key of Object.keys(this._storedMap)) { |
| 127 | if (hasOwnProperty.call(newMap, key)) continue; |
| 128 | const mapping = this._storedMap[key]; |
| 129 | const buffer = this._storedBlob.slice(mapping[1], mapping[2]); |
| 130 | push(key, mapping[0], buffer); |
| 131 | } |
| 132 | |
| 133 | return [buffers, newMap]; |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | //------------------------------------------------------------------------------ |
| 138 | // NativeCompileCache |
| 139 | //------------------------------------------------------------------------------ |
| 140 | |
| 141 | class NativeCompileCache { |
| 142 | constructor() { |
| 143 | this._cacheStore = null; |
| 144 | this._previousModuleCompile = null; |
| 145 | } |
| 146 | |
| 147 | setCacheStore(cacheStore) { |
| 148 | this._cacheStore = cacheStore; |
| 149 | } |
| 150 | |
| 151 | install() { |
| 152 | const self = this; |
| 153 | const hasRequireResolvePaths = typeof require.resolve.paths === 'function'; |
| 154 | this._previousModuleCompile = Module.prototype._compile; |
| 155 | Module.prototype._compile = this._ownModuleCompile = _ownModuleCompile; |
| 156 | self.enabled = true; |
| 157 | function _ownModuleCompile(content, filename) { |
| 158 | if(!self.enabled) return this._previousModuleCompile.apply(this, arguments); |
| 159 | const mod = this; |
| 160 | |
| 161 | function require(id) { |
| 162 | return mod.require(id); |
| 163 | } |
| 164 | |
| 165 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L28 |
| 166 | function resolve(request, options) { |
| 167 | return Module._resolveFilename(request, mod, false, options); |
| 168 | } |
| 169 | require.resolve = resolve; |
| 170 | |
| 171 | // https://github.com/nodejs/node/blob/v10.15.3/lib/internal/modules/cjs/helpers.js#L37 |
| 172 | // resolve.resolve.paths was added in v8.9.0 |
| 173 | if (hasRequireResolvePaths) { |
| 174 | resolve.paths = function paths(request) { |
| 175 | return Module._resolveLookupPaths(request, mod, true); |
| 176 | }; |
| 177 | } |
| 178 | |
| 179 | require.main = process.mainModule; |
| 180 | |
| 181 | // Enable support to add extra extension types |
| 182 | require.extensions = Module._extensions; |
| 183 | require.cache = Module._cache; |
| 184 | |
| 185 | const dirname = path.dirname(filename); |
| 186 | |
| 187 | const compiledWrapper = self._moduleCompile(filename, content); |
| 188 | |
| 189 | // We skip the debugger setup because by the time we run, node has already |
| 190 | // done that itself. |
| 191 | |
| 192 | // `Buffer` is included for Electron. |
| 193 | // See https://github.com/zertosh/v8-compile-cache/pull/10#issuecomment-518042543 |
| 194 | const args = [mod.exports, require, mod, filename, dirname, process, global, Buffer]; |
| 195 | return compiledWrapper.apply(mod.exports, args); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | uninstall() { |
| 200 | this.enabled = false; |
| 201 | // If something else has since been installed on top of us, we cannot overwrite it. |
| 202 | if(Module.prototype._compile === this._ownModuleCompile) { |
| 203 | Module.prototype._compile = this._previousModuleCompile; |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | _moduleCompile(filename, content) { |
| 208 | // https://github.com/nodejs/node/blob/v7.5.0/lib/module.js#L511 |
| 209 | |
| 210 | // Remove shebang |
| 211 | var contLen = content.length; |
| 212 | if (contLen >= 2) { |
| 213 | if (content.charCodeAt(0) === 35/*#*/ && |
| 214 | content.charCodeAt(1) === 33/*!*/) { |
| 215 | if (contLen === 2) { |
| 216 | // Exact match |
| 217 | content = ''; |
| 218 | } else { |
| 219 | // Find end of shebang line and slice it off |
| 220 | var i = 2; |
| 221 | for (; i < contLen; ++i) { |
| 222 | var code = content.charCodeAt(i); |
| 223 | if (code === 10/*\n*/ || code === 13/*\r*/) break; |
| 224 | } |
| 225 | if (i === contLen) { |
| 226 | content = ''; |
| 227 | } else { |
| 228 | // Note that this actually includes the newline character(s) in the |
| 229 | // new output. This duplicates the behavior of the regular |
| 230 | // expression that was previously used to replace the shebang line |
| 231 | content = content.slice(i); |
| 232 | } |
| 233 | } |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | // create wrapper function |
| 238 | var wrapper = Module.wrap(content); |
| 239 | |
| 240 | var invalidationKey = crypto |
| 241 | .createHash('sha1') |
| 242 | .update(content, 'utf8') |
| 243 | .digest('hex'); |
| 244 | |
| 245 | var buffer = this._cacheStore.get(filename, invalidationKey); |
| 246 | |
| 247 | var script = new vm.Script(wrapper, { |
| 248 | filename: filename, |
| 249 | lineOffset: 0, |
| 250 | displayErrors: true, |
| 251 | cachedData: buffer, |
| 252 | produceCachedData: true, |
| 253 | }); |
| 254 | |
| 255 | if (script.cachedDataProduced) { |
| 256 | this._cacheStore.set(filename, invalidationKey, script.cachedData); |
| 257 | } else if (script.cachedDataRejected) { |
| 258 | this._cacheStore.delete(filename); |
| 259 | } |
| 260 | |
| 261 | var compiledWrapper = script.runInThisContext({ |
| 262 | filename: filename, |
| 263 | lineOffset: 0, |
| 264 | columnOffset: 0, |
| 265 | displayErrors: true, |
| 266 | }); |
| 267 | |
| 268 | return compiledWrapper; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | //------------------------------------------------------------------------------ |
| 273 | // utilities |
| 274 | // |
| 275 | // https://github.com/substack/node-mkdirp/blob/f2003bb/index.js#L55-L98 |
| 276 | // https://github.com/zertosh/slash-escape/blob/e7ebb99/slash-escape.js |
| 277 | //------------------------------------------------------------------------------ |
| 278 | |
| 279 | function mkdirpSync(p_) { |
| 280 | _mkdirpSync(path.resolve(p_), 0o777); |
| 281 | } |
| 282 | |
| 283 | function _mkdirpSync(p, mode) { |
| 284 | try { |
| 285 | fs.mkdirSync(p, mode); |
| 286 | } catch (err0) { |
| 287 | if (err0.code === 'ENOENT') { |
| 288 | _mkdirpSync(path.dirname(p)); |
| 289 | _mkdirpSync(p); |
| 290 | } else { |
| 291 | try { |
| 292 | const stat = fs.statSync(p); |
| 293 | if (!stat.isDirectory()) { throw err0; } |
| 294 | } catch (err1) { |
| 295 | throw err0; |
| 296 | } |
| 297 | } |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | function slashEscape(str) { |
| 302 | const ESCAPE_LOOKUP = { |
| 303 | '\\': 'zB', |
| 304 | ':': 'zC', |
| 305 | '/': 'zS', |
| 306 | '\x00': 'z0', |
| 307 | 'z': 'zZ', |
| 308 | }; |
| 309 | const ESCAPE_REGEX = /[\\:/\x00z]/g; // eslint-disable-line no-control-regex |
| 310 | return str.replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); |
| 311 | } |
| 312 | |
| 313 | function supportsCachedData() { |
| 314 | const script = new vm.Script('""', {produceCachedData: true}); |
| 315 | // chakracore, as of v1.7.1.0, returns `false`. |
| 316 | return script.cachedDataProduced === true; |
| 317 | } |
| 318 | |
| 319 | function getCacheDir() { |
| 320 | const v8_compile_cache_cache_dir = process.env.V8_COMPILE_CACHE_CACHE_DIR; |
| 321 | if (v8_compile_cache_cache_dir) { |
| 322 | return v8_compile_cache_cache_dir; |
| 323 | } |
| 324 | |
| 325 | // Avoid cache ownership issues on POSIX systems. |
| 326 | const dirname = typeof process.getuid === 'function' |
| 327 | ? 'v8-compile-cache-' + process.getuid() |
| 328 | : 'v8-compile-cache'; |
| 329 | const version = typeof process.versions.v8 === 'string' |
| 330 | ? process.versions.v8 |
| 331 | : typeof process.versions.chakracore === 'string' |
| 332 | ? 'chakracore-' + process.versions.chakracore |
| 333 | : 'node-' + process.version; |
| 334 | const cacheDir = path.join(os.tmpdir(), dirname, version); |
| 335 | return cacheDir; |
| 336 | } |
| 337 | |
| 338 | function getMainName() { |
| 339 | // `require.main.filename` is undefined or null when: |
| 340 | // * node -e 'require("v8-compile-cache")' |
| 341 | // * node -r 'v8-compile-cache' |
| 342 | // * Or, requiring from the REPL. |
| 343 | const mainName = require.main && typeof require.main.filename === 'string' |
| 344 | ? require.main.filename |
| 345 | : process.cwd(); |
| 346 | return mainName; |
| 347 | } |
| 348 | |
| 349 | function install(opts) { |
| 350 | if (!process.env.DISABLE_V8_COMPILE_CACHE && supportsCachedData()) { |
| 351 | if(typeof opts === 'undefined') opts = {} |
| 352 | let cacheDir = opts.cacheDir |
| 353 | if(typeof cacheDir === 'undefined') cacheDir = getCacheDir(); |
| 354 | let prefix = opts.prefix |
| 355 | if(typeof prefix === 'undefined') prefix = getMainName(); |
| 356 | const blobStore = new FileSystemBlobStore(cacheDir, prefix); |
| 357 | |
| 358 | const nativeCompileCache = new NativeCompileCache(); |
| 359 | nativeCompileCache.setCacheStore(blobStore); |
| 360 | nativeCompileCache.install(); |
| 361 | |
| 362 | let uninstalled = false; |
| 363 | const uninstall = () => { |
| 364 | if(uninstalled) return; |
| 365 | uninstalled = true; |
| 366 | process.removeListener('exit', uninstall); |
| 367 | if (blobStore.isDirty()) { |
| 368 | blobStore.save(); |
| 369 | } |
| 370 | nativeCompileCache.uninstall(); |
| 371 | } |
| 372 | process.once('exit', uninstall); |
| 373 | return {uninstall}; |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | //------------------------------------------------------------------------------ |
| 378 | // main |
| 379 | //------------------------------------------------------------------------------ |
| 380 | |
| 381 | module.exports.install = install; |
| 382 | |
| 383 | module.exports.__TEST__ = { |
| 384 | FileSystemBlobStore, |
| 385 | NativeCompileCache, |
| 386 | mkdirpSync, |
| 387 | slashEscape, |
| 388 | supportsCachedData, |
| 389 | getCacheDir, |
| 390 | getMainName, |
| 391 | }; |