@whyour/qinglong 2.21.0-13 → 2.21.0-15
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/docker/Dockerfile +21 -6
- package/docker/Dockerfile.310 +21 -6
- package/docker/Dockerfile.debian +3 -1
- package/docker/Dockerfile.debian310 +3 -1
- package/package.json +4 -3
- package/static/build/config/util.js +23 -2
- package/static/build/services/config.js +2 -3
- package/static/build/services/sampleConfig.js +73 -0
- package/test/race-conditions.test.js +175 -0
package/docker/Dockerfile
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
# Run Node package installation natively on the builder. Node/npm can spin at
|
|
2
|
+
# 100% CPU when Alpine s390x is emulated through QEMU.
|
|
3
|
+
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
|
4
|
+
|
|
5
|
+
ARG TARGETARCH
|
|
6
|
+
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
|
7
|
+
ENV PATH=/opt/node-global/bin:${PATH}
|
|
8
|
+
|
|
2
9
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
|
3
10
|
RUN set -x \
|
|
4
|
-
&& apk
|
|
5
|
-
&& apk add nodejs npm git \
|
|
11
|
+
&& apk add --no-cache git \
|
|
6
12
|
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
|
7
13
|
&& cd /tmp/build \
|
|
8
|
-
&&
|
|
14
|
+
&& case "${TARGETARCH}" in \
|
|
15
|
+
amd64) NODE_ARCH=x64 ;; \
|
|
16
|
+
386) NODE_ARCH=ia32 ;; \
|
|
17
|
+
ppc64le) NODE_ARCH=ppc64 ;; \
|
|
18
|
+
*) NODE_ARCH="${TARGETARCH}" ;; \
|
|
19
|
+
esac \
|
|
20
|
+
&& npm_config_target_platform=linux \
|
|
21
|
+
npm_config_target_arch="${NODE_ARCH}" \
|
|
22
|
+
npm_config_target_libc=musl \
|
|
23
|
+
pnpm install --prod
|
|
9
24
|
|
|
10
25
|
FROM python:3.11-alpine
|
|
11
26
|
|
|
@@ -26,8 +41,8 @@ VOLUME /ql/data
|
|
|
26
41
|
|
|
27
42
|
EXPOSE 5700
|
|
28
43
|
|
|
29
|
-
COPY --from=builder /
|
|
30
|
-
COPY --from=builder /
|
|
44
|
+
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
|
|
45
|
+
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
|
|
31
46
|
|
|
32
47
|
RUN set -x \
|
|
33
48
|
&& apk update -f \
|
package/docker/Dockerfile.310
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
# Run Node package installation natively on the builder. Node/npm can spin at
|
|
2
|
+
# 100% CPU when Alpine s390x is emulated through QEMU.
|
|
3
|
+
FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder
|
|
4
|
+
|
|
5
|
+
ARG TARGETARCH
|
|
6
|
+
ENV NPM_CONFIG_PREFIX=/opt/node-global
|
|
7
|
+
ENV PATH=/opt/node-global/bin:${PATH}
|
|
8
|
+
|
|
2
9
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
|
3
10
|
RUN set -x \
|
|
4
|
-
&& apk
|
|
5
|
-
&& apk add nodejs npm git \
|
|
11
|
+
&& apk add --no-cache git \
|
|
6
12
|
&& npm i -g pnpm@8.3.1 pm2 ts-node typescript@5 \
|
|
7
13
|
&& cd /tmp/build \
|
|
8
|
-
&&
|
|
14
|
+
&& case "${TARGETARCH}" in \
|
|
15
|
+
amd64) NODE_ARCH=x64 ;; \
|
|
16
|
+
386) NODE_ARCH=ia32 ;; \
|
|
17
|
+
ppc64le) NODE_ARCH=ppc64 ;; \
|
|
18
|
+
*) NODE_ARCH="${TARGETARCH}" ;; \
|
|
19
|
+
esac \
|
|
20
|
+
&& npm_config_target_platform=linux \
|
|
21
|
+
npm_config_target_arch="${NODE_ARCH}" \
|
|
22
|
+
npm_config_target_libc=musl \
|
|
23
|
+
pnpm install --prod
|
|
9
24
|
|
|
10
25
|
FROM python:3.10-alpine
|
|
11
26
|
|
|
@@ -26,8 +41,8 @@ VOLUME /ql/data
|
|
|
26
41
|
|
|
27
42
|
EXPOSE 5700
|
|
28
43
|
|
|
29
|
-
COPY --from=builder /
|
|
30
|
-
COPY --from=builder /
|
|
44
|
+
COPY --from=builder /opt/node-global/lib/node_modules/. /usr/local/lib/node_modules/
|
|
45
|
+
COPY --from=builder /opt/node-global/bin/. /usr/local/bin/
|
|
31
46
|
|
|
32
47
|
RUN set -x \
|
|
33
48
|
&& apk update -f \
|
package/docker/Dockerfile.debian
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# Node 20 Bookworm is the latest official Node image variant that covers the
|
|
2
|
+
# full Debian build matrix, including arm/v7, ppc64le, and s390x.
|
|
3
|
+
FROM node:20-bookworm-slim AS nodebuilder
|
|
2
4
|
|
|
3
5
|
FROM python:3.11-slim-bookworm AS builder
|
|
4
6
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
# Node 20 Bookworm is the latest official Node image variant that covers the
|
|
2
|
+
# full Debian build matrix, including arm/v7, ppc64le, and s390x.
|
|
3
|
+
FROM node:20-bookworm-slim AS nodebuilder
|
|
2
4
|
|
|
3
5
|
FROM python:3.10-slim-bookworm AS builder
|
|
4
6
|
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whyour/qinglong",
|
|
3
3
|
"packageManager": "pnpm@8.3.1",
|
|
4
|
-
"version": "2.21.0-
|
|
4
|
+
"version": "2.21.0-15",
|
|
5
5
|
"description": "Timed task management platform supporting Python3, JavaScript, Shell, Typescript",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"start:front": "max dev",
|
|
19
19
|
"build:front": "max build",
|
|
20
20
|
"build:back": "tsc -p back/tsconfig.json",
|
|
21
|
+
"test:race": "node -r ts-node/register test/race-conditions.test.js",
|
|
21
22
|
"panel": "npm run build:back && node static/build/app.js",
|
|
22
23
|
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
|
23
24
|
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
}
|
|
66
67
|
},
|
|
67
68
|
"overrides": {
|
|
68
|
-
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0
|
|
69
|
+
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.1.0",
|
|
69
70
|
"@codemirror/state": "6.5.4",
|
|
70
71
|
"@codemirror/view": "6.39.16"
|
|
71
72
|
}
|
|
@@ -109,7 +110,7 @@
|
|
|
109
110
|
"request-ip": "3.3.0",
|
|
110
111
|
"sequelize": "^6.37.5",
|
|
111
112
|
"sockjs": "^0.3.24",
|
|
112
|
-
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0
|
|
113
|
+
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.1.0",
|
|
113
114
|
"toad-scheduler": "^3.0.1",
|
|
114
115
|
"typedi": "^0.10.0",
|
|
115
116
|
"undici": "^7.9.0",
|
|
@@ -232,13 +232,34 @@ async function readDirs(dir, baseDir = '', blacklist = [], sort = dirSort) {
|
|
|
232
232
|
const result = [];
|
|
233
233
|
for (const file of files) {
|
|
234
234
|
const subPath = path.join(dir, file);
|
|
235
|
-
|
|
235
|
+
let stats;
|
|
236
|
+
try {
|
|
237
|
+
stats = await fs.lstat(subPath);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// Files can disappear between readdir and lstat while logs are pruned.
|
|
241
|
+
// Skip only that expected race and keep surfacing all other I/O errors.
|
|
242
|
+
if (error.code === 'ENOENT') {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
236
247
|
const key = path.join(relativePath, file);
|
|
237
248
|
if (blacklist.includes(file) || stats.isSymbolicLink()) {
|
|
238
249
|
continue;
|
|
239
250
|
}
|
|
240
251
|
if (stats.isDirectory()) {
|
|
241
|
-
|
|
252
|
+
let children;
|
|
253
|
+
try {
|
|
254
|
+
children = await readDirs(subPath, baseDir, blacklist, sort);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// A directory may also be removed after lstat but before recursion.
|
|
258
|
+
if (error.code === 'ENOENT') {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
242
263
|
result.push({
|
|
243
264
|
title: file,
|
|
244
265
|
key,
|
|
@@ -40,7 +40,7 @@ const path_1 = __importStar(require("path"));
|
|
|
40
40
|
const config_1 = __importDefault(require("../config"));
|
|
41
41
|
const util_1 = require("../config/util");
|
|
42
42
|
const i18n_1 = require("../shared/i18n");
|
|
43
|
-
const
|
|
43
|
+
const sampleConfig_1 = require("./sampleConfig");
|
|
44
44
|
let ConfigService = class ConfigService {
|
|
45
45
|
constructor() { }
|
|
46
46
|
async getFile(filePath, res) {
|
|
@@ -65,8 +65,7 @@ let ConfigService = class ConfigService {
|
|
|
65
65
|
return res.send({ code: 403, message: (0, i18n_1.t)('文件无法访问') });
|
|
66
66
|
}
|
|
67
67
|
if (filePath.startsWith('sample/')) {
|
|
68
|
-
|
|
69
|
-
content = await res.body.text();
|
|
68
|
+
content = await (0, sampleConfig_1.loadSampleFile)(filePath);
|
|
70
69
|
}
|
|
71
70
|
else if (filePath.startsWith('data/scripts/')) {
|
|
72
71
|
content = await (0, util_1.getFileContentByName)((0, path_1.join)(config_1.default.rootPath, filePath));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadSampleFile = void 0;
|
|
7
|
+
const promises_1 = require("fs/promises");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const undici_1 = require("undici");
|
|
10
|
+
const config_1 = __importDefault(require("../config"));
|
|
11
|
+
const util_1 = require("../config/util");
|
|
12
|
+
const logger_1 = __importDefault(require("../loaders/logger"));
|
|
13
|
+
const sampleRequestDispatcher = new undici_1.RetryAgent(new undici_1.Agent({
|
|
14
|
+
connect: { timeout: 10000 },
|
|
15
|
+
headersTimeout: 10000,
|
|
16
|
+
bodyTimeout: 10000,
|
|
17
|
+
keepAliveTimeout: 10000,
|
|
18
|
+
keepAliveMaxTimeout: 30000,
|
|
19
|
+
}), {
|
|
20
|
+
maxRetries: 2,
|
|
21
|
+
minTimeout: 200,
|
|
22
|
+
maxTimeout: 1000,
|
|
23
|
+
timeoutFactor: 2,
|
|
24
|
+
statusCodes: [429, 500, 502, 503, 504],
|
|
25
|
+
errorCodes: [
|
|
26
|
+
'ECONNRESET',
|
|
27
|
+
'ECONNREFUSED',
|
|
28
|
+
'ENOTFOUND',
|
|
29
|
+
'ENETDOWN',
|
|
30
|
+
'ENETUNREACH',
|
|
31
|
+
'EHOSTDOWN',
|
|
32
|
+
'EHOSTUNREACH',
|
|
33
|
+
'EPIPE',
|
|
34
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
35
|
+
'UND_ERR_HEADERS_TIMEOUT',
|
|
36
|
+
'UND_ERR_BODY_TIMEOUT',
|
|
37
|
+
'UND_ERR_SOCKET',
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
const pendingSampleRequests = new Map();
|
|
41
|
+
async function loadSampleFile(filePath) {
|
|
42
|
+
const pendingRequest = pendingSampleRequests.get(filePath);
|
|
43
|
+
if (pendingRequest) {
|
|
44
|
+
return pendingRequest;
|
|
45
|
+
}
|
|
46
|
+
const requestPromise = (async () => {
|
|
47
|
+
const remoteUrl = `https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`;
|
|
48
|
+
try {
|
|
49
|
+
const response = await (0, undici_1.request)(remoteUrl, {
|
|
50
|
+
dispatcher: sampleRequestDispatcher,
|
|
51
|
+
maxRedirections: 3,
|
|
52
|
+
});
|
|
53
|
+
const content = await response.body.text();
|
|
54
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
55
|
+
throw new Error(`GitLab returned HTTP ${response.statusCode} for ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
logger_1.default.warn(`[config] Failed to fetch ${filePath}, using bundled sample: ${(0, util_1.errStack)(error)}`);
|
|
61
|
+
return (0, promises_1.readFile)((0, path_1.join)(config_1.default.rootPath, filePath), 'utf8');
|
|
62
|
+
}
|
|
63
|
+
})();
|
|
64
|
+
pendingSampleRequests.set(filePath, requestPromise);
|
|
65
|
+
try {
|
|
66
|
+
return await requestPromise;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
pendingSampleRequests.delete(filePath);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.loadSampleFile = loadSampleFile;
|
|
73
|
+
//# sourceMappingURL=sampleConfig.js.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs/promises');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
require('reflect-metadata');
|
|
7
|
+
|
|
8
|
+
async function testReadDirsSkipsDisappearedEntries() {
|
|
9
|
+
const temporaryRoot = await fs.mkdtemp(
|
|
10
|
+
path.join(os.tmpdir(), 'qinglong-read-dirs-'),
|
|
11
|
+
);
|
|
12
|
+
const disappearingFile = path.join(temporaryRoot, 'disappearing.log');
|
|
13
|
+
const disappearingDirectory = path.join(temporaryRoot, 'disappearing-dir');
|
|
14
|
+
const originalLstat = fs.lstat;
|
|
15
|
+
const originalReaddir = fs.readdir;
|
|
16
|
+
|
|
17
|
+
await fs.writeFile(path.join(temporaryRoot, 'stable.log'), 'stable');
|
|
18
|
+
await fs.writeFile(disappearingFile, 'gone');
|
|
19
|
+
await fs.mkdir(disappearingDirectory);
|
|
20
|
+
|
|
21
|
+
fs.lstat = async (filePath, ...args) => {
|
|
22
|
+
if (filePath === disappearingFile) {
|
|
23
|
+
const error = new Error('file disappeared');
|
|
24
|
+
error.code = 'ENOENT';
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return originalLstat(filePath, ...args);
|
|
28
|
+
};
|
|
29
|
+
fs.readdir = async (directoryPath, ...args) => {
|
|
30
|
+
if (directoryPath === disappearingDirectory) {
|
|
31
|
+
const error = new Error('directory disappeared');
|
|
32
|
+
error.code = 'ENOENT';
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return originalReaddir(directoryPath, ...args);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const { readDirs } = require('../back/config/util');
|
|
40
|
+
const entries = await readDirs(temporaryRoot, temporaryRoot);
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
entries.map((entry) => entry.title),
|
|
43
|
+
['stable.log'],
|
|
44
|
+
);
|
|
45
|
+
} finally {
|
|
46
|
+
fs.lstat = originalLstat;
|
|
47
|
+
fs.readdir = originalReaddir;
|
|
48
|
+
await fs.rm(temporaryRoot, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function testReadDirsPreservesUnexpectedErrors() {
|
|
53
|
+
const temporaryRoot = await fs.mkdtemp(
|
|
54
|
+
path.join(os.tmpdir(), 'qinglong-read-dirs-error-'),
|
|
55
|
+
);
|
|
56
|
+
const blockedFile = path.join(temporaryRoot, 'blocked.log');
|
|
57
|
+
const originalLstat = fs.lstat;
|
|
58
|
+
|
|
59
|
+
await fs.writeFile(blockedFile, 'blocked');
|
|
60
|
+
fs.lstat = async (filePath, ...args) => {
|
|
61
|
+
if (filePath === blockedFile) {
|
|
62
|
+
const error = new Error('permission denied');
|
|
63
|
+
error.code = 'EACCES';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
return originalLstat(filePath, ...args);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const { readDirs } = require('../back/config/util');
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
readDirs(temporaryRoot, temporaryRoot),
|
|
73
|
+
(error) => error.code === 'EACCES',
|
|
74
|
+
);
|
|
75
|
+
} finally {
|
|
76
|
+
fs.lstat = originalLstat;
|
|
77
|
+
await fs.rm(temporaryRoot, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function testSampleRequestsAreCoalescedAndFallbackLocally() {
|
|
82
|
+
const undici = require('undici');
|
|
83
|
+
const originalRequest = undici.request;
|
|
84
|
+
let releaseRequest;
|
|
85
|
+
let requestCount = 0;
|
|
86
|
+
|
|
87
|
+
undici.request = async () => {
|
|
88
|
+
requestCount += 1;
|
|
89
|
+
await new Promise((resolve) => {
|
|
90
|
+
releaseRequest = resolve;
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 200,
|
|
94
|
+
body: { text: async () => 'remote sample' },
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const { loadSampleFile } = require('../back/services/sampleConfig');
|
|
99
|
+
const pendingLoads = [
|
|
100
|
+
loadSampleFile('sample/config.sample.sh'),
|
|
101
|
+
loadSampleFile('sample/config.sample.sh'),
|
|
102
|
+
loadSampleFile('sample/config.sample.sh'),
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
106
|
+
assert.equal(requestCount, 1);
|
|
107
|
+
releaseRequest();
|
|
108
|
+
assert.deepEqual(await Promise.all(pendingLoads), [
|
|
109
|
+
'remote sample',
|
|
110
|
+
'remote sample',
|
|
111
|
+
'remote sample',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const temporaryRoot = await fs.mkdtemp(
|
|
115
|
+
path.join(os.tmpdir(), 'qinglong-sample-fallback-'),
|
|
116
|
+
);
|
|
117
|
+
const bundledSample = path.join(temporaryRoot, 'sample', 'config.sample.sh');
|
|
118
|
+
const config = require('../back/config').default;
|
|
119
|
+
const originalRootPath = config.rootPath;
|
|
120
|
+
|
|
121
|
+
await fs.mkdir(path.dirname(bundledSample), { recursive: true });
|
|
122
|
+
await fs.writeFile(bundledSample, 'bundled sample');
|
|
123
|
+
config.rootPath = temporaryRoot;
|
|
124
|
+
undici.request = async () => {
|
|
125
|
+
const error = new Error('other side closed');
|
|
126
|
+
error.code = 'UND_ERR_SOCKET';
|
|
127
|
+
throw error;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
assert.equal(
|
|
132
|
+
await loadSampleFile('sample/config.sample.sh'),
|
|
133
|
+
'bundled sample',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
undici.request = async () => ({
|
|
137
|
+
statusCode: 503,
|
|
138
|
+
body: { text: async () => 'upstream unavailable' },
|
|
139
|
+
});
|
|
140
|
+
assert.equal(
|
|
141
|
+
await loadSampleFile('sample/config.sample.sh'),
|
|
142
|
+
'bundled sample',
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
undici.request = originalRequest;
|
|
146
|
+
config.rootPath = originalRootPath;
|
|
147
|
+
await fs.rm(temporaryRoot, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function main() {
|
|
152
|
+
const testRoot = await fs.mkdtemp(
|
|
153
|
+
path.join(os.tmpdir(), 'qinglong-test-root-'),
|
|
154
|
+
);
|
|
155
|
+
process.env.QL_DIR = testRoot;
|
|
156
|
+
await fs.writeFile(path.join(testRoot, '.env'), 'NODE_ENV=test\n');
|
|
157
|
+
await fs.mkdir(path.join(testRoot, 'data', 'syslog'), { recursive: true });
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await testReadDirsSkipsDisappearedEntries();
|
|
161
|
+
await testReadDirsPreservesUnexpectedErrors();
|
|
162
|
+
await testSampleRequestsAreCoalescedAndFallbackLocally();
|
|
163
|
+
console.log('race condition regression tests passed');
|
|
164
|
+
} finally {
|
|
165
|
+
const loggerModule =
|
|
166
|
+
require.cache[require.resolve('../back/loaders/logger')];
|
|
167
|
+
loggerModule?.exports?.default?.close();
|
|
168
|
+
await fs.rm(testRoot, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main().catch((error) => {
|
|
173
|
+
console.error(error);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
});
|