diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18834cf1..b0ec89be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: ci: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - name: Checkout diff --git a/.github/workflows/os-compatibility.yml b/.github/workflows/os-compatibility.yml index 9281e194..72ce8112 100644 --- a/.github/workflows/os-compatibility.yml +++ b/.github/workflows/os-compatibility.yml @@ -14,6 +14,8 @@ jobs: fail-fast: false matrix: os: [macos-13, macos-14, macos-15, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04, windows-2019, windows-2022] + #There is no 10.0.0 at the time of writing, but since greater than characters are not allowed in GitHub Actions artifacts names, 9.0.1 - 10.0.0 will be used instead of >9.0.0 + version-requirement: ['5.7.19 - 5.7.24', '5.7.25 - 5.7.29', '5.7.30 - 5.7.34', '5.7.35 - 5.7.39', '5.7.40 - 5.7.44', '8.0.0 - 8.0.4', '8.0.4 - 8.0.13', '8.0.14 - 8.0.19', '8.0.20 - 8.0.24', '8.0.25 - 8.0.29', '8.0.30 - 8.0.34', '8.0.35 - 8.0.39', '8.0.40 - 8.3.0', '8.4.0 - 9.0.0', '9.0.1 - 10.0.0'] steps: - name: Checkout @@ -29,23 +31,25 @@ jobs: run: npm ci - name: Run tests - run: npm run test:ci + env: + VERSION_REQUIREMENT: ${{ matrix.version-requirement }} + run: npm run os-compat:ci - name: Upload mysqlmsn directory (Windows) if: ${{ failure() && runner.os == 'Windows' }} uses: actions/upload-artifact@v4.3.5 with: - name: ${{ matrix.os }} + name: ${{ matrix.os }}-${{ matrix.version-requirement }} path: "C:\\Users\\RUNNER~1\\mysqlmsn" - compression-level: 9 + compression-level: 0 - name: Upload mysqlmsn directory (Not Windows) if: ${{ failure() && runner.os != 'Windows' }} uses: actions/upload-artifact@v4.3.5 with: - name: ${{ matrix.os }} + name: ${{ matrix.os }}-${{ matrix.version-requirement }} path: /tmp/mysqlmsn - compression-level: 9 + compression-level: 0 fedora-docker: runs-on: ubuntu-latest @@ -54,6 +58,8 @@ jobs: fail-fast: false matrix: version: [40] + #There is no 10.0.0 at the time of writing, but since greater than characters are not allowed in GitHub Actions artifacts names, 9.0.1 - 10.0.0 will be used instead of >9.0.0 + version-requirement: ['5.7.19 - 5.7.24', '5.7.25 - 5.7.29', '5.7.30 - 5.7.34', '5.7.35 - 5.7.39', '5.7.40 - 5.7.44', '8.0.0 - 8.0.4', '8.0.4 - 8.0.13', '8.0.14 - 8.0.19', '8.0.20 - 8.0.24', '8.0.25 - 8.0.29', '8.0.30 - 8.0.34', '8.0.35 - 8.0.39', '8.0.40 - 8.3.0', '8.4.0 - 9.0.0', '9.0.1 - 10.0.0'] container: fedora:${{ matrix.version }} @@ -68,18 +74,23 @@ jobs: check-latest: true - name: Install required libraries - run: sudo dnf install libaio numactl -y + run: sudo dnf install libaio numactl libxcrypt-compat -y - name: Install packages run: npm ci + - name: Print available storage space + run: df -h + - name: Run tests - run: npm run test:ci + env: + VERSION_REQUIREMENT: ${{ matrix.version-requirement }} + run: npm run os-compat:ci - name: Upload mysqlmsn directory if: ${{ failure() }} uses: actions/upload-artifact@v4.3.5 with: - name: docker-fedora-${{ matrix.version }} + name: docker-fedora-${{ matrix.version }}-${{ matrix.version-requirement }} path: /tmp/mysqlmsn - compression-level: 9 \ No newline at end of file + compression-level: 0 \ No newline at end of file diff --git a/.github/workflows/stress.yml b/.github/workflows/stress.yml deleted file mode 100644 index 2665885a..00000000 --- a/.github/workflows/stress.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Stress Tests - -on: - push: - branches: [ main ] - pull_request: - workflow_dispatch: - -jobs: - stress: - runs-on: ${{ matrix.os }} - - strategy: - fail-fast: false - matrix: - os: [macos-latest, ubuntu-latest, windows-2019, windows-2022] - - steps: - - name: Free Disk Space (Ubuntu) - uses: jlumbroso/free-disk-space@main - if: ${{ runner.os == 'Linux' }} - with: - tool-cache: true - android: true - dotnet: true - haskell: true - large-packages: true - docker-images: true - swap-storage: false - - - name: Free Disk Space (macOS) - if: ${{ runner.os == 'macOS' }} - working-directory: /Applications - run: | - echo 'BEFORE CLEANUP:' - df -h - sudo find . -name Xcode\*.app -delete - sudo rm -rf /Users/runner/Library/Android - echo 'AFTER CLEANUP:' - df -h - - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node LTS - uses: actions/setup-node@v4 - with: - node-version: lts/* - check-latest: true - - - name: Install packages - run: npm ci - - - name: Run tests - run: npm run stress - - - name: Upload mysqlmsn directory (Windows) - if: ${{ failure() && runner.os == 'Windows' }} - uses: actions/upload-artifact@v4.3.5 - with: - name: ${{ matrix.os }} - path: "C:\\Users\\RUNNER~1\\mysqlmsn" - compression-level: 9 - - - name: Upload mysqlmsn directory (Not Windows) - if: ${{ failure() && runner.os != 'Windows' }} - uses: actions/upload-artifact@v4.3.5 - with: - name: ${{ matrix.os }} - path: /tmp/mysqlmsn - compression-level: 9 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f4c6eda..85171b60 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ There are also automated tests which you can run by running the command: npm test ``` -Please do not run the `npm run test:ci` command to test your code locally. That command is meant to only be used in GitHub Actions. +Please do not run the `npm run test:ci` or `npm run stress:ci` command to test your code locally. That command is meant to only be used in GitHub Actions. # Submitting the pull request diff --git a/README.md b/README.md index a169b24d..76278b9a 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Requirements for Linux: #### Currently supported MySQL versions -- Running MySQL versions already installed on the system: 5.7.19 and newer -- MySQL versions available to download through ```mysql-memory-server``` and run: 8.0.39 - 8.0.41, 8.1.0 - 8.3.0, 8.4.2 - 8.4.4, and 9.0.1 - 9.2.0 +- ```mysql-memory-server``` can run MySQL versions 5.7.19 and newer +- ```mysql-memory-server``` can download MySQL versions 5.7.20 - 9.2.0 ## Example Usage - Application Code diff --git a/ciSetup.js b/ciSetup.js index 73c1ccdb..aaa87092 100644 --- a/ciSetup.js +++ b/ciSetup.js @@ -1 +1,10 @@ -process.env.useCIDBPath = true; \ No newline at end of file +const { normalize } = require("path"); + +process.env.useCIDBPath = true; + +const GitHubActionsTempFolder = process.platform === 'win32' ? 'C:\\Users\\RUNNER~1\\mysqlmsn' : '/tmp/mysqlmsn' + +process.env.mysqlmsn_internal_DO_NOT_USE_databaseDirectoryPath = normalize(GitHubActionsTempFolder + '/dbs') +process.env.mysqlmsn_internal_DO_NOT_USE_binaryDirectoryPath = normalize(GitHubActionsTempFolder + '/binaries') + +process.env.mysqlmsn_internal_DO_NOT_USE_deleteDBAfterStopped = false; diff --git a/package.json b/package.json index 8bc5b585..ba4c870d 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,9 @@ "memory database" ], "scripts": { - "test": "jest --testPathIgnorePatterns=/stress-tests/ --verbose --colors", - "test:ci": "jest --testPathIgnorePatterns=/stress-tests/ --setupFilesAfterEnv ./ciSetup.js --verbose --colors", - "stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose --colors" + "test": "jest --verbose --colors", + "test:ci": "jest --setupFilesAfterEnv ./ciSetup.js --verbose --colors --runTestsByPath tests/concurrency.test.ts", + "os-compat:ci": "jest --setupFilesAfterEnv ./ciSetup.js --verbose --colors --runTestsByPath tests/versions.test.ts" }, "engines": { "node": ">=16.6.0", diff --git a/src/constants.ts b/src/constants.ts index 799c3392..58c257f9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,9 +4,7 @@ import {normalize as normalizePath} from 'path' import { tmpdir } from "os"; import { valid as validSemver, coerce as coerceSemver } from "semver"; -export const MIN_SUPPORTED_MYSQL = '5.7.19'; - -export const DEFAULT_OPTIONS_GENERATOR: () => InternalServerOptions = () => ({ +export const DEFAULT_OPTIONS: InternalServerOptions = { version: undefined, dbName: 'dbdata', logLevel: 'ERROR', @@ -21,9 +19,9 @@ export const DEFAULT_OPTIONS_GENERATOR: () => InternalServerOptions = () => ({ downloadRetries: 10, initSQLString: '', arch: process.arch -}); +} as const; -export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS_GENERATOR())) +export const DEFAULT_OPTIONS_KEYS = Object.freeze(Object.keys(DEFAULT_OPTIONS)) export const LOG_LEVELS = { 'LOG': 0, @@ -34,7 +32,7 @@ export const LOG_LEVELS = { const internalOptions = { deleteDBAfterStopped: 'true', //mysqlmsn = MySQL Memory Server Node.js - dbPath: normalizePath(`${tmpdir()}/mysqlmsn/dbs/${randomUUID().replace(/-/g, '')}`), + databaseDirectoryPath: normalizePath(`${tmpdir()}/mysqlmsn/dbs`), binaryDirectoryPath: `${tmpdir()}/mysqlmsn/binaries`, cli: 'false' } @@ -115,4 +113,83 @@ export const OPTION_TYPE_CHECKS: OptionTypeChecks = { errorMessage: `Option arch must be either of the following: ${allowedArches.join(', ')}`, definedType: 'string' } -} as const; \ No newline at end of file +} as const; + +export const MIN_SUPPORTED_MYSQL = '5.7.19'; +export const downloadsBaseURL = 'https://cdn.mysql.com//Downloads/MySQL-' +export const archiveBaseURL = 'https://cdn.mysql.com/archives/mysql-' +// Versions 8.0.29, 8.0.38, 8.4.1, and 9.0.0 have been purposefully left out of this list as MySQL has removed them from the CDN due to critical issues. +export const DOWNLOADABLE_MYSQL_VERSIONS = [ + '5.7.19', '5.7.20', '5.7.21', '5.7.22', '5.7.23', '5.7.24', '5.7.25', '5.7.26', '5.7.27', '5.7.28', '5.7.29', '5.7.30', '5.7.31', '5.7.32', '5.7.33', '5.7.34', '5.7.35', '5.7.36', '5.7.37', '5.7.38', '5.7.39', '5.7.40', '5.7.41', '5.7.42', '5.7.43', '5.7.44', + + '8.0.0', '8.0.1', '8.0.2', '8.0.3', '8.0.4', + + '8.0.11', '8.0.12', '8.0.13', '8.0.14', '8.0.15', '8.0.16', '8.0.17', '8.0.18', '8.0.19', '8.0.20', '8.0.21', '8.0.22', '8.0.23', '8.0.24', '8.0.25', '8.0.26', '8.0.27', '8.0.28', '8.0.30', '8.0.31', '8.0.32', '8.0.33', '8.0.34', '8.0.35', '8.0.36', '8.0.37', '8.0.39', '8.0.40', '8.0.41', + + '8.1.0', '8.2.0', '8.3.0', + + '8.4.0', '8.4.2', '8.4.3', '8.4.4', + + '9.0.1', '9.1.0', '9.2.0' +] as const; +export const MYSQL_ARCH_SUPPORT = { + darwin: { + arm64: '8.0.26 - 9.2.0', + x64: '5.7.19 - 9.2.0' + }, + linux: { + arm64: '8.0.31 - 9.2.0', + x64: '5.7.19 - 9.2.0' + }, + win32: { + x64: '5.7.19 - 9.2.0' + } +} as const; +export const MYSQL_MIN_OS_SUPPORT = { + win32: { + x: '0.0.0' // No minimum version is documented as far as I can tell, so allow any minimum version + }, + linux: { + x: '0.0.0'// No minimum version is documented as far as I can tell, so allow any minimum version + }, + darwin: { + '5.7.19 - 5.7.23 || 8.0.1 - 8.0.3 || 8.0.11 - 8.0.12': '16.0.0', + '5.7.24 - 5.7.29 || 8.0.4 || 8.0.13 - 8.0.18': '17.0.0', + '5.7.30 - 5.7.31 || 8.0.19 - 8.0.22': '18.0.0', + //5.7.32 - 5.7.44 is not supported for macOS by MySQL. Those versions are not appearing in this list + '8.0.0': '13.0.0', + '8.0.23 - 8.0.27': '19.0.0', + '8.0.28 - 8.0.31': '20.0.0', + '8.0.32 - 8.0.34': '21.0.0', + '8.0.35 - 8.0.39 || 8.1.0 - 8.4.2': '22.0.0', + '8.0.40 - 8.0.41 || 8.4.3 - 9.2.0': '23.0.0' + } +} as const; +export const DMR_MYSQL_VERSIONS = '8.0.0 - 8.0.2'; +export const RC_MYSQL_VERSIONS = '8.0.3 - 8.0.4'; +export const MYSQL_MACOS_VERSIONS_IN_FILENAME = { + '5.7.19 - 5.7.20 || 8.0.1 - 8.0.3': 'macos10.12', + '5.7.21 - 5.7.23 || 8.0.4 - 8.0.12': 'macos10.13', + '5.7.24 - 5.7.31 || 8.0.13 - 8.0.18': 'macos10.14', + '8.0.0': 'osx10.11', + '8.0.19 - 8.0.23': 'macos10.15', + '8.0.24 - 8.0.28': 'macos11', + '8.0.30 - 8.0.31': 'macos12', + '8.0.32 - 8.0.35 || 8.1.0 - 8.2.0': 'macos13', + '8.0.36 - 8.0.40 || 8.3.0 - 8.4.3 || 9.0.1 - 9.1.0': 'macos14', + '8.0.41 || 8.4.4 || 9.2.0': 'macos15' +} as const; +export const MYSQL_LINUX_GLIBC_VERSIONS = { + '5.7.19 - 8.0.20': '2.12', + '8.0.21 - 9.2.0': '2.17' +} as const; +export const MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE = { + '5.7.19 - 8.0.15': 'no', + '8.0.16 - 8.0.20': 'no-glibc-tag', + '8.0.21 - 9.2.0': 'glibc-tag' +} as const; +export const MYSQL_LINUX_FILE_EXTENSIONS = { + '5.7.19 - 8.0.11': 'gz', + '8.0.12 - 9.2.0': 'xz' +} as const; +export const MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS = '8.0.26'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4556a5ce..b4872d6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,16 @@ import Logger from './libraries/Logger' -import * as os from 'node:os' import Executor from "./libraries/Executor" -import { satisfies, lt, coerce } from "semver" -import { BinaryInfo, ServerOptions } from '../types' +import { satisfies, lt } from "semver" +import { BinaryInfo, InternalServerOptions, ServerOptions } from '../types' import getBinaryURL from './libraries/Version' -import MySQLVersions from './versions.json' import { downloadBinary } from './libraries/Downloader' -import { MIN_SUPPORTED_MYSQL, DEFAULT_OPTIONS_KEYS, OPTION_TYPE_CHECKS, DEFAULT_OPTIONS_GENERATOR } from './constants' +import { MIN_SUPPORTED_MYSQL, DEFAULT_OPTIONS_KEYS, OPTION_TYPE_CHECKS, DEFAULT_OPTIONS } from './constants' export async function createDB(opts?: ServerOptions) { const suppliedOpts = opts || {}; const suppliedOptsKeys = Object.keys(suppliedOpts); - const options = DEFAULT_OPTIONS_GENERATOR(); + const options: InternalServerOptions = {...DEFAULT_OPTIONS} for (const opt of suppliedOptsKeys) { if (!DEFAULT_OPTIONS_KEYS.includes(opt)) { @@ -47,28 +45,13 @@ export async function createDB(opts?: ServerOptions) { if (version === null || (options.version && !satisfies(version.version, options.version)) || unsupportedMySQLIsInstalled) { let binaryInfo: BinaryInfo; let binaryFilepath: string; - try { - binaryInfo = getBinaryURL(MySQLVersions, options.version, options) - logger.log('Using MySQL binary version:', binaryInfo.version, 'from URL:', binaryInfo.url) - } catch (e) { - if (options.version && lt(coerce(options.version), MIN_SUPPORTED_MYSQL)) { - //The difference between the throw here and the throw above is this throw is because the selected "version" is not supported. - //The throw above is because the system-installed MySQL is out of date and "ignoreUnsupportedSystemVersion" is not set to true. - throw `The selected version of MySQL (${options.version}) is not currently supported by this package. Please choose a different version to use.` - } - - logger.error(e) - if (options.version) { - throw `A MySQL version ${options.version} binary could not be found that supports your OS (${os.platform()} | ${os.version()} | ${os.release()}) and CPU architecture (${os.arch()}). Please check you have the latest version of mysql-memory-server. If the latest version still doesn't support the version you want to use, feel free to make a pull request to add support!` - } - throw `A MySQL binary could not be found that supports your OS (${os.platform()} | ${os.version()} | ${os.release()}) and CPU architecture (${os.arch()}). Please check you have the latest version of mysql-memory-server. If the latest version still doesn't support your OS and CPU architecture, feel free to make a pull request to add support!` - } + binaryInfo = getBinaryURL(options.version, options.arch) try { binaryFilepath = await downloadBinary(binaryInfo, options, logger); } catch (error) { logger.error('Failed to download binary:', error) - throw `Failed to download binary. The error was: "${error}"` + throw `Failed to download binary. The error was: "${error}"` } logger.log('Running downloaded binary') diff --git a/src/libraries/Downloader.ts b/src/libraries/Downloader.ts index 30dea449..a2072a12 100644 --- a/src/libraries/Downloader.ts +++ b/src/libraries/Downloader.ts @@ -8,19 +8,7 @@ import { randomUUID } from 'crypto'; import { execFile } from 'child_process'; import { BinaryInfo, InternalServerOptions } from '../../types'; import { lockFile, waitForLock } from './FileLock'; -import { getInternalEnvVariable } from '../constants'; - -function getZipData(entry: AdmZip.IZipEntry): Promise { - return new Promise((resolve, reject) => { - entry.getDataAsync((data, err) => { - if (err) { - reject(err) - } else { - resolve(data) - } - }) - }) -} +import { archiveBaseURL, downloadsBaseURL, getInternalEnvVariable } from '../constants'; function handleTarExtraction(filepath: string, extractedPath: string): Promise { return new Promise((resolve, reject) => { @@ -33,23 +21,6 @@ function handleTarExtraction(filepath: string, extractedPath: string): Promise { - return new Promise((resolve, reject) => { - let json = ""; - - https.get("https://github.com/Sebastian-Webster/mysql-memory-server-nodejs/raw/main/versions.json", function(response) { - response - .on("data", append => json += append ) - .on("error", e => { - reject(e) - } ) - .on("end", ()=>{ - resolve(json) - } ); - }); - }) -} - function downloadFromCDN(url: string, downloadLocation: string, logger: Logger): Promise { return new Promise(async (resolve, reject) => { if (fs.existsSync(downloadLocation)) { @@ -64,7 +35,9 @@ function downloadFromCDN(url: string, downloadLocation: string, logger: Logger): fileStream.on('open', () => { const request = https.get(url, (response) => { if (response.statusCode !== 200) { - fileStream.close((err) => { + request.destroy(); + + fileStream.end((err) => { if (err) { logger.error('An error occurred while closing the fileStream for non-200 status code. The error was:', err) } @@ -74,16 +47,17 @@ function downloadFromCDN(url: string, downloadLocation: string, logger: Logger): logger.error('An error occurred while deleting downloadLocation after non-200 status code download attempt. The error was:', rmError) } - logger.error('Received status code:', response.statusCode, 'while downloading MySQL binary.') reject(`Received status code ${response.statusCode} while downloading MySQL binary.`) }) }) } else { response.pipe(fileStream) fileStream.on('finish', () => { - if (!error) { - resolve() - } + request.end(() => { + if (!error) { + resolve() + } + }) }) } }) @@ -91,32 +65,51 @@ function downloadFromCDN(url: string, downloadLocation: string, logger: Logger): request.on('error', (err) => { error = err; logger.error(err) - fileStream.end(() => { - fs.rm(downloadLocation, {force: true}, (rmError) => { - if (rmError) { - logger.error('An error occurred while deleting downloadLocation after an error occurred with the MySQL server binary download. The error was:', rmError) - } - reject(err.message); + request.end(() => { + fileStream.end(() => { + fs.rm(downloadLocation, {force: true}, (rmError) => { + if (rmError) { + logger.error('An error occurred while deleting downloadLocation after an error occurred with the MySQL server binary download. The error was:', rmError) + } + + reject(err.message); + }) }) }) }) - }) - fileStream.on('error', (err) => { - error = err; - logger.error(err) - fileStream.end(() => { - fs.rm(downloadLocation, {force: true}, (rmError) => { - if (rmError) { - logger.error('An error occurred while deleting downloadLocation after an error occurred with the fileStream. The error was:', rmError) - } - reject(err.message) + fileStream.on('error', (err) => { + error = err; + logger.error(err) + request.end(() => { + fileStream.end(() => { + fs.rm(downloadLocation, {force: true}, (rmError) => { + if (rmError) { + logger.error('An error occurred while deleting downloadLocation after an error occurred with the fileStream. The error was:', rmError) + } + + reject(err.message) + }) + }) }) }) }) }) } +function promisifiedZipExtraction(archiveLocation: string, extractedLocation: string): Promise { + return new Promise((resolve, reject) => { + const zip = new AdmZip(archiveLocation) + zip.extractAllToAsync(extractedLocation, false, false, (err) => { + if (err) { + reject(err); + } else { + resolve() + } + }) + }) +} + function extractBinary(url: string, archiveLocation: string, extractedLocation: string, logger: Logger): Promise { return new Promise(async (resolve, reject) => { if (fs.existsSync(extractedLocation)) { @@ -134,48 +127,64 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation: return reject(`Folder name is undefined for url: ${url}`) } const folderName = mySQLFolderName.replace(`.${fileExtension}`, '') + + let extractionError: any = undefined; if (fileExtension === 'zip') { //Only Windows MySQL files use the .zip extension - const zip = new AdmZip(archiveLocation) - const entries = zip.getEntries() - for (const entry of entries) { - if (entry.entryName.indexOf('..') === -1) { - if (entry.isDirectory) { - if (entry.name === folderName) { - await fsPromises.mkdir(`${extractedLocation}/mysql`, {recursive: true}) - } else { - await fsPromises.mkdir(`${extractedLocation}/${entry.entryName}`, {recursive: true}) - } - } else { - const data = await getZipData(entry) - await fsPromises.writeFile(`${extractedLocation}/${entry.entryName}`, data) - } - } - } + try { - await fsPromises.rm(archiveLocation) + await promisifiedZipExtraction(archiveLocation, extractedLocation) } catch (e) { - logger.error('A non-fatal error occurred while removing no longer needed archive file:', e) + extractionError = e + logger.log('An error occurred while extracting the ZIP file. The error was:', e) } finally { + try { + await fsPromises.rm(archiveLocation) + } catch (e) { + logger.error('A non-fatal error occurred while deleting archive location. The error was:', e) + } + } + + if (extractionError) { + return reject(extractionError) + } + + try { await fsPromises.rename(`${extractedLocation}/${folderName}`, `${extractedLocation}/mysql`) - return resolve(normalizePath(`${extractedLocation}/mysql/bin/mysqld.exe`)) + } catch (e) { + logger.error('An error occurred while moving MySQL binary into correct folder (mysql folder). The error was:', e) + return reject(e) } - } - handleTarExtraction(archiveLocation, extractedLocation).then(async () => { + resolve(normalizePath(`${extractedLocation}/mysql/bin/mysqld.exe`)) + } else { try { - await fsPromises.rm(archiveLocation) + await handleTarExtraction(archiveLocation, extractedLocation) } catch (e) { - logger.error('A non-fatal error occurred while removing no longer needed archive file:', e) + extractionError = e + logger.error('An error occurred while extracting the tar file. Please make sure tar is installed and there is enough storage space for the extraction. The error was:', e) } finally { + try { + await fsPromises.rm(archiveLocation) + } catch (e) { + logger.error('A non-fatal error occurred while deleting archive location. The error was:', e) + } + } + + if (extractionError) { + return reject(extractionError) + } + + try { await fsPromises.rename(`${extractedLocation}/${folderName}`, `${extractedLocation}/mysql`) - resolve(`${extractedLocation}/mysql/bin/mysqld`) + } catch (e) { + logger.error('An error occurred while moving MySQL binary into correct folder (mysql folder). The error was:', e) + return reject(e) } - }).catch(error => { - logger.error(`An error occurred while extracting the tar file. Please make sure tar is installed and there is enough storage space for the extraction. The error was: ${error}`) - reject(error) - }) + + resolve(`${extractedLocation}/mysql/bin/mysqld`) + } }) } @@ -228,12 +237,17 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp //The code below only runs if the lock has been acquired by us let downloadTries = 0; + let useDownloadsURL = false; do { try { downloadTries++; - await downloadFromCDN(url, archivePath, logger) - await extractBinary(url, archivePath, extractedPath, logger) + const downloadURL = useDownloadsURL ? url.replace(archiveBaseURL, downloadsBaseURL) : url + logger.log(`Starting download for MySQL version ${version} from ${downloadURL}.`) + await downloadFromCDN(downloadURL, archivePath, logger) + logger.log(`Finished downloading MySQL version ${version} from ${downloadURL}. Now starting binary extraction.`) + await extractBinary(downloadURL, archivePath, extractedPath, logger) + logger.log(`Finished extraction for version ${version}`) break } catch (e) { //Delete generated files since either download or extraction failed @@ -246,6 +260,24 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp logger.error('An error occurred while deleting extractedPath and/or archivePath:', e) } + if (e?.includes?.('status code 404')) { + if (!useDownloadsURL) { + //Retry with downloads URL + downloadTries--; + useDownloadsURL = true; + logger.log(`Encountered error 404 when using archives URL for version ${version}. Now retrying with the downloads URL.`) + continue; + } else { + try { + await releaseFunction() + } catch (e) { + logger.error('An error occurred while releasing lock after receiving a 404 error on both downloads and archives URLs. The error was:', e) + } + + return reject(`Both URLs for MySQL version ${binaryInfo.version} returned status code 404. Aborting download.`) + } + } + if (downloadTries > options.downloadRetries) { //Only reject if we have met the downloadRetries limit try { @@ -256,13 +288,13 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp logger.error('downloadRetries have been exceeded. Aborting download.') return reject(e) } else { - console.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e) + logger.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e) } } } while (downloadTries <= options.downloadRetries) try { - releaseFunction() + await releaseFunction() } catch (e) { logger.error('An error occurred while releasing lock after successful binary download. The error was:', e) } @@ -270,6 +302,7 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp return resolve(binaryPath) } else { let downloadTries = 0; + let useDownloadsURL = false; do { const uuid = randomUUID() @@ -279,8 +312,12 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp try { downloadTries++ - await downloadFromCDN(url, zipFilepath, logger) - const binaryPath = await extractBinary(url, zipFilepath, extractedPath, logger) + const downloadURL = useDownloadsURL ? url.replace(archiveBaseURL, downloadsBaseURL) : url + logger.log(`Starting download for MySQL version ${version} from ${downloadURL}.`) + await downloadFromCDN(downloadURL, zipFilepath, logger) + logger.log(`Finished downloading MySQL version ${version} from ${downloadURL}. Now starting binary extraction.`) + const binaryPath = await extractBinary(downloadURL, zipFilepath, extractedPath, logger) + logger.log(`Finished extraction for version ${version}`) return resolve(binaryPath) } catch (e) { //Delete generated files since either download or extraction failed @@ -293,6 +330,18 @@ export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOp logger.error('An error occurred while deleting extractedPath and/or archivePath:', e) } + if (e?.includes?.('status code 404')) { + if (!useDownloadsURL) { + //Retry with downloads URL + downloadTries--; + useDownloadsURL = true; + logger.log(`Encountered error 404 when using archives URL for version ${version}. Now retrying with the downloads URL.`) + continue; + } else { + return reject(`Both URLs for MySQL version ${binaryInfo.version} returned status code 404. Aborting download.`) + } + } + if (downloadTries > options.downloadRetries) { //Only reject if we have met the downloadRetries limit return reject(e) diff --git a/src/libraries/Executor.ts b/src/libraries/Executor.ts index 6023be33..aabda158 100644 --- a/src/libraries/Executor.ts +++ b/src/libraries/Executor.ts @@ -18,6 +18,7 @@ class Executor { removeExitHandler: () => void; version: string; versionInstalledOnSystem: boolean; + databasePath: string constructor(logger: Logger) { this.logger = logger; @@ -115,6 +116,11 @@ class Executor { let resolveFunction: () => void; process.on('close', async (code, signal) => { + if (signal) { + this.logger.log('Exiting because of aborted signal.') + return + } + let errorLog: string; try { @@ -348,7 +354,13 @@ class Executor { const libaioPath = await fsPromises.realpath(libaioSymlinkPath) - const copyPath = resolvePath(`${binaryFilepath}/../../lib/private/libaio.so.1`) + let copyPath: string; + + if (lt(this.version, '8.0.18')) { + copyPath = resolvePath(`${binaryFilepath}/../../bin/libaio.so.1`) + } else { + copyPath = resolvePath(`${binaryFilepath}/../../lib/private/libaio.so.1`) + } let lockRelease: () => Promise; @@ -425,7 +437,7 @@ class Executor { let initText = `CREATE DATABASE ${options.dbName};`; if (options.username !== 'root') { - initText += `\nRENAME USER 'root'@'localhost' TO '${options.username}'@'localhost';` + initText += `\nCREATE USER '${options.username}'@'localhost';\nGRANT ALL ON *.* TO '${options.username}'@'localhost' WITH GRANT OPTION;` } if (options.initSQLString.length > 0) { @@ -434,7 +446,7 @@ class Executor { this.logger.log('Writing init file') - await fsPromises.writeFile(`${getInternalEnvVariable('dbPath')}/init.sql`, initText, {encoding: 'utf8'}) + await fsPromises.writeFile(`${this.databasePath}/init.sql`, initText, {encoding: 'utf8'}) this.logger.log('Finished writing init file') } @@ -451,7 +463,7 @@ class Executor { if (getInternalEnvVariable('deleteDBAfterStopped') === 'true') { try { - fs.rmSync(getInternalEnvVariable('dbPath'), {recursive: true, maxRetries: 50, force: true}) + fs.rmSync(this.databasePath, {recursive: true, maxRetries: 50, force: true}) } catch (e) { this.logger.error('An error occurred while deleting database directory path:', e) } @@ -473,7 +485,9 @@ class Executor { let retries = 0; - const datadir = normalizePath(`${getInternalEnvVariable('dbPath')}/data`) + this.databasePath = normalizePath(`${getInternalEnvVariable('databaseDirectoryPath')}/${randomUUID().replaceAll("-", '')}`) + + const datadir = normalizePath(`${this.databasePath}/data`) do { await this.#setupDataDirectories(options, installedMySQLBinary.path, datadir, true); @@ -485,7 +499,7 @@ class Executor { try { this.logger.log('Starting MySQL process') - const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, getInternalEnvVariable('dbPath'), installedMySQLBinary.path) + const resolved = await this.#startMySQLProcess(options, port, mySQLXPort, datadir, this.databasePath, installedMySQLBinary.path) this.logger.log('Starting process was successful') return resolved } catch (e) { diff --git a/src/libraries/LinuxOSRelease.ts b/src/libraries/LinuxOSRelease.ts new file mode 100644 index 00000000..52bee717 --- /dev/null +++ b/src/libraries/LinuxOSRelease.ts @@ -0,0 +1,17 @@ +import fs from 'fs' +import { LinuxEtcOSRelease } from '../../types' + +const releaseDetails = {} + +if (process.platform === 'linux') { + const file = fs.readFileSync('/etc/os-release', 'utf8') + const entries = file.split('\n') + for (const entry of entries) { + const [key, value] = entry.split('=') + if (typeof key === 'string' && typeof value === 'string') { + releaseDetails[key] = value.replaceAll('"', '') + } + } +} + +export default releaseDetails as LinuxEtcOSRelease; \ No newline at end of file diff --git a/src/libraries/Version.ts b/src/libraries/Version.ts index 53af00c5..a1f5d356 100644 --- a/src/libraries/Version.ts +++ b/src/libraries/Version.ts @@ -1,37 +1,111 @@ -import { InternalServerOptions, MySQLVersion } from "../../types"; +import { BinaryInfo, JSRuntimeVersion } from "../../types"; import * as os from 'os' -import { satisfies, coerce } from "semver"; +import { satisfies, coerce, lt, major, minor } from "semver"; +import { archiveBaseURL, DMR_MYSQL_VERSIONS, DOWNLOADABLE_MYSQL_VERSIONS, MYSQL_ARCH_SUPPORT, MYSQL_LINUX_FILE_EXTENSIONS, MYSQL_LINUX_GLIBC_VERSIONS, MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE, MYSQL_MACOS_VERSIONS_IN_FILENAME, MYSQL_MIN_OS_SUPPORT, RC_MYSQL_VERSIONS, MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS } from "../constants"; +import etcOSRelease from "./LinuxOSRelease"; -export default function getBinaryURL(versions: MySQLVersion[], versionToGet: string = "x", options: InternalServerOptions) { - let availableVersions = versions; +export default function getBinaryURL(versionToGet: string = "x", currentArch: string): BinaryInfo { + let selectedVersions = DOWNLOADABLE_MYSQL_VERSIONS.filter(version => satisfies(version, versionToGet)); - availableVersions = availableVersions.filter(v => v.arch === options.arch) + if (selectedVersions.length === 0) { + throw `mysql-memory-server does not support downloading a version of MySQL that fits the following version requirement: ${versionToGet}. This package only supports downloads of MySQL for MySQL >= ${DOWNLOADABLE_MYSQL_VERSIONS[0]} <= ${DOWNLOADABLE_MYSQL_VERSIONS.at(-1)}. Please check for typos, choose a different version of MySQL to use, or make an issue or pull request on GitHub if you belive this is a bug.` + } + + const currentOS = os.platform(); + const OSVersionSupport = MYSQL_MIN_OS_SUPPORT[currentOS]; + + if (!OSVersionSupport) throw `MySQL and/or mysql-memory-server does not support your operating system. Please make sure you are running the latest version of mysql-memory-server or try running on a different operating system or report an issue on GitHub if you believe this is a bug.` + + const OSSupportVersionRanges = Object.keys(OSVersionSupport); + + selectedVersions = selectedVersions.filter(possibleVersion => { + const OSKey = OSSupportVersionRanges.find(item => satisfies(possibleVersion, item)) + return !!OSKey + }) + + if (selectedVersions.length === 0) { + throw `No version of MySQL could be found that supports your operating system and fits the following version requirement: ${versionToGet}. Please check for typos, choose a different version of MySQL to run, or if you think this is a bug, please report this on GitHub.` + } - if (availableVersions.length === 0) throw `No MySQL binary could be found for your CPU architecture: ${options.arch}. ${process.platform === 'win32' && process.arch === 'arm64' ? 'This package has detected you are running Windows on ARM. MySQL does not support Windows on ARM. To get this package working, please try setting the "arch" option to "x64".' : ''}` + const archSupport = MYSQL_ARCH_SUPPORT[currentOS][currentArch] + + if (!archSupport) { + if (currentOS === 'win32' && currentArch === 'arm64') throw 'mysql-memory-server has detected you are running Windows on ARM. MySQL does not support Windows on ARM. To get this package working, please try setting the "arch" option to "x64".' + throw `MySQL and/or mysql-memory-server does not support the CPU architecture you want to use (${currentArch}). Please make sure you are using the latest version of mysql-memory-server or try using a different architecture, or if you believe this is a bug, please report this on GitHub.` + } - availableVersions = availableVersions.filter(v => v.os === process.platform) + selectedVersions = selectedVersions.filter(possibleVersion => satisfies(possibleVersion, archSupport)) - if (availableVersions.length === 0) throw `No MySQL binary could be found for your OS: ${process.platform}` + if (selectedVersions.length === 0) { + throw `No version of MySQL could be found that supports the CPU architecture ${currentArch === os.arch() ? 'for your system' : 'you have chosen'} (${currentArch}). Please try choosing a different version of MySQL, or if you believe this is a bug, please report this on GitHub.` + } - availableVersions = availableVersions.filter(v => { - const release = coerce(os.release()) - if (!release) return false - return satisfies(release.version, v.osKernelVersionsSupported) + const versionsBeforeOSVersionCheck = selectedVersions.slice() + const coercedOSRelease = coerce(os.release()) + selectedVersions = selectedVersions.filter(possibleVersion => { + const OSVersionKey = OSSupportVersionRanges.find(item => satisfies(possibleVersion, item)) + return !lt(coercedOSRelease, OSVersionSupport[OSVersionKey]) }) - if (availableVersions.length === 0) throw `No MySQL binary could be found that supports your OS version: ${os.release()} | ${os.version()}` + if (selectedVersions.length === 0) { + const versionKeys = new Set() + for (const v of versionsBeforeOSVersionCheck) { + versionKeys.add(OSSupportVersionRanges.find(item => satisfies(v, item))) + } + const minVersions = Array.from(versionKeys).map(v => OSVersionSupport[v]) + //Sorts versions in ascending order + minVersions.sort((a, b) => a < b ? -1 : 1) + const minVersion = minVersions[0] + throw `Your operating system is too out of date to run a version of MySQL that fits the following requirement: ${versionToGet}. The oldest version for your operating system that you would need to get a version that satisfies the version requirement is ${minVersion} but your current operating system is ${coercedOSRelease.version}. Please try changing your MySQL version requirement, updating your OS to a newer version, or if you believe this is a bug, please report this on GitHub.` + } - const wantedVersions = availableVersions.filter(v => satisfies(v.version, versionToGet)) + if (process.platform === 'linux' && etcOSRelease.NAME === 'Ubuntu' && etcOSRelease.VERSION_ID >= '24.04') { + //Since Ubuntu >= 24.04 uses libaio1t64 instead of libaio, this package has to copy libaio1t64 into a folder that MySQL looks in for dynamically linked libraries with the filename "libaio.so.1". + //I have not been able to find a suitable folder for libaio1t64 to be copied into for MySQL < 8.0.4, so here we are filtering all versions lower than 8.0.4 since they fail to launch in Ubuntu 24.04. + //If there is a suitable filepath for libaio1t64 to be copied into for MySQL < 8.0.4 then this check can be removed and these older MySQL versions can run on Ubuntu. + //Pull requests are welcome for adding >= Ubuntu 24.04 support for MySQL < 8.0.4. + //A way to get MySQL running on Ubuntu >= 24.04 is to symlink libaio1t64 to the location libaio would be. It is not suitable for this package to be doing that automatically, so instead this package has been copying libaio1t64 into the MySQL binary folder. + selectedVersions = selectedVersions.filter(v => !lt(v, '8.0.4')) + } - if (wantedVersions.length === 0) throw `No MySQL binary could be found that meets your version requirement: ${versionToGet} for OS ${process.platform} version ${os.release()} on arch ${process.arch}. The available versions for download are: ${availableVersions.map(v => v.version)}` + if (selectedVersions.length === 0) { + throw `You are running a version of Ubuntu that is too modern to run any MySQL versions with this package that match the following version requirement: ${versionToGet}. Please choose a newer version of MySQL to use, or if you believe this is a bug please report this on GitHub.` + } //Sorts versions in descending order - wantedVersions.sort((a, b) => a.version < b.version ? 1 : a.version === b.version ? 0 : -1) + selectedVersions.sort((a, b) => a < b ? 1 : -1) + + const selectedVersion = selectedVersions[0] - const v = wantedVersions[0] + const isRC = satisfies(selectedVersion, RC_MYSQL_VERSIONS) + const isDMR = satisfies(selectedVersion, DMR_MYSQL_VERSIONS) + + let fileLocation: string = '' + + if (currentOS === 'win32') { + fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-winx64.zip` + } else if (currentOS === 'darwin') { + const MySQLmacOSVersionNameKeys = Object.keys(MYSQL_MACOS_VERSIONS_IN_FILENAME); + const macOSVersionNameKey = MySQLmacOSVersionNameKeys.find(range => satisfies(selectedVersion, range)) + fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-${MYSQL_MACOS_VERSIONS_IN_FILENAME[macOSVersionNameKey]}-${currentArch === 'x64' ? 'x86_64' : 'arm64'}.tar.gz` + } else if (currentOS === 'linux') { + const glibcVersionKeys = Object.keys(MYSQL_LINUX_GLIBC_VERSIONS); + const glibcVersionKey = glibcVersionKeys.find(range => satisfies(selectedVersion, range)) + const glibcVersion = MYSQL_LINUX_GLIBC_VERSIONS[glibcVersionKey]; + + const minimalInstallAvailableKeys = Object.keys(MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE); + const minimalInstallAvailableKey = minimalInstallAvailableKeys.find(range => satisfies(selectedVersion, range)) + const minimalInstallAvailable = MYSQL_LINUX_MINIMAL_INSTALL_AVAILABLE[minimalInstallAvailableKey] + + const fileExtensionKeys = Object.keys(MYSQL_LINUX_FILE_EXTENSIONS); + const fileExtensionKey = fileExtensionKeys.find(range => satisfies(selectedVersion, range)) + const fileExtension = MYSQL_LINUX_FILE_EXTENSIONS[fileExtensionKey] + + fileLocation = `${major(selectedVersion)}.${minor(selectedVersion)}/mysql-${selectedVersion}${isRC ? '-rc' : isDMR ? '-dmr' : ''}-linux-${minimalInstallAvailable !== 'no-glibc-tag' ? `glibc${glibcVersion}-` : ''}${currentArch === 'x64' ? 'x86_64' : 'arm64'}${minimalInstallAvailable !== 'no' ? `-minimal${satisfies(selectedVersion, MYSQL_LINUX_MINIMAL_REBUILD_VERSIONS) ? '-rebuild' : ''}` : ''}.tar.${fileExtension}` + } return { - url: v.url, - version: v.version + version: selectedVersion, + url: archiveBaseURL + fileLocation } } \ No newline at end of file diff --git a/src/versions.json b/src/versions.json deleted file mode 100644 index aa0f48db..00000000 --- a/src/versions.json +++ /dev/null @@ -1,422 +0,0 @@ -[ - { - "version": "9.2.0", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.2/mysql-9.2.0-macos15-arm64.tar.gz" - }, - { - "version": "9.2.0", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.2/mysql-9.2.0-macos15-x86_64.tar.gz" - }, - { - "version": "9.2.0", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.2/mysql-9.2.0-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "9.2.0", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.2/mysql-9.2.0-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "9.2.0", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.2/mysql-9.2.0-winx64.zip" - }, - { - "version": "8.4.4", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.4-macos15-arm64.tar.gz" - }, - { - "version": "8.4.4", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.4-macos15-x86_64.tar.gz" - }, - { - "version": "8.4.4", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.4-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.4.4", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.4-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.4.4", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.4-winx64.zip" - }, - { - "version": "8.0.41", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.41-macos15-arm64.tar.gz" - }, - { - "version": "8.0.41", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.41-macos15-x86_64.tar.gz" - }, - { - "version": "8.0.41", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.41-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.0.41", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.41-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.0.41", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.41-winx64.zip" - }, - { - "version": "9.1.0", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.1/mysql-9.1.0-macos14-arm64.tar.gz" - }, - { - "version": "9.1.0", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.1/mysql-9.1.0-macos14-x86_64.tar.gz" - }, - { - "version": "9.1.0", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.1/mysql-9.1.0-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "9.1.0", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.1/mysql-9.1.0-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "9.1.0", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.1/mysql-9.1.0-winx64.zip" - }, - { - "version": "8.4.3", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.3-macos14-arm64.tar.gz" - }, - { - "version": "8.4.3", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.3-macos14-x86_64.tar.gz" - }, - { - "version": "8.4.3", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.3-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.4.3", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.3-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.4.3", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.3-winx64.zip" - }, - { - "version": "8.0.40", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.40-macos14-arm64.tar.gz" - }, - { - "version": "8.0.40", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.40-macos14-x86_64.tar.gz" - }, - { - "version": "8.0.40", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.40-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.0.40", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.40-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.0.40", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.40-winx64.zip" - }, - { - "version": "9.0.1", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.0/mysql-9.0.1-macos14-arm64.tar.gz" - }, - { - "version": "9.0.1", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.0/mysql-9.0.1-macos14-x86_64.tar.gz" - }, - { - "version": "9.0.1", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.0/mysql-9.0.1-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "9.0.1", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.0/mysql-9.0.1-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "9.0.1", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-9.0/mysql-9.0.1-winx64.zip" - }, - { - "version": "8.4.2", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.2-macos14-arm64.tar.gz" - }, - { - "version": "8.4.2", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.2-macos14-x86_64.tar.gz" - }, - { - "version": "8.4.2", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.2-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.4.2", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.2-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.4.2", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.4/mysql-8.4.2-winx64.zip" - }, - { - "version": "8.0.39", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.39-macos14-arm64.tar.gz" - }, - { - "version": "8.0.39", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.39-macos14-x86_64.tar.gz" - }, - { - "version": "8.0.39", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.39-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.0.39", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.39-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.0.39", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.39-winx64.zip" - }, - { - "version": "8.1.0", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.1/mysql-8.1.0-macos13-arm64.tar.gz" - }, - { - "version": "8.1.0", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.1/mysql-8.1.0-macos13-x86_64.tar.gz" - }, - { - "version": "8.1.0", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.1/mysql-8.1.0-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.1.0", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.1/mysql-8.1.0-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.1.0", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.1/mysql-8.1.0-winx64.zip" - }, - { - "version": "8.2.0", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.2/mysql-8.2.0-macos13-arm64.tar.gz" - }, - { - "version": "8.2.0", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.2/mysql-8.2.0-macos13-x86_64.tar.gz" - }, - { - "version": "8.2.0", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.2/mysql-8.2.0-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.2.0", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.2/mysql-8.2.0-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.2.0", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.2/mysql-8.2.0-winx64.zip" - }, - { - "version": "8.3.0", - "arch": "arm64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.3/mysql-8.3.0-macos14-arm64.tar.gz" - }, - { - "version": "8.3.0", - "arch": "x64", - "os": "darwin", - "osKernelVersionsSupported": ">=22", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.3/mysql-8.3.0-macos14-x86_64.tar.gz" - }, - { - "version": "8.3.0", - "arch": "x64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.3/mysql-8.3.0-linux-glibc2.17-x86_64-minimal.tar.xz" - }, - { - "version": "8.3.0", - "arch": "arm64", - "os": "linux", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.3/mysql-8.3.0-linux-glibc2.17-aarch64-minimal.tar.xz" - }, - { - "version": "8.3.0", - "arch": "x64", - "os": "win32", - "osKernelVersionsSupported": "*", - "url": "https://cdn.mysql.com//Downloads/MySQL-8.3/mysql-8.3.0-winx64.zip" - } -] \ No newline at end of file diff --git a/stress-tests/stress.test.ts b/stress-tests/stress.test.ts deleted file mode 100644 index 0f522f6b..00000000 --- a/stress-tests/stress.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {expect, test, jest} from '@jest/globals' -import { createDB } from '../src/index' -import sql from 'mysql2/promise' -import { ServerOptions } from '../types'; -import { normalize } from 'path'; - -jest.setTimeout(500_000); - -const GitHubActionsTempFolder = process.platform === 'win32' ? 'C:\\Users\\RUNNER~1\\mysqlmsn' : '/tmp/mysqlmsn' -const dbPath = normalize(GitHubActionsTempFolder + '/dbs') -const binaryPath = normalize(GitHubActionsTempFolder + '/binaries') - -for (let i = 0; i < 100; i++) { - test(`if run ${i} is successful`, async () => { - console.log('CI:', process.env.useCIDBPath) - - process.env.mysqlmsn_internal_DO_NOT_USE_deleteDBAfterStopped = String(!process.env.useCIDBPath) - - const options: ServerOptions = { - username: 'dbuser', - logLevel: 'LOG' - } - - if (process.env.useCIDBPath) { - process.env.mysqlmsn_internal_DO_NOT_USE_dbPath = `${dbPath}/${i}` - process.env.mysqlmsn_internal_DO_NOT_USE_binaryDirectoryPath = binaryPath - } - - const db = await createDB(options) - try { - const connection = await sql.createConnection({ - host: '127.0.0.1', - user: db.username, - port: db.port - }) - - const result = await connection.query('SELECT 1 + 1') - - await connection.end(); - - expect(result[0][0]['1 + 1']).toBe(2) - } finally { - await db.stop(); - } - }) -} \ No newline at end of file diff --git a/tests/concurrency.test.ts b/tests/concurrency.test.ts new file mode 100644 index 00000000..4ca457c2 --- /dev/null +++ b/tests/concurrency.test.ts @@ -0,0 +1,28 @@ +import {expect, test, jest} from '@jest/globals' +import { createDB } from '../src/index' +import sql from 'mysql2/promise' + +jest.setTimeout(500_000); + +const databaseCount = 3; + +test(`concurrency with ${databaseCount} simulataneous database creations`, async () => { + const dbs = await Promise.all( + Array.from(new Array(databaseCount)).map(() => createDB({logLevel: 'LOG'})) + ) + + for (const db of dbs) { + const connection = await sql.createConnection({ + host: '127.0.0.1', + user: db.username, + port: db.port + }) + + const result = await connection.query('SELECT 1 + 1') + + await connection.end() + await db.stop() + + expect(result[0][0]['1 + 1']).toBe(2) + } +}) \ No newline at end of file diff --git a/tests/sql.test.ts b/tests/sql.test.ts deleted file mode 100644 index e60bd45f..00000000 --- a/tests/sql.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {expect, test, jest, beforeEach, afterEach} from '@jest/globals' -import { createDB } from '../src/index' -import sql from 'mysql2/promise' -import { MySQLDB, ServerOptions } from '../types'; -import { randomUUID } from 'crypto'; -import { normalize } from 'path'; - -jest.setTimeout(500_000); - -let db: MySQLDB; - -const GitHubActionsTempFolder = process.platform === 'win32' ? 'C:\\Users\\RUNNER~1\\mysqlmsn' : '/tmp/mysqlmsn' -const dbPath = normalize(GitHubActionsTempFolder + '/dbs') -const binaryPath = normalize(GitHubActionsTempFolder + '/binaries') - -beforeEach(async () => { - process.env.mysqlmsn_internal_DO_NOT_USE_deleteDBAfterStopped = String(!process.env.useCIDBPath) - - const options: ServerOptions = { - username: 'root', - logLevel: 'LOG' - } - - if (process.env.useCIDBPath) { - process.env.mysqlmsn_internal_DO_NOT_USE_dbPath = `${dbPath}/${randomUUID()}` - process.env.mysqlmsn_internal_DO_NOT_USE_binaryDirectoryPath = binaryPath - } - - db = await createDB(options) -}) - -afterEach(async () => { - await db.stop(); -}) - -test('Runs with installed version (or downloads version if one is not available)', async () => { - Error.stackTraceLimit = Infinity - const connection = await sql.createConnection({ - host: '127.0.0.1', - user: db.username, - port: db.port - }) - - const result = await connection.query('SELECT 1 + 1') - - expect(result[0][0]['1 + 1']).toBe(2) - - await connection.end(); -}) \ No newline at end of file diff --git a/tests/versions.test.ts b/tests/versions.test.ts index 3039185c..59a1033e 100644 --- a/tests/versions.test.ts +++ b/tests/versions.test.ts @@ -1,36 +1,34 @@ import {expect, test, jest} from '@jest/globals' import { createDB } from '../src/index' import sql from 'mysql2/promise' -import { coerce } from 'semver'; -import { randomUUID } from 'crypto'; +import { coerce, satisfies } from 'semver'; import { ServerOptions } from '../types'; -import { normalize } from 'path'; +import getBinaryURL from '../src/libraries/Version'; +import { DOWNLOADABLE_MYSQL_VERSIONS } from '../src/constants'; -const versions = ['8.0.39', '8.0.40', '8.0.41', '8.1.0', '8.2.0', '8.3.0', '8.4.2', '8.4.3', '8.4.4', '9.0.1', '9.1.0', '9.2.0'] const usernames = ['root', 'dbuser'] -const GitHubActionsTempFolder = process.platform === 'win32' ? 'C:\\Users\\RUNNER~1\\mysqlmsn' : '/tmp/mysqlmsn' -const dbPath = normalize(GitHubActionsTempFolder + '/dbs') -const binaryPath = normalize(GitHubActionsTempFolder + '/binaries') +jest.setTimeout(500_000); //5 minutes -jest.setTimeout(500_000); +const arch = process.arch === 'x64' || (process.platform === 'win32' && process.arch === 'arm64') ? 'x64' : 'arm64'; + +for (const version of DOWNLOADABLE_MYSQL_VERSIONS.filter(v => satisfies(v, process.env.VERSION_REQUIREMENT || '>0.0.0'))) { + try { + getBinaryURL(version, arch) + } catch (e) { + console.warn(`Skipping version ${version} because the version is not supported on this system. The reason given from getBinaryURL was: ${e}`) + continue + } -for (const version of versions) { for (const username of usernames) { test(`running on version ${version} with username ${username}`, async () => { - process.env.mysqlmsn_internal_DO_NOT_USE_deleteDBAfterStopped = String(!process.env.useCIDBPath) - const options: ServerOptions = { version, dbName: 'testingdata', username: username, logLevel: 'LOG', - initSQLString: 'CREATE DATABASE mytestdb;' - } - - if (process.env.useCIDBPath) { - process.env.mysqlmsn_internal_DO_NOT_USE_dbPath = `${dbPath}/${randomUUID()}` - process.env.mysqlmsn_internal_DO_NOT_USE_binaryDirectoryPath = binaryPath + initSQLString: 'CREATE DATABASE mytestdb;', + arch } const db = await createDB(options) @@ -48,7 +46,13 @@ for (const version of versions) { await connection.end(); await db.stop(); - expect(coerce(mySQLVersion)?.version).toBe(version) + expect(satisfies(coerce(mySQLVersion) || 'error', version)).toBe(true) }) } -} \ No newline at end of file +} + +//The test suites will fail if there aren't any tests. Since we're skipping creating tests if the test platform doesn't support the MySQL +//binary, we need this test here just in case all the MySQL binaries are skipped +test('dummy test', () => { + expect(1 + 1).toBe(2) +}) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 3440b0d7..3dda31ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2019", + "target": "es2021", "declaration": true, "outDir": "./dist", "resolveJsonModule": true, diff --git a/types/index.ts b/types/index.ts index 6038dbee..071e359f 100644 --- a/types/index.ts +++ b/types/index.ts @@ -36,10 +36,6 @@ export type InternalServerOptions = { arch: string } -export type ExecutorOptions = { - logLevel: LOG_LEVEL -} - export type ExecuteFileReturn = { error: ExecFileException | null, stdout: string, @@ -60,14 +56,6 @@ export type MySQLDB = { stop: () => Promise } -export type MySQLVersion = { - version: string, - arch: string, - os: string, - osKernelVersionsSupported: string, - url: string -} - export type DownloadedMySQLVersion = { version: string, path: string, @@ -85,4 +73,20 @@ export type OptionTypeChecks = { errorMessage: string, definedType: "string" | "boolean" | "number" } +} + +export type LinuxEtcOSRelease = { + PRETTY_NAME?: string, + NAME?: string, + VERSION_ID?: string, + VERSION?: string, + VERSION_CODENAME?: string, + ID?: string, + ID_LIKE?: string, + UBUNTU_CODENAME?: string +} + +export type JSRuntimeVersion = { + runtimeName: string, + runtimeVersion: string } \ No newline at end of file