mysql-memory-server 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -7
- package/dist/src/index.js +4 -2
- package/dist/src/libraries/Downloader.js +126 -53
- package/dist/src/libraries/Executor.d.ts +0 -1
- package/dist/src/libraries/Executor.js +64 -54
- package/dist/src/libraries/FileLock.js +1 -1
- package/dist/src/libraries/Logger.js +3 -3
- package/dist/types/index.d.ts +7 -3
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Download with your package manager of choice. The package name is `mysql-memory-
|
|
|
10
10
|
|
|
11
11
|
#### Requirements
|
|
12
12
|
|
|
13
|
-
- Node.js 16.6.0
|
|
13
|
+
- Node.js >=16.6.0 or Bun >= 1.0.0
|
|
14
14
|
- macOS 13+, Windows, or Linux (This package is only tested on Ubuntu 20.04, 22.04, 24.04, and Fedora 40. Other Linux distributions may or may not work at this time.)
|
|
15
15
|
|
|
16
16
|
#### Requirements for downloaded MySQL versions
|
|
@@ -54,7 +54,7 @@ const connection = await sql.createConnection({
|
|
|
54
54
|
host: '127.0.0.1',
|
|
55
55
|
user: db.username,
|
|
56
56
|
port: db.port,
|
|
57
|
-
database: db.dbName
|
|
57
|
+
database: db.dbName,
|
|
58
58
|
password: ''
|
|
59
59
|
})
|
|
60
60
|
|
|
@@ -186,22 +186,39 @@ Default: false
|
|
|
186
186
|
|
|
187
187
|
Description: This option only applies if the system-installed MySQL version is lower than the oldest supported MySQL version for this package (8.0.20) and the `version` option is not defined. If set to `true`, this package will use the latest version of MySQL instead of the system-installed version. If `false`, the package will throw an error.
|
|
188
188
|
|
|
189
|
+
- `downloadRetries: number`
|
|
190
|
+
|
|
191
|
+
Required: No
|
|
192
|
+
|
|
193
|
+
Default: 3
|
|
194
|
+
|
|
195
|
+
Description: The number of times to try to download a MySQL binary before giving up and rejecting the `createDB()` promise.
|
|
196
|
+
|
|
197
|
+
***
|
|
198
|
+
### :warning: Internal Options :warning:
|
|
199
|
+
|
|
200
|
+
The following options are only meant for internal debugging use. Their behaviour may change or they may get removed between major/minor/patch versions and they are not to be considered stable. The options below will not follow Semantic Versioning so it is advised to not use them.
|
|
201
|
+
|
|
189
202
|
- `deleteDBAfterStopped: boolean`
|
|
190
203
|
|
|
191
204
|
Required: No
|
|
192
205
|
|
|
193
206
|
Default: true
|
|
194
207
|
|
|
195
|
-
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.
|
|
196
|
-
|
|
197
208
|
Description: Changes whether or not the database will be deleted after it has been stopped. If set to `true`, the database WILL be deleted after it has been stopped.
|
|
198
209
|
|
|
199
210
|
- `dataPath: string`
|
|
200
211
|
|
|
201
212
|
Required: No
|
|
202
213
|
|
|
203
|
-
Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory and UUID with a UUIDv4 without seperating dashes)
|
|
204
|
-
|
|
205
|
-
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.
|
|
214
|
+
Default: `TMPDIR/mysqlmsn/dbs/UUID` (replacing TMPDIR with the OS temp directory and UUID with a UUIDv4 without seperating dashes).
|
|
206
215
|
|
|
207
216
|
Description: The folder to store database-related data in
|
|
217
|
+
|
|
218
|
+
- `binaryDirectoryPath: string`
|
|
219
|
+
|
|
220
|
+
Required: No
|
|
221
|
+
|
|
222
|
+
Default: `TMPDIR/mysqlmsn/binaries` (replacing TMPDIR with the OS temp directory)
|
|
223
|
+
|
|
224
|
+
Description: The folder to store the MySQL binaries when they are downloaded from the CDN.
|
package/dist/src/index.js
CHANGED
|
@@ -51,7 +51,9 @@ async function createDB(opts) {
|
|
|
51
51
|
dbPath: (0, path_1.normalize)(`${os.tmpdir()}/mysqlmsn/dbs/${(0, crypto_1.randomUUID)().replace(/-/g, '')}`),
|
|
52
52
|
ignoreUnsupportedSystemVersion: false,
|
|
53
53
|
port: 0,
|
|
54
|
-
xPort: 0
|
|
54
|
+
xPort: 0,
|
|
55
|
+
binaryDirectoryPath: `${os.tmpdir()}/mysqlmsn/binaries`,
|
|
56
|
+
downloadRetries: 10
|
|
55
57
|
};
|
|
56
58
|
const options = { ...defaultOptions, ...opts };
|
|
57
59
|
const logger = new Logger_1.default(options.logLevel);
|
|
@@ -73,7 +75,7 @@ async function createDB(opts) {
|
|
|
73
75
|
let binaryFilepath;
|
|
74
76
|
try {
|
|
75
77
|
binaryInfo = (0, Version_1.default)(versions_json_1.default, options.version);
|
|
76
|
-
logger.log('
|
|
78
|
+
logger.log('Using MySQL binary version:', binaryInfo.version, 'from URL:', binaryInfo.url);
|
|
77
79
|
}
|
|
78
80
|
catch (e) {
|
|
79
81
|
logger.error(e);
|
|
@@ -31,7 +31,6 @@ exports.downloadBinary = downloadBinary;
|
|
|
31
31
|
const https = __importStar(require("https"));
|
|
32
32
|
const fs = __importStar(require("fs"));
|
|
33
33
|
const fsPromises = __importStar(require("fs/promises"));
|
|
34
|
-
const os = __importStar(require("os"));
|
|
35
34
|
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
36
35
|
const path_1 = require("path");
|
|
37
36
|
const crypto_1 = require("crypto");
|
|
@@ -52,7 +51,7 @@ function getZipData(entry) {
|
|
|
52
51
|
}
|
|
53
52
|
function handleTarExtraction(filepath, extractedPath) {
|
|
54
53
|
return new Promise((resolve, reject) => {
|
|
55
|
-
(0, child_process_1.
|
|
54
|
+
(0, child_process_1.execFile)(`tar`, ['-xf', filepath, '-C', extractedPath], (error, stdout, stderr) => {
|
|
56
55
|
if (error || stderr) {
|
|
57
56
|
return reject(error || stderr);
|
|
58
57
|
}
|
|
@@ -76,39 +75,71 @@ function downloadVersions() {
|
|
|
76
75
|
});
|
|
77
76
|
}
|
|
78
77
|
function downloadFromCDN(url, downloadLocation, logger) {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
78
|
+
return new Promise(async (resolve, reject) => {
|
|
79
|
+
if (fs.existsSync(downloadLocation)) {
|
|
80
|
+
logger.warn('Removing item at downloadLocation:', downloadLocation, 'so the MySQL binary archive can be stored there. This is probably because a previous download/extraction failed.');
|
|
81
|
+
await fsPromises.rm(downloadLocation, { recursive: true, force: true });
|
|
82
|
+
}
|
|
80
83
|
const fileStream = fs.createWriteStream(downloadLocation);
|
|
81
84
|
let error;
|
|
82
85
|
fileStream.on('open', () => {
|
|
83
86
|
const request = https.get(url, (response) => {
|
|
84
|
-
response.
|
|
87
|
+
if (response.statusCode !== 200) {
|
|
88
|
+
fileStream.close((err) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
logger.error('An error occurred while closing the fileStream for non-200 status code. The error was:', err);
|
|
91
|
+
}
|
|
92
|
+
fs.unlink(downloadLocation, (unlinkErr) => {
|
|
93
|
+
if (unlinkErr) {
|
|
94
|
+
logger.error('An error occurred while deleting downloadLocation after non-200 status code download attempt. The error was:', err);
|
|
95
|
+
}
|
|
96
|
+
logger.error('Received status code:', response.statusCode, 'while downloading MySQL binary.');
|
|
97
|
+
reject(`Received status code ${response.statusCode} while downloading MySQL binary.`);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
response.pipe(fileStream);
|
|
103
|
+
fileStream.on('finish', () => {
|
|
104
|
+
if (!error) {
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
85
109
|
});
|
|
86
110
|
request.on('error', (err) => {
|
|
87
111
|
error = err;
|
|
88
112
|
logger.error(err);
|
|
89
|
-
fileStream.
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
fileStream.end(() => {
|
|
114
|
+
fs.unlink(downloadLocation, (unlinkError) => {
|
|
115
|
+
if (unlinkError) {
|
|
116
|
+
logger.error('An error occurred while deleting downloadLocation after an error occurred with the MySQL server binary download. The error was:', unlinkError);
|
|
117
|
+
}
|
|
118
|
+
reject(err.message);
|
|
119
|
+
});
|
|
92
120
|
});
|
|
93
121
|
});
|
|
94
122
|
});
|
|
95
|
-
fileStream.on('finish', () => {
|
|
96
|
-
if (!error) {
|
|
97
|
-
resolve();
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
123
|
fileStream.on('error', (err) => {
|
|
101
124
|
error = err;
|
|
102
125
|
logger.error(err);
|
|
103
|
-
fileStream.end()
|
|
104
|
-
|
|
105
|
-
|
|
126
|
+
fileStream.end(() => {
|
|
127
|
+
fs.unlink(downloadLocation, (unlinkError) => {
|
|
128
|
+
if (unlinkError) {
|
|
129
|
+
logger.error('An error occurred while deleting downloadLocation after an error occurred with the fileStream. The error was:', unlinkError);
|
|
130
|
+
}
|
|
131
|
+
reject(err.message);
|
|
132
|
+
});
|
|
106
133
|
});
|
|
107
134
|
});
|
|
108
135
|
});
|
|
109
136
|
}
|
|
110
137
|
function extractBinary(url, archiveLocation, extractedLocation, logger) {
|
|
111
138
|
return new Promise(async (resolve, reject) => {
|
|
139
|
+
if (fs.existsSync(extractedLocation)) {
|
|
140
|
+
logger.warn('Removing item at extractedLocation:', extractedLocation, 'so the MySQL binary can be stored there. This is probably because a previous download/extraction failed.');
|
|
141
|
+
await fsPromises.rm(extractedLocation, { recursive: true, force: true });
|
|
142
|
+
}
|
|
112
143
|
const lastDashIndex = url.lastIndexOf('-');
|
|
113
144
|
const fileExtension = url.slice(lastDashIndex).split('.').splice(1).join('.');
|
|
114
145
|
await fsPromises.mkdir(extractedLocation, { recursive: true });
|
|
@@ -160,83 +191,125 @@ function extractBinary(url, archiveLocation, extractedLocation, logger) {
|
|
|
160
191
|
resolve(`${extractedLocation}/mysql/bin/mysqld`);
|
|
161
192
|
}
|
|
162
193
|
}).catch(error => {
|
|
163
|
-
|
|
194
|
+
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}`);
|
|
195
|
+
reject(error);
|
|
164
196
|
});
|
|
165
197
|
});
|
|
166
198
|
}
|
|
167
199
|
function downloadBinary(binaryInfo, options, logger) {
|
|
168
200
|
return new Promise(async (resolve, reject) => {
|
|
169
201
|
const { url, version } = binaryInfo;
|
|
170
|
-
const dirpath =
|
|
202
|
+
const dirpath = options.binaryDirectoryPath;
|
|
171
203
|
logger.log('Binary path:', dirpath);
|
|
172
204
|
await fsPromises.mkdir(dirpath, { recursive: true });
|
|
173
205
|
const lastDashIndex = url.lastIndexOf('-');
|
|
174
206
|
const fileExtension = url.slice(lastDashIndex).split('.').splice(1).join('.');
|
|
175
207
|
if (options.downloadBinaryOnce) {
|
|
176
208
|
const extractedPath = `${dirpath}/${version}`;
|
|
177
|
-
await fsPromises.mkdir(extractedPath, { recursive: true });
|
|
178
209
|
const binaryPath = (0, path_1.normalize)(`${extractedPath}/mysql/bin/mysqld${process.platform === 'win32' ? '.exe' : ''}`);
|
|
179
210
|
const archivePath = `${dirpath}/${version}.${fileExtension}`;
|
|
180
211
|
const binaryExists = fs.existsSync(binaryPath);
|
|
181
212
|
if (binaryExists) {
|
|
182
213
|
return resolve(binaryPath);
|
|
183
214
|
}
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
(0, proper_lockfile_1.lockSync)(extractedPath);
|
|
187
|
-
lockedByUs = true;
|
|
188
|
-
await downloadFromCDN(url, archivePath, logger);
|
|
189
|
-
await extractBinary(url, archivePath, extractedPath, logger);
|
|
215
|
+
let releaseFunction;
|
|
216
|
+
while (true) {
|
|
190
217
|
try {
|
|
191
|
-
(0, proper_lockfile_1.
|
|
218
|
+
releaseFunction = (0, proper_lockfile_1.lockSync)(extractedPath, { realpath: false });
|
|
219
|
+
break;
|
|
192
220
|
}
|
|
193
221
|
catch (e) {
|
|
222
|
+
if (e.code === 'ELOCKED') {
|
|
223
|
+
logger.log('Waiting for lock for MySQL version', version);
|
|
224
|
+
await (0, FileLock_1.waitForLock)(extractedPath, options);
|
|
225
|
+
logger.log('Lock is gone for version', version);
|
|
226
|
+
//If the binary does not exist after lock has been released (like if the download fails and the binary got deleted as a result)
|
|
227
|
+
//then the lock acquisition process should start again
|
|
228
|
+
const binaryExists = fs.existsSync(binaryPath);
|
|
229
|
+
if (!binaryExists)
|
|
230
|
+
continue;
|
|
231
|
+
return resolve(binaryPath);
|
|
232
|
+
}
|
|
194
233
|
return reject(e);
|
|
195
234
|
}
|
|
196
|
-
return resolve(binaryPath);
|
|
197
235
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
//The code below only runs if the lock has been acquired by us
|
|
237
|
+
let downloadTries = 0;
|
|
238
|
+
do {
|
|
239
|
+
try {
|
|
240
|
+
downloadTries++;
|
|
241
|
+
await downloadFromCDN(url, archivePath, logger);
|
|
242
|
+
await extractBinary(url, archivePath, extractedPath, logger);
|
|
243
|
+
break;
|
|
204
244
|
}
|
|
205
|
-
|
|
245
|
+
catch (e) {
|
|
246
|
+
//Delete generated files since either download or extraction failed
|
|
206
247
|
try {
|
|
207
248
|
await Promise.all([
|
|
208
249
|
fsPromises.rm(extractedPath, { force: true, recursive: true }),
|
|
209
250
|
fsPromises.rm(archivePath, { force: true, recursive: true })
|
|
210
251
|
]);
|
|
211
252
|
}
|
|
212
|
-
|
|
253
|
+
catch (e) {
|
|
254
|
+
logger.error('An error occurred while deleting extractedPath and/or archivePath:', e);
|
|
255
|
+
}
|
|
256
|
+
if (downloadTries >= options.downloadRetries) {
|
|
257
|
+
//Only reject if we have met the downloadRetries limit
|
|
213
258
|
try {
|
|
214
|
-
(
|
|
259
|
+
releaseFunction();
|
|
215
260
|
}
|
|
216
261
|
catch (e) {
|
|
217
|
-
logger.error('An error occurred while
|
|
262
|
+
logger.error('An error occurred while releasing lock after downloadRetries exhaustion. The error was:', e);
|
|
218
263
|
}
|
|
264
|
+
logger.error('downloadRetries have been exceeded. Aborting download.');
|
|
265
|
+
return reject(e);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e);
|
|
219
269
|
}
|
|
220
270
|
}
|
|
221
|
-
|
|
271
|
+
} while (downloadTries < options.downloadRetries);
|
|
272
|
+
try {
|
|
273
|
+
releaseFunction();
|
|
222
274
|
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
logger.error('An error occurred while releasing lock after successful binary download. The error was:', e);
|
|
277
|
+
}
|
|
278
|
+
return resolve(binaryPath);
|
|
223
279
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
280
|
+
else {
|
|
281
|
+
let downloadTries = 0;
|
|
282
|
+
do {
|
|
283
|
+
const uuid = (0, crypto_1.randomUUID)();
|
|
284
|
+
const zipFilepath = `${dirpath}/${uuid}.${fileExtension}`;
|
|
285
|
+
logger.log('Binary filepath:', zipFilepath);
|
|
286
|
+
const extractedPath = `${dirpath}/${uuid}`;
|
|
287
|
+
try {
|
|
288
|
+
downloadTries++;
|
|
289
|
+
await downloadFromCDN(url, zipFilepath, logger);
|
|
290
|
+
const binaryPath = await extractBinary(url, zipFilepath, extractedPath, logger);
|
|
291
|
+
return resolve(binaryPath);
|
|
292
|
+
}
|
|
293
|
+
catch (e) {
|
|
294
|
+
//Delete generated files since either download or extraction failed
|
|
295
|
+
try {
|
|
296
|
+
await Promise.all([
|
|
297
|
+
fsPromises.rm(extractedPath, { force: true, recursive: true }),
|
|
298
|
+
fsPromises.rm(zipFilepath, { force: true, recursive: true })
|
|
299
|
+
]);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
logger.error('An error occurred while deleting extractedPath and/or archivePath:', e);
|
|
303
|
+
}
|
|
304
|
+
if (downloadTries >= options.downloadRetries) {
|
|
305
|
+
//Only reject if we have met the downloadRetries limit
|
|
306
|
+
return reject(e);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
console.warn(`An error was encountered during the binary download process. Retrying for retry ${downloadTries}/${options.downloadRetries}. The error was:`, e);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} while (downloadTries < options.downloadRetries);
|
|
240
313
|
}
|
|
241
314
|
});
|
|
242
315
|
}
|
|
@@ -4,7 +4,6 @@ declare class Executor {
|
|
|
4
4
|
#private;
|
|
5
5
|
logger: Logger;
|
|
6
6
|
constructor(logger: Logger);
|
|
7
|
-
deleteDatabaseDirectory(path: string): Promise<void>;
|
|
8
7
|
getMySQLVersion(preferredVersion?: string): Promise<InstalledMySQLVersion | null>;
|
|
9
8
|
startMySQL(options: InternalServerOptions, binaryFilepath: string): Promise<MySQLDB>;
|
|
10
9
|
}
|
|
@@ -30,7 +30,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
30
30
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
31
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
32
|
};
|
|
33
|
-
var _Executor_instances,
|
|
33
|
+
var _Executor_instances, _Executor_executeFile, _Executor_killProcess, _Executor_deleteDatabaseDirectory, _Executor_startMySQLProcess, _Executor_setupDataDirectories;
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
const child_process_1 = require("child_process");
|
|
36
36
|
const semver_1 = require("semver");
|
|
@@ -47,29 +47,6 @@ class Executor {
|
|
|
47
47
|
_Executor_instances.add(this);
|
|
48
48
|
this.logger = logger;
|
|
49
49
|
}
|
|
50
|
-
async deleteDatabaseDirectory(path) {
|
|
51
|
-
let retries = 0;
|
|
52
|
-
//Maximum wait of 10 seconds | 500ms * 20 retries = 10,000ms = 10 seconds
|
|
53
|
-
const waitTime = 500;
|
|
54
|
-
const maxRetries = 20;
|
|
55
|
-
//Since the database processes are killed instantly (SIGKILL) sometimes the database file handles may still be open
|
|
56
|
-
//This would cause an EBUSY error. Retrying the deletions for 10 seconds should give the OS enough time to close
|
|
57
|
-
//the file handles.
|
|
58
|
-
while (retries <= maxRetries) {
|
|
59
|
-
try {
|
|
60
|
-
await fsPromises.rm(path, { recursive: true, force: true });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
if (retries === maxRetries) {
|
|
65
|
-
throw e;
|
|
66
|
-
}
|
|
67
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
68
|
-
retries++;
|
|
69
|
-
this.logger.log('DB data directory deletion failed. Now on retry', retries);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
50
|
getMySQLVersion(preferredVersion) {
|
|
74
51
|
return new Promise(async (resolve, reject) => {
|
|
75
52
|
if (process.platform === 'win32') {
|
|
@@ -83,7 +60,7 @@ class Executor {
|
|
|
83
60
|
const versions = [];
|
|
84
61
|
for (const dir of servers) {
|
|
85
62
|
const path = `${process.env.PROGRAMFILES}\\MySQL\\${dir}\\bin\\mysqld`;
|
|
86
|
-
const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m",
|
|
63
|
+
const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, path, ['--version']);
|
|
87
64
|
if (error || stderr) {
|
|
88
65
|
return reject(error || stderr);
|
|
89
66
|
}
|
|
@@ -110,8 +87,8 @@ class Executor {
|
|
|
110
87
|
}
|
|
111
88
|
}
|
|
112
89
|
else {
|
|
113
|
-
const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m",
|
|
114
|
-
if (
|
|
90
|
+
const { error, stdout, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'mysqld', ['--version']);
|
|
91
|
+
if (error && error.code === 'ENOENT') {
|
|
115
92
|
resolve(null);
|
|
116
93
|
}
|
|
117
94
|
else if (error || stderr) {
|
|
@@ -161,22 +138,16 @@ class Executor {
|
|
|
161
138
|
} while (retries <= options.portRetries);
|
|
162
139
|
}
|
|
163
140
|
}
|
|
164
|
-
_Executor_instances = new WeakSet(),
|
|
141
|
+
_Executor_instances = new WeakSet(), _Executor_executeFile = function _Executor_executeFile(command, args) {
|
|
165
142
|
return new Promise(resolve => {
|
|
166
|
-
(0, child_process_1.
|
|
143
|
+
(0, child_process_1.execFile)(command, args, { signal: AbortSignal_1.default.signal }, (error, stdout, stderr) => {
|
|
167
144
|
resolve({ error, stdout, stderr });
|
|
168
145
|
});
|
|
169
146
|
});
|
|
170
|
-
}, _Executor_executeFile = function _Executor_executeFile(command, args, cwd) {
|
|
171
|
-
return new Promise(resolve => {
|
|
172
|
-
(0, child_process_1.execFile)(command, args, { signal: AbortSignal_1.default.signal, cwd }, (error, stdout, stderr) => {
|
|
173
|
-
resolve({ stdout, stderr: (error === null || error === void 0 ? void 0 : error.message) || stderr });
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
147
|
}, _Executor_killProcess = async function _Executor_killProcess(process) {
|
|
177
148
|
let killed = false;
|
|
178
149
|
if (os.platform() === 'win32') {
|
|
179
|
-
const { error, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m",
|
|
150
|
+
const { error, stderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'taskkill', ['/pid', String(process.pid), '/t', '/f']);
|
|
180
151
|
if (!error && !stderr) {
|
|
181
152
|
killed = true;
|
|
182
153
|
}
|
|
@@ -188,6 +159,28 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
188
159
|
killed = process.kill();
|
|
189
160
|
}
|
|
190
161
|
return killed;
|
|
162
|
+
}, _Executor_deleteDatabaseDirectory = async function _Executor_deleteDatabaseDirectory(path) {
|
|
163
|
+
let retries = 0;
|
|
164
|
+
//Maximum wait of 10 seconds | 500ms * 20 retries = 10,000ms = 10 seconds
|
|
165
|
+
const waitTime = 500;
|
|
166
|
+
const maxRetries = 20;
|
|
167
|
+
//Since the database processes are killed instantly (SIGKILL) sometimes the database file handles may still be open
|
|
168
|
+
//This would cause an EBUSY error. Retrying the deletions for 10 seconds should give the OS enough time to close
|
|
169
|
+
//the file handles.
|
|
170
|
+
while (retries <= maxRetries) {
|
|
171
|
+
try {
|
|
172
|
+
await fsPromises.rm(path, { recursive: true, force: true });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (retries === maxRetries) {
|
|
177
|
+
throw e;
|
|
178
|
+
}
|
|
179
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
180
|
+
retries++;
|
|
181
|
+
this.logger.log('DB data directory deletion failed. Now on retry', retries);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
191
184
|
}, _Executor_startMySQLProcess = function _Executor_startMySQLProcess(options, port, mySQLXPort, datadir, dbPath, binaryFilepath) {
|
|
192
185
|
const errors = [];
|
|
193
186
|
const logFile = `${dbPath}/log.log`;
|
|
@@ -213,7 +206,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
213
206
|
if (portIssue || xPortIssue) {
|
|
214
207
|
this.logger.log('Error log when exiting for port in use error:', errorLog);
|
|
215
208
|
try {
|
|
216
|
-
await this.
|
|
209
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, options.dbPath);
|
|
217
210
|
}
|
|
218
211
|
catch (e) {
|
|
219
212
|
this.logger.error(e);
|
|
@@ -223,7 +216,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
223
216
|
}
|
|
224
217
|
try {
|
|
225
218
|
if (options.deleteDBAfterStopped) {
|
|
226
|
-
await this.
|
|
219
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, dbPath);
|
|
227
220
|
}
|
|
228
221
|
}
|
|
229
222
|
catch (e) {
|
|
@@ -310,7 +303,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
310
303
|
await fsPromises.mkdir(datadir, { recursive: true });
|
|
311
304
|
let stderr;
|
|
312
305
|
if (binaryFilepath === 'mysqld') {
|
|
313
|
-
const { error, stderr: output } = await __classPrivateFieldGet(this, _Executor_instances, "m",
|
|
306
|
+
const { error, stderr: output } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'mysqld', ['--no-defaults', `--datadir=${datadir}`, '--initialize-insecure']);
|
|
314
307
|
stderr = output;
|
|
315
308
|
if (error) {
|
|
316
309
|
this.logger.error('An error occurred while initializing database with system-installed MySQL:', error);
|
|
@@ -320,7 +313,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
320
313
|
else {
|
|
321
314
|
let result;
|
|
322
315
|
try {
|
|
323
|
-
result = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, `${binaryFilepath}`, [`--no-defaults`, `--datadir=${datadir}`, `--initialize-insecure`]
|
|
316
|
+
result = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, `${binaryFilepath}`, [`--no-defaults`, `--datadir=${datadir}`, `--initialize-insecure`]);
|
|
324
317
|
}
|
|
325
318
|
catch (e) {
|
|
326
319
|
this.logger.error('Error occurred from executeFile:', e);
|
|
@@ -342,7 +335,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
342
335
|
throw 'Tried to copy libaio into lib folder and MySQL is still failing to initialize. Please check the console for more information.';
|
|
343
336
|
}
|
|
344
337
|
if (binaryFilepath.slice(-16) === 'mysql/bin/mysqld') {
|
|
345
|
-
const { error: lderror, stdout, stderr: ldstderr } = await __classPrivateFieldGet(this, _Executor_instances, "m",
|
|
338
|
+
const { error: lderror, stdout, stderr: ldstderr } = await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_executeFile).call(this, 'ldconfig', ['-p']);
|
|
346
339
|
if (lderror || ldstderr) {
|
|
347
340
|
this.logger.error('The following libaio error occurred:', stderr);
|
|
348
341
|
this.logger.error('After the libaio error, an ldconfig error occurred:', lderror || ldstderr);
|
|
@@ -358,8 +351,34 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
358
351
|
const libaioSymlinkPath = libaioEntry.slice(libaioPathIndex + 3);
|
|
359
352
|
const libaioPath = await fsPromises.realpath(libaioSymlinkPath);
|
|
360
353
|
const copyPath = (0, path_1.resolve)(`${binaryFilepath}/../../lib/private/libaio.so.1`);
|
|
361
|
-
|
|
362
|
-
|
|
354
|
+
let lockRelease;
|
|
355
|
+
while (true) {
|
|
356
|
+
try {
|
|
357
|
+
lockRelease = (0, proper_lockfile_1.lockSync)(copyPath, { realpath: false });
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
catch (e) {
|
|
361
|
+
if (e.code === 'ELOCKED') {
|
|
362
|
+
this.logger.log('Waiting for lock for libaio copy');
|
|
363
|
+
await (0, FileLock_1.waitForLock)(copyPath, options);
|
|
364
|
+
this.logger.log('Lock is gone for libaio copy');
|
|
365
|
+
//If libaio does not exist after the lock has been released (like if the copy fails)
|
|
366
|
+
//then the lock acquisition process should start again
|
|
367
|
+
const binaryExists = fs.existsSync(copyPath);
|
|
368
|
+
if (!binaryExists)
|
|
369
|
+
continue;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.logger.error('An error occurred from locking libaio section:', e);
|
|
374
|
+
throw e;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (lockRelease) {
|
|
379
|
+
//This code will only run if the lock has been acquired successfully.
|
|
380
|
+
//If the lock was already acquired by some other process, this process would have already waited for the lock, so no copying needs to be done since it's already happened.
|
|
381
|
+
//If the lock failed to acquire for some other reason, the error would've already been thrown.
|
|
363
382
|
this.logger.log('libaio copy path:', copyPath, '| libaio symlink path:', libaioSymlinkPath, '| libaio actual path:', libaioPath);
|
|
364
383
|
let copyError;
|
|
365
384
|
try {
|
|
@@ -377,7 +396,7 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
377
396
|
}
|
|
378
397
|
finally {
|
|
379
398
|
try {
|
|
380
|
-
(
|
|
399
|
+
lockRelease();
|
|
381
400
|
}
|
|
382
401
|
catch (e) {
|
|
383
402
|
this.logger.error('Error unlocking libaio file:', e);
|
|
@@ -387,20 +406,11 @@ _Executor_instances = new WeakSet(), _Executor_execute = function _Executor_exec
|
|
|
387
406
|
}
|
|
388
407
|
//Retry setting up directory now that libaio has been copied
|
|
389
408
|
this.logger.log('Retrying directory setup');
|
|
390
|
-
await this.
|
|
409
|
+
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_deleteDatabaseDirectory).call(this, datadir);
|
|
391
410
|
await __classPrivateFieldGet(this, _Executor_instances, "m", _Executor_setupDataDirectories).call(this, options, binaryFilepath, datadir, false);
|
|
392
411
|
return;
|
|
393
412
|
}
|
|
394
413
|
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
if (String(error).includes('Lock file is already being held')) {
|
|
397
|
-
this.logger.log('Waiting for lock for libaio copy');
|
|
398
|
-
await (0, FileLock_1.waitForLock)(copyPath, options);
|
|
399
|
-
this.logger.log('Lock is gone for libaio copy');
|
|
400
|
-
}
|
|
401
|
-
this.logger.error('An error occurred from locking libaio section:', error);
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
414
|
}
|
|
405
415
|
else {
|
|
406
416
|
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';
|
|
@@ -8,7 +8,7 @@ function waitForLock(path, options) {
|
|
|
8
8
|
while (retries <= options.lockRetries) {
|
|
9
9
|
retries++;
|
|
10
10
|
try {
|
|
11
|
-
const locked = (0, proper_lockfile_1.checkSync)(path);
|
|
11
|
+
const locked = (0, proper_lockfile_1.checkSync)(path, { realpath: false });
|
|
12
12
|
if (!locked) {
|
|
13
13
|
return resolve();
|
|
14
14
|
}
|
|
@@ -11,17 +11,17 @@ class Logger {
|
|
|
11
11
|
}
|
|
12
12
|
log(...args) {
|
|
13
13
|
if (this.LOG_LEVEL === 0) {
|
|
14
|
-
console.log(...args);
|
|
14
|
+
console.log('[mysql-memory-server - LOG]:', ...args);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
warn(...args) {
|
|
18
18
|
if (this.LOG_LEVEL <= 1) {
|
|
19
|
-
console.warn(...args);
|
|
19
|
+
console.warn('[mysql-memory-server - WARN]:', ...args);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
error(...args) {
|
|
23
23
|
if (this.LOG_LEVEL <= 2) {
|
|
24
|
-
console.error(...args);
|
|
24
|
+
console.error('[mysql-memory-server - ERROR]:', ...args);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ExecFileException } from "child_process";
|
|
2
2
|
export type LOG_LEVEL = 'LOG' | 'WARN' | 'ERROR';
|
|
3
3
|
export type ServerOptions = {
|
|
4
4
|
version?: string;
|
|
@@ -14,6 +14,8 @@ export type ServerOptions = {
|
|
|
14
14
|
ignoreUnsupportedSystemVersion?: boolean;
|
|
15
15
|
port?: number;
|
|
16
16
|
xPort?: number;
|
|
17
|
+
binaryDirectoryPath?: string;
|
|
18
|
+
downloadRetries?: number;
|
|
17
19
|
};
|
|
18
20
|
export type InternalServerOptions = {
|
|
19
21
|
version?: string;
|
|
@@ -29,12 +31,14 @@ export type InternalServerOptions = {
|
|
|
29
31
|
ignoreUnsupportedSystemVersion: boolean;
|
|
30
32
|
port: number;
|
|
31
33
|
xPort: number;
|
|
34
|
+
binaryDirectoryPath: string;
|
|
35
|
+
downloadRetries: number;
|
|
32
36
|
};
|
|
33
37
|
export type ExecutorOptions = {
|
|
34
38
|
logLevel: LOG_LEVEL;
|
|
35
39
|
};
|
|
36
|
-
export type
|
|
37
|
-
error:
|
|
40
|
+
export type ExecuteFileReturn = {
|
|
41
|
+
error: ExecFileException | null;
|
|
38
42
|
stdout: string;
|
|
39
43
|
stderr: string;
|
|
40
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mysql-memory-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Spin up an ephemeral MySQL database from your JavaScript code",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/src/index.d.ts",
|
|
@@ -20,20 +20,24 @@
|
|
|
20
20
|
"test:ci": "jest --testPathIgnorePatterns=/stress-tests/ --setupFilesAfterEnv ./ciSetup.js --verbose",
|
|
21
21
|
"stress": "jest --runTestsByPath stress-tests/stress.test.ts --setupFilesAfterEnv ./ciSetup.js --verbose"
|
|
22
22
|
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16.6.0",
|
|
25
|
+
"bun": ">=1.0.0"
|
|
26
|
+
},
|
|
23
27
|
"author": "Sebastian-Webster",
|
|
24
28
|
"license": "MIT",
|
|
25
29
|
"devDependencies": {
|
|
26
|
-
"@babel/core": "^7.
|
|
27
|
-
"@babel/preset-env": "^7.25.
|
|
28
|
-
"@babel/preset-typescript": "^7.
|
|
30
|
+
"@babel/core": "^7.25.8",
|
|
31
|
+
"@babel/preset-env": "^7.25.8",
|
|
32
|
+
"@babel/preset-typescript": "^7.25.7",
|
|
29
33
|
"@types/adm-zip": "^0.5.5",
|
|
30
|
-
"@types/node": "^22.5
|
|
34
|
+
"@types/node": "^22.7.5",
|
|
31
35
|
"@types/proper-lockfile": "^4.1.4",
|
|
32
36
|
"@types/semver": "^7.5.8",
|
|
33
37
|
"babel-jest": "^29.7.0",
|
|
34
38
|
"jest": "^29.7.0",
|
|
35
|
-
"mysql2": "^3.
|
|
36
|
-
"typescript": "^5.
|
|
39
|
+
"mysql2": "^3.11.3",
|
|
40
|
+
"typescript": "^5.6.3"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
43
|
"adm-zip": "^0.5.16",
|