Skip to content

Commit f5454e6

Browse files
Merge pull request #56 from Sebastian-Webster/add-ubuntu-24-support
Add Ubuntu 24.04 and newer support
2 parents 0e48b9e + 8d44621 commit f5454e6

File tree

6 files changed

+159
-50
lines changed

6 files changed

+159
-50
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
16+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
1717
# We only support Node.js 16 and newer
1818
node-version: [16.6.0, 16.x, 17.0.0, 17.x, 18.x, 19.x, 20.x, 21.x, 22.x]
1919

@@ -52,7 +52,7 @@ jobs:
5252
strategy:
5353
fail-fast: false
5454
matrix:
55-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
55+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
5656
bun-version: [1.1.22]
5757

5858
steps:
@@ -91,7 +91,7 @@ jobs:
9191
strategy:
9292
fail-fast: false
9393
matrix:
94-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
94+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
9595

9696
steps:
9797
- name: Checkout
@@ -115,7 +115,7 @@ jobs:
115115
strategy:
116116
fail-fast: false
117117
matrix:
118-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
118+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
119119

120120
steps:
121121
- name: Checkout
@@ -139,7 +139,7 @@ jobs:
139139
strategy:
140140
fail-fast: false
141141
matrix:
142-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
142+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
143143

144144
steps:
145145
- name: Free Disk Space (Ubuntu)
@@ -191,7 +191,7 @@ jobs:
191191
strategy:
192192
fail-fast: false
193193
matrix:
194-
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04]
194+
os: [windows-2019, windows-2022, macos-13, macos-14, ubuntu-20.04, ubuntu-22.04, ubuntu-24.04]
195195

196196
steps:
197197
- name: Free Disk Space (Ubuntu)

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ Download with your package manager of choice. The package name is `mysql-memory-
1111
#### Requirements
1212

1313
- Node.js 16.6.0 and newer
14-
- macOS 13+, Windows, or Linux (Only Ubuntu has been tested. Other Linux distributions may or may not work at this time. Ubuntu 24.04 and newer is not fully supported at this time - go to the bottom of this file to learn more)
14+
- macOS 13+, Windows, or Linux (Only Ubuntu has been tested. Other Linux distributions may or may not work at this time.)
1515

1616
Windows only requirements:
1717
- `Microsoft Visual C++ 2019 Redistributable Package` needs to be installed
1818

1919
Linux only requirements:
20-
- The `libaio1` package needs to be installed
21-
- The `tar` package needs to be installed
20+
- The `libaio1` or `libaio1t64` package needs to be installed
21+
- If `libaio1` is not available but `libaio1t64` is, the `ldconfig` command needs to be available to run
22+
- The `tar` package needs to be installed if you want to use MySQL versions that aren't system installed
2223

2324
Currently supported MySQL versions:
2425
- If using the system installed MySQL server: 8.0.20 and newer
@@ -169,7 +170,3 @@ Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory
169170
Gotchas: This option is intended to be for internal debugging purposes only and not meant for people to use. As such, this option will not follow Semantic Versioning.
170171

171172
Description: The folder to store database-related data in
172-
173-
## If using Ubuntu 24.04 and newer
174-
175-
Selecting what MySQL version to use is not currently supported on Ubuntu 24.04 and newer. To use this package on Ubuntu 24.04 and newer you must have the `mysql-server` package installed on your system and `ServerOptions.version` must either be the version that is installed on the system or undefined.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
"memory database"
1717
],
1818
"scripts": {
19-
"test": "jest --testPathIgnorePatterns=/stress-tests/",
20-
"test:ci": "jest --testPathIgnorePatterns=/stress-tests/ --setupFilesAfterEnv ./ciSetup.js",
21-
"stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js"
19+
"test": "jest --testPathIgnorePatterns=/stress-tests/ --verbose",
20+
"test:ci": "jest --testPathIgnorePatterns=/stress-tests/ --setupFilesAfterEnv ./ciSetup.js --verbose",
21+
"stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose"
2222
},
2323
"author": "Sebastian-Webster",
2424
"license": "MIT",

src/libraries/Downloader.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import AdmZip from 'adm-zip'
77
import { normalize as normalizePath } from 'path';
88
import { randomUUID } from 'crypto';
99
import { exec } from 'child_process';
10-
import { lockSync, checkSync, unlockSync } from 'proper-lockfile';
10+
import { lockSync, unlockSync } from 'proper-lockfile';
1111
import { BinaryInfo, InternalServerOptions } from '../../types';
12+
import { waitForLock } from './FileLock';
1213

1314
function getZipData(entry: AdmZip.IZipEntry): Promise<Buffer> {
1415
return new Promise((resolve, reject) => {
@@ -140,26 +141,6 @@ function extractBinary(url: string, archiveLocation: string, extractedLocation:
140141
})
141142
}
142143

143-
function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
144-
return new Promise(async (resolve, reject) => {
145-
let retries = 0;
146-
while (retries <= options.lockRetries) {
147-
retries++
148-
try {
149-
const locked = checkSync(path);
150-
if (!locked) {
151-
return resolve()
152-
} else {
153-
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
154-
}
155-
} catch (e) {
156-
return reject(e)
157-
}
158-
}
159-
reject(`lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} milliseconds.`)
160-
})
161-
}
162-
163144
export function downloadBinary(binaryInfo: BinaryInfo, options: InternalServerOptions, logger: Logger): Promise<string> {
164145
return new Promise(async (resolve, reject) => {
165146
const {url, version} = binaryInfo;

src/libraries/Executor.ts

Lines changed: 122 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ChildProcess, exec, spawn } from "child_process"
1+
import { ChildProcess, exec, execFile, spawn } from "child_process"
22
import {coerce, satisfies} from 'semver';
33
import * as os from 'os'
44
import * as fsPromises from 'fs/promises';
@@ -7,7 +7,9 @@ import Logger from "./Logger";
77
import { GenerateRandomPort } from "./Port";
88
import DBDestroySignal from "./AbortSignal";
99
import { ExecuteReturn, InstalledMySQLVersion, InternalServerOptions, MySQLDB } from "../../types";
10-
import {normalize as normalizePath} from 'path'
10+
import {normalize as normalizePath, resolve as resolvePath} from 'path'
11+
import { lockSync, unlockSync } from 'proper-lockfile';
12+
import { waitForLock } from "./FileLock";
1113

1214
class Executor {
1315
logger: Logger;
@@ -24,6 +26,14 @@ class Executor {
2426
})
2527
}
2628

29+
#executeFile(command: string, args: string[], cwd: string): Promise<{stdout: string, stderr: string}> {
30+
return new Promise(resolve => {
31+
execFile(command, args, {signal: DBDestroySignal.signal, cwd}, (error, stdout, stderr) => {
32+
resolve({stdout, stderr: error?.message || stderr})
33+
})
34+
})
35+
}
36+
2737
async #killProcess(process: ChildProcess): Promise<boolean> {
2838
let killed = false;
2939
if (os.platform() === 'win32') {
@@ -246,32 +256,130 @@ class Executor {
246256
})
247257
}
248258

249-
async #setupDataDirectories(options: InternalServerOptions, binaryFilepath: string, datadir: string): Promise<void> {
259+
async #setupDataDirectories(options: InternalServerOptions, binaryFilepath: string, datadir: string, retry: boolean): Promise<void> {
250260
this.logger.log('Created data directory for database at:', datadir)
251261
await fsPromises.mkdir(datadir, {recursive: true})
252262

253-
const {error: err, stderr} = await this.#execute(`"${binaryFilepath}" --no-defaults --datadir=${datadir} --initialize-insecure`)
263+
let stderr: string;
264+
265+
if (binaryFilepath === 'mysqld') {
266+
const {error, stderr: output} = await this.#execute(`mysqld --no-defaults --datadir=${datadir} --initialize-insecure`)
267+
stderr = output
268+
if (error) {
269+
this.logger.error('An error occurred while initializing database with system-installed MySQL:', error)
270+
throw 'An error occurred while initializing database with system-installed MySQL. Please check the console for more information.'
271+
}
272+
} else {
273+
let result: {stderr: string, stdout: string};
274+
try {
275+
result = await this.#executeFile(`${binaryFilepath}`, [`--no-defaults`, `--datadir=${datadir}`, `--initialize-insecure`], resolvePath(`${binaryFilepath}/..`))
276+
} catch (e) {
277+
this.logger.error('Error occurred from executeFile:', e)
278+
throw e
279+
}
280+
stderr = result?.stderr
281+
}
254282

255-
if (err || (stderr && !stderr.includes('InnoDB initialization has ended'))) {
256-
if (process.platform === 'win32' && (err?.message.includes('Command failed') || stderr.includes('Command failed'))) {
257-
this.logger.error(err || stderr)
283+
if (stderr && !stderr.includes('InnoDB initialization has ended')) {
284+
if (process.platform === 'win32' && stderr.includes('Command failed')) {
285+
this.logger.error(stderr)
258286
throw 'The mysqld command failed to run. A possible cause is that the Microsoft Visual C++ Redistributable Package is not installed. MySQL 5.7.40 and newer requires Microsoft Visual C++ Redistributable Package 2019 to be installed. Check the MySQL docs for Microsoft Visual C++ requirements for other MySQL versions. If you are sure you have this installed, check the error message in the console for more details.'
259287
}
260288

261-
if (process.platform === 'linux' && (err?.message.includes('libaio.so') || stderr.includes('libaio.so'))) {
262-
this.logger.error(err || stderr)
263-
throw 'The mysqld command failed to run. MySQL needs the libaio package installed on Linux systems to run. Do you have this installed? Learn more at https://dev.mysql.com/doc/refman/en/binary-installation.html'
289+
if (process.platform === 'linux' && stderr.includes('libaio.so')) {
290+
if (binaryFilepath === 'mysqld') {
291+
throw 'libaio could not be found while running system-installed MySQL. libaio must be installed on this system for MySQL to run. To learn more, please check out https://dev.mysql.com/doc/refman/en/binary-installation.html'
292+
}
293+
294+
if (retry === false) {
295+
this.logger.error('An error occurred while initializing database:', stderr)
296+
throw 'Tried to copy libaio into lib folder and MySQL is still failing to initialize. Please check the console for more information.'
297+
}
298+
299+
if (binaryFilepath.slice(-16) === 'mysql/bin/mysqld') {
300+
const {error: lderror, stdout, stderr: ldstderr} = await this.#execute('ldconfig -p')
301+
if (lderror || ldstderr) {
302+
this.logger.error('The following libaio error occurred:', stderr)
303+
this.logger.error('After the libaio error, an ldconfig error occurred:', lderror || ldstderr)
304+
throw 'The ldconfig command failed to run. This command was ran to find libaio because libaio could not be found on the system. libaio is needed for MySQL to run. Do you have ldconfig and libaio installed? Learn more about libaio at Learn more at https://dev.mysql.com/doc/refman/en/binary-installation.html'
305+
}
306+
const libaioFound = stdout.split('\n').filter(lib => lib.includes('libaio.so.1t64'))
307+
if (!libaioFound.length) {
308+
this.logger.error('Error from launching MySQL:', stderr)
309+
throw 'An error occurred while launching MySQL. The most likely cause is that libaio1 and libaio1t64 could not be found. Either libaio1 or libaio1t64 must be installed on this system for MySQL to run. To learn more, please check out https://dev.mysql.com/doc/refman/en/binary-installation.html. Check error in console for more information.'
310+
}
311+
const libaioEntry = libaioFound[0]
312+
const libaioPathIndex = libaioEntry.indexOf('=>')
313+
const libaioSymlinkPath = libaioEntry.slice(libaioPathIndex + 3)
314+
315+
const libaioPath = await fsPromises.realpath(libaioSymlinkPath)
316+
317+
const copyPath = resolvePath(`${binaryFilepath}/../../lib/private/libaio.so.1`)
318+
319+
try {
320+
lockSync(copyPath, {realpath: false})
321+
322+
this.logger.log('libaio copy path:', copyPath, '| libaio symlink path:', libaioSymlinkPath, '| libaio actual path:', libaioPath)
323+
let copyError: Error;
324+
325+
try {
326+
await fsPromises.copyFile(libaioPath, copyPath)
327+
} catch (e) {
328+
copyError = e
329+
this.logger.error('An error occurred while copying libaio1t64 to lib folder:', e)
330+
331+
try {
332+
await fsPromises.rm(copyPath, {force: true})
333+
} catch (e) {
334+
this.logger.error('An error occurred while deleting libaio file:', e)
335+
}
336+
} finally {
337+
338+
try {
339+
unlockSync(copyPath, {realpath: false})
340+
} catch (e) {
341+
this.logger.error('Error unlocking libaio file:', e)
342+
}
343+
344+
if (copyError) {
345+
throw 'An error occurred while copying libaio1t64 to the MySQL lib folder. Please check the console for more details.'
346+
}
347+
348+
//Retry setting up directory now that libaio has been copied
349+
this.logger.log('Retrying directory setup')
350+
await this.deleteDatabaseDirectory(datadir)
351+
await this.#setupDataDirectories(options, binaryFilepath, datadir, false)
352+
return
353+
}
354+
} catch (error) {
355+
if (String(error) === 'Error: Lock file is already being held') {
356+
this.logger.log('Waiting for lock for libaio copy')
357+
await waitForLock(copyPath, options)
358+
this.logger.log('Lock is gone for libaio copy')
359+
}
360+
this.logger.error('An error occurred from locking libaio section:', error)
361+
throw error
362+
}
363+
} else {
364+
throw 'Cannot recognize file structure for the MySQL binary folder. This was caused by not being able to find libaio. Try installing libaio. Learn more at https://dev.mysql.com/doc/refman/en/binary-installation.html'
365+
}
264366
}
265-
throw err || stderr
367+
throw stderr
266368
}
267369

370+
this.logger.log('Creating init text')
371+
268372
let initText = `CREATE DATABASE ${options.dbName};`;
269373

270374
if (options.username !== 'root') {
271375
initText += `\nRENAME USER 'root'@'localhost' TO '${options.username}'@'localhost';`
272376
}
273377

378+
this.logger.log('Writing init file')
379+
274380
await fsPromises.writeFile(`${options.dbPath}/init.sql`, initText, {encoding: 'utf8'})
381+
382+
this.logger.log('Finished writing init file')
275383
}
276384

277385
async startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise<MySQLDB> {
@@ -280,7 +388,8 @@ class Executor {
280388
const datadir = normalizePath(`${options.dbPath}/data`)
281389

282390
do {
283-
await this.#setupDataDirectories(options, binaryFilepath, datadir);
391+
await this.#setupDataDirectories(options, binaryFilepath, datadir, true);
392+
this.logger.log('Setting up directories was successful')
284393

285394
const port = GenerateRandomPort()
286395
const mySQLXPort = GenerateRandomPort();
@@ -308,4 +417,4 @@ class Executor {
308417
}
309418
}
310419

311-
export default Executor
420+
export default Executor

src/libraries/FileLock.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { checkSync } from "proper-lockfile";
2+
import { InternalServerOptions } from "../../types";
3+
4+
export function waitForLock(path: string, options: InternalServerOptions): Promise<void> {
5+
return new Promise(async (resolve, reject) => {
6+
let retries = 0;
7+
while (retries <= options.lockRetries) {
8+
retries++
9+
try {
10+
const locked = checkSync(path);
11+
if (!locked) {
12+
return resolve()
13+
} else {
14+
await new Promise(resolve => setTimeout(resolve, options.lockRetryWait))
15+
}
16+
} catch (e) {
17+
return reject(e)
18+
}
19+
}
20+
reject(`lockRetries has been exceeded. Lock had not been released after ${options.lockRetryWait} * ${options.lockRetries} milliseconds.`)
21+
})
22+
}

0 commit comments

Comments
 (0)