diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000..6d6c4dc --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,34 @@ +/** + * @type {Record} + */ +const cacheObject = {}; + +module.exports = { + cacheFn: (key, fn, async = false) => { + return () => { + const cachedValue = cacheObject[key]; + + if (cachedValue !== undefined) { + if (async) { + return Promise.resolve(cachedValue); + } + + return cachedValue; + } + + const result = fn(); + + if (async) { + return result.then(value => { + cacheObject[key] = value; + + return value; + }); + } + + cacheObject[key] = result; + + return result; + }; + } +}; diff --git a/lib/detect-libc.js b/lib/detect-libc.js index 6e3507e..820e5bb 100644 --- a/lib/detect-libc.js +++ b/lib/detect-libc.js @@ -5,6 +5,10 @@ const childProcess = require('child_process'); const { isLinux, getReport } = require('./process'); +const { LDD_PATH, readFile, readFileSync } = require('./filesystem'); + +let cachedFamilyFilesystem; +let cachedVersionFilesystem; const command = 'getconf GNU_LIBC_VERSION 2>&1 || true; ldd --version 2>&1 || true'; let commandOut = ''; @@ -39,6 +43,13 @@ const safeCommandSync = () => { */ const GLIBC = 'glibc'; +/** + * A Regexp constant to get the GLIBC Version. + * @type {string} + * @public + */ +const RE_GLIBC_VERSION = /GLIBC\s(\d+\.\d+)/; + /** * A String constant containing the value `musl`. * @type {string} @@ -72,6 +83,38 @@ const familyFromCommand = (out) => { return null; }; +const familyFromFilesystem = async () => { + if (cachedFamilyFilesystem !== undefined) { + return cachedFamilyFilesystem; + } + cachedFamilyFilesystem = null; + try { + const lddContent = await readFile(LDD_PATH); + if (lddContent.includes('musl')) { + cachedFamilyFilesystem = MUSL; + } else if (lddContent.includes('GLIBC')) { + cachedFamilyFilesystem = GLIBC; + } + } catch (e) {} + return cachedFamilyFilesystem; +}; + +const familyFromFilesystemSync = () => { + if (cachedFamilyFilesystem !== undefined) { + return cachedFamilyFilesystem; + } + cachedFamilyFilesystem = null; + try { + const lddContent = readFileSync(LDD_PATH); + if (lddContent.includes('musl')) { + cachedFamilyFilesystem = MUSL; + } else if (lddContent.includes('GLIBC')) { + cachedFamilyFilesystem = GLIBC; + } + } catch (e) {} + return cachedFamilyFilesystem; +}; + /** * Resolves with the libc family when it can be determined, `null` otherwise. * @returns {Promise} @@ -79,7 +122,10 @@ const familyFromCommand = (out) => { const family = async () => { let family = null; if (isLinux()) { - family = familyFromReport(); + family = await familyFromFilesystem(); + if (!family) { + family = familyFromReport(); + } if (!family) { const out = await safeCommand(); family = familyFromCommand(out); @@ -95,7 +141,10 @@ const family = async () => { const familySync = () => { let family = null; if (isLinux()) { - family = familyFromReport(); + family = familyFromFilesystemSync(); + if (!family) { + family = familyFromReport(); + } if (!family) { const out = safeCommandSync(); family = familyFromCommand(out); @@ -116,6 +165,38 @@ const isNonGlibcLinux = async () => isLinux() && await family() !== GLIBC; */ const isNonGlibcLinuxSync = () => isLinux() && familySync() !== GLIBC; +const versionFromFilesystem = async () => { + if (cachedVersionFilesystem !== undefined) { + return cachedVersionFilesystem; + } + cachedVersionFilesystem = null; + try { + const lddContent = await readFile(LDD_PATH); + const versionMatch = lddContent.match(RE_GLIBC_VERSION); + + if (versionMatch) { + cachedVersionFilesystem = versionMatch[1]; + } + } catch (e) {} + return cachedVersionFilesystem; +}; + +const versionFromFilesystemSync = () => { + if (cachedVersionFilesystem !== undefined) { + return cachedVersionFilesystem; + } + cachedVersionFilesystem = null; + try { + const lddContent = readFileSync(LDD_PATH); + const versionMatch = lddContent.match(RE_GLIBC_VERSION); + + if (versionMatch) { + cachedVersionFilesystem = versionMatch[1]; + } + } catch (e) {} + return cachedVersionFilesystem; +}; + const versionFromReport = () => { const report = getReport(); if (report.header && report.header.glibcVersionRuntime) { @@ -144,7 +225,10 @@ const versionFromCommand = (out) => { const version = async () => { let version = null; if (isLinux()) { - version = versionFromReport(); + version = await versionFromFilesystem(); + if (!version) { + version = versionFromReport(); + } if (!version) { const out = await safeCommand(); version = versionFromCommand(out); @@ -160,7 +244,10 @@ const version = async () => { const versionSync = () => { let version = null; if (isLinux()) { - version = versionFromReport(); + version = versionFromFilesystemSync(); + if (!version) { + version = versionFromReport(); + } if (!version) { const out = safeCommandSync(); version = versionFromCommand(out); diff --git a/lib/filesystem.js b/lib/filesystem.js new file mode 100644 index 0000000..d0f7864 --- /dev/null +++ b/lib/filesystem.js @@ -0,0 +1,36 @@ +const fs = require('fs'); + +/** + * The path where we can find the ldd + */ +const LDD_PATH = '/usr/bin/ldd'; + +/** + * Read the content of a file synchronous + * + * @param {string} path + * @returns {string} + */ +const readFileSync = path => fs.readFileSync(path, 'utf-8'); + +/** + * Read the content of a file + * + * @param {string} path + * @returns {Promise} + */ +const readFile = path => new Promise((resolve, reject) => { + fs.readFile(path, 'utf-8', (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); +}); + +module.exports = { + LDD_PATH, + readFileSync, + readFile +}; diff --git a/test/fixtexture-file.txt b/test/fixtexture-file.txt new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/test/fixtexture-file.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/test/unit.js b/test/unit.js index 691d9b9..cfe4c87 100644 --- a/test/unit.js +++ b/test/unit.js @@ -4,10 +4,43 @@ 'use strict'; const test = require('ava'); +const path = require('path'); const proxyquire = require('proxyquire') .noCallThru() .noPreserveCache(); +const filePermissionError = new Error('Read error'); +filePermissionError.code = 'ERR_ACCESS_DENIED'; + +test('filesystem - file found', async (t) => { + t.plan(2); + + const filesystem = require('../lib/filesystem'); + const notExistFile = path.join(__dirname, './non-exist.txt'); + + try { + await filesystem.readFile(notExistFile); + } catch (e) { + t.true(e instanceof Error); + } + + try { + filesystem.readFileSync(notExistFile); + } catch (e) { + t.true(e instanceof Error); + } +}); + +test('filesystem - file not found', async (t) => { + t.plan(2); + + const filesystem = require('../lib/filesystem'); + const fixTextureFilePath = path.join(__dirname, './fixtexture-file.txt'); + + t.is(await filesystem.readFile(fixTextureFilePath), '1'); + t.is(filesystem.readFileSync(fixTextureFilePath), '1'); +}); + test('constants', (t) => { t.plan(2); @@ -17,8 +50,84 @@ test('constants', (t) => { t.is(libc.MUSL, 'musl'); }); +test('linux - glibc family detected via ldd', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFile: () => Promise.resolve('bunch-of-text GLIBC') + } + }); + + t.is(await libc.family(), libc.GLIBC); + t.false(await libc.isNonGlibcLinux()); +}); + +test('linux - glibc familySync detected via ldd', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFileSync: () => 'bunch-of-text GLIBC' + } + }); + + t.is(libc.familySync(), libc.GLIBC); + t.false(libc.isNonGlibcLinuxSync()); +}); + +test('linux - glibc family detected via ldd on error fallback', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + header: { + glibcVersionRuntime: '1.23' + } + }) + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError) + } + }); + + t.is(await libc.family(), libc.GLIBC); + t.false(await libc.isNonGlibcLinux()); +}); + +test('linux - glibc familySync detected via ldd on error fallback', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + header: { + glibcVersionRuntime: '1.23' + } + }) + }, + './filesystem': { + readFileSync: () => { + throw filePermissionError; + } + } + }); + + t.is(libc.familySync(), libc.GLIBC); + t.false(libc.isNonGlibcLinuxSync()); +}); + test('linux - glibc family and version detected via report', async (t) => { - t.plan(6); + t.plan(3); const libc = proxyquire('../', { './process': { @@ -28,18 +137,115 @@ test('linux - glibc family and version detected via report', async (t) => { glibcVersionRuntime: '1.23' } }) + }, + './filesystem': { + readFile: () => Promise.resolve('bunch-of-text') } }); t.is(await libc.family(), libc.GLIBC); t.is(await libc.version(), '1.23'); t.false(await libc.isNonGlibcLinux()); +}); + +test('linux - glibc familySync and version detected via report', async (t) => { + t.plan(3); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + header: { + glibcVersionRuntime: '1.23' + } + }) + }, + './filesystem': { + readFileSync: () => 'bunch-of-text' + } + }); t.is(libc.familySync(), libc.GLIBC); t.is(libc.versionSync(), '1.23'); t.false(libc.isNonGlibcLinuxSync()); }); +test('linux - musl family detected via ldd', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFile: () => Promise.resolve('bunch-of-text-musl') + } + }); + + t.is(await libc.family(), libc.MUSL); + t.true(await libc.isNonGlibcLinux()); +}); + +test('linux - musl familySync detected via ldd', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFileSync: () => 'bunch-of-text-musl' + } + }); + + t.is(libc.familySync(), libc.MUSL); + t.true(libc.isNonGlibcLinuxSync()); +}); + +test('linux - musl family fallback when not found ldd', async (t) => { + t.plan(2); + + const someError = new Error('Some error'); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + sharedObjects: ['/lib/ld-musl-x86_64.so.1'] + }) + }, + './filesystem': { + readFile: () => Promise.reject(someError) + } + }); + + t.is(await libc.family(), libc.MUSL); + t.true(await libc.isNonGlibcLinux()); +}); + +test('linux - musl familySync fallback when not found ldd', async (t) => { + t.plan(2); + + const someError = new Error('Some error'); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + sharedObjects: ['/lib/ld-musl-x86_64.so.1'] + }) + }, + './filesystem': { + readFileSync: () => { + throw someError; + } + } + }); + + t.is(libc.familySync(), libc.MUSL); + t.true(libc.isNonGlibcLinuxSync()); +}); + test('linux - musl family detected via report', async (t) => { t.plan(4); @@ -49,6 +255,12 @@ test('linux - musl family detected via report', async (t) => { getReport: () => ({ sharedObjects: ['/lib/ld-musl-x86_64.so.1'] }) + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); @@ -70,6 +282,12 @@ test('linux - glibc family detected via async child process', async (t) => { }, child_process: { exec: (_c, cb) => cb(null, out) + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); @@ -88,6 +306,12 @@ test('linux - glibc family detected via sync child process', async (t) => { }, child_process: { execSync: () => out + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); @@ -95,8 +319,8 @@ test('linux - glibc family detected via sync child process', async (t) => { t.false(libc.isNonGlibcLinuxSync()); }); -test('linux - musl family detected via child process', async (t) => { - t.plan(4); +test('linux - musl family detected via async child process', async (t) => { + t.plan(2); const out = 'getconf: GNU_LIBC_VERSION: unknown variable\nmusl libc\nVersion 1.2.3\netc'; const libc = proxyquire('../', { @@ -107,13 +331,41 @@ test('linux - musl family detected via child process', async (t) => { }) }, child_process: { - exec: (_c, cb) => cb(null, out), - execSync: () => out + exec: (_c, cb) => cb(null, out) + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); t.is(await libc.family(), libc.MUSL); t.true(await libc.isNonGlibcLinux()); +}); + +test('linux - musl family detected via sync child process', async (t) => { + t.plan(2); + + const out = 'getconf: GNU_LIBC_VERSION: unknown variable\nmusl libc\nVersion 1.2.3\netc'; + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({ + sharedObjects: [] + }) + }, + child_process: { + execSync: () => out + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } + } + }); t.is(libc.familySync(), libc.MUSL); t.true(libc.isNonGlibcLinuxSync()); @@ -131,6 +383,12 @@ test('linux - unknown family', async (t) => { child_process: { exec: (_c, cb) => cb(null, out), execSync: () => out + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); @@ -142,7 +400,7 @@ test('linux - unknown family', async (t) => { }); test('linux - unknown family (exec fails)', async (t) => { - t.plan(4); + t.plan(2); const libc = proxyquire('../', { './process': { @@ -150,20 +408,44 @@ test('linux - unknown family (exec fails)', async (t) => { getReport: () => ({}) }, child_process: { - exec: (_c, cb) => cb(new Error()), - execSync: () => { throw new Error(); } + exec: (_c, cb) => cb(new Error()) + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError), + readFileSync: () => { + throw filePermissionError; + } } }); t.is(await libc.family(), null); t.true(await libc.isNonGlibcLinux()); +}); + +test('linux - unknown family (execSync fails)', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({}) + }, + child_process: { + execSync: () => { throw new Error(); } + }, + './filesystem': { + readFileSync: () => { + throw filePermissionError; + } + } + }); t.is(libc.familySync(), null); t.true(libc.isNonGlibcLinuxSync()); }); test('non-linux - unknown family', async (t) => { - t.plan(4); + t.plan(2); const libc = proxyquire('../', { './process': { @@ -173,6 +455,16 @@ test('non-linux - unknown family', async (t) => { t.is(await libc.family(), null); t.false(await libc.isNonGlibcLinux()); +}); + +test('non-linux - unknown familySync', async (t) => { + t.plan(2); + + const libc = proxyquire('../', { + './process': { + isLinux: () => false + } + }); t.is(libc.familySync(), null); t.false(libc.isNonGlibcLinuxSync()); @@ -180,8 +472,40 @@ test('non-linux - unknown family', async (t) => { // version +test('linux - glibc version detected via filesystem', async (t) => { + t.plan(1); + + const out = '--vers | --versi | --versio | --version)\necho \'ldd (Ubuntu GLIBC 1.23-0ubuntu9.9) 1.23\''; + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFile: () => Promise.resolve(out) + } + }); + + t.is(await libc.version(), '1.23'); +}); + +test('linux - glibc version detected via filesystemSync', async (t) => { + t.plan(1); + + const out = '--vers | --versi | --versio | --version)\necho \'ldd (Ubuntu GLIBC 1.23-0ubuntu9.9) 1.23\''; + const libc = proxyquire('../', { + './process': { + isLinux: () => true + }, + './filesystem': { + readFileSync: () => out + } + }); + + t.is(libc.versionSync(), '1.23'); +}); + test('linux - glibc version detected via child process', async (t) => { - t.plan(2); + t.plan(1); const out = 'glibc 1.23\nldd (GLIBC) 1.23\nCopyright\netc'; const libc = proxyquire('../', { @@ -192,15 +516,39 @@ test('linux - glibc version detected via child process', async (t) => { child_process: { exec: (_c, cb) => cb(null, out), execSync: () => out + }, + './filesystem': { + readFile: () => Promise.reject(filePermissionError) } }); t.is(await libc.version(), '1.23'); +}); + +test('linux - glibc version detected via child process sync', async (t) => { + t.plan(1); + + const out = 'glibc 1.23\nldd (GLIBC) 1.23\nCopyright\netc'; + const libc = proxyquire('../', { + './process': { + isLinux: () => true, + getReport: () => ({}) + }, + child_process: { + execSync: () => out + }, + './filesystem': { + readFileSync: () => { + throw filePermissionError; + } + } + }); + t.is(libc.versionSync(), '1.23'); }); test('linux - musl version detected via child process', async (t) => { - t.plan(2); + t.plan(4); const out = 'getconf: GNU_LIBC_VERSION: unknown variable\nmusl libc\nVersion 1.2.3\netc'; const libc = proxyquire('../', { @@ -211,11 +559,18 @@ test('linux - musl version detected via child process', async (t) => { child_process: { exec: (_c, cb) => cb(null, out), execSync: () => out + }, + './filesystem': { + readFile: () => Promise.resolve('does not have version') } }); t.is(await libc.version(), '1.2.3'); t.is(libc.versionSync(), '1.2.3'); + + // calling twice to check the cache + t.is(await libc.version(), '1.2.3'); + t.is(libc.versionSync(), '1.2.3'); }); test('linux - unknown version', async (t) => { @@ -230,6 +585,9 @@ test('linux - unknown version', async (t) => { child_process: { exec: (_c, cb) => cb(null, out), execSync: () => out + }, + './filesystem': { + readFile: () => Promise.resolve('does not have version') } }); @@ -248,6 +606,9 @@ test('linux - unknown version (exec fails)', async (t) => { child_process: { exec: (_c, cb) => cb(new Error()), execSync: () => { throw new Error(); } + }, + './filesystem': { + readFile: () => Promise.resolve('does not have version') } });