com.jimuwd.xian.registry-proxy 1.0.124 → 1.0.126
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/dist/gracefullShutdown.d.ts +5 -0
- package/dist/gracefullShutdown.js +34 -0
- package/dist/index.js +23 -28
- package/dist/port.d.ts +4 -0
- package/dist/port.js +26 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { deletePortFile } from "./port.js";
|
|
2
|
+
import logger from "./utils/logger.js";
|
|
3
|
+
/**
|
|
4
|
+
* 优雅退出
|
|
5
|
+
* 本函数是对process.exit的封装,同时执行资源释放动作,程序必须统一调用本方法退出,决不允许直接调用{@link process.exit}来退出。
|
|
6
|
+
*/
|
|
7
|
+
export async function gracefulShutdown() {
|
|
8
|
+
try {
|
|
9
|
+
logger.info('Shutdown...');
|
|
10
|
+
await doCleanup();
|
|
11
|
+
logger.info('Shutdown completed.');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
logger.error('Failed to clean:', err);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function doCleanup() {
|
|
20
|
+
await deletePortFile();
|
|
21
|
+
}
|
|
22
|
+
// 捕获信号或异常
|
|
23
|
+
process.on('SIGINT', () => {
|
|
24
|
+
logger.info('收到 SIGINT(Ctrl+C)');
|
|
25
|
+
process.exit(0); // 触发 exit 事件
|
|
26
|
+
});
|
|
27
|
+
process.on('SIGTERM', () => {
|
|
28
|
+
logger.info('收到 SIGTERM');
|
|
29
|
+
process.exit(0); // 触发 exit 事件
|
|
30
|
+
});
|
|
31
|
+
process.on('uncaughtException', (err) => {
|
|
32
|
+
logger.info('uncaughtException:', err);
|
|
33
|
+
process.exit(1); // 触发 exit 事件
|
|
34
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import http, { createServer } from 'node:http';
|
|
3
3
|
import https, { createServer as createHttpsServer } from 'node:https';
|
|
4
|
-
import { promises as fsPromises, readFileSync } from 'fs';
|
|
4
|
+
import { promises as fsPromises, readFileSync } from 'node:fs';
|
|
5
5
|
import { load } from 'js-yaml';
|
|
6
6
|
import fetch from 'node-fetch';
|
|
7
7
|
import { homedir } from 'os';
|
|
@@ -9,7 +9,9 @@ import { join, resolve } from 'path';
|
|
|
9
9
|
import { URL } from 'url';
|
|
10
10
|
import logger from "./utils/logger.js";
|
|
11
11
|
import ConcurrencyLimiter from "./utils/ConcurrencyLimiter.js";
|
|
12
|
-
|
|
12
|
+
import { gracefulShutdown } from "./gracefullShutdown.js";
|
|
13
|
+
import { writePortFile } from "./port.js";
|
|
14
|
+
const { readFile } = fsPromises;
|
|
13
15
|
const limiter = new ConcurrencyLimiter(Infinity);
|
|
14
16
|
function removeEndingSlashAndForceStartingSlash(str) {
|
|
15
17
|
if (!str)
|
|
@@ -56,20 +58,21 @@ function removeRegistryPrefix(tarballUrl, registries) {
|
|
|
56
58
|
throw new Error(`Can't find tarball url ${tarballUrl} does not match given registries ${normalizedRegistries}`);
|
|
57
59
|
}
|
|
58
60
|
async function readProxyConfig(proxyConfigPath = './.registry-proxy.yml') {
|
|
61
|
+
let config = undefined;
|
|
59
62
|
const resolvedPath = resolvePath(proxyConfigPath);
|
|
60
63
|
try {
|
|
61
64
|
const content = await readFile(resolvedPath, 'utf8');
|
|
62
|
-
|
|
63
|
-
if (!config.registries) {
|
|
64
|
-
logger.error('Missing required "registries" field in config');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
return config;
|
|
65
|
+
config = load(content);
|
|
68
66
|
}
|
|
69
67
|
catch (e) {
|
|
70
68
|
logger.error(`Failed to load proxy config from ${resolvedPath}:`, e);
|
|
71
|
-
|
|
69
|
+
await gracefulShutdown();
|
|
70
|
+
}
|
|
71
|
+
if (!config?.registries) {
|
|
72
|
+
logger.error('Missing required "registries" field in config');
|
|
73
|
+
await gracefulShutdown();
|
|
72
74
|
}
|
|
75
|
+
return config;
|
|
73
76
|
}
|
|
74
77
|
async function readYarnConfig(path) {
|
|
75
78
|
try {
|
|
@@ -162,16 +165,16 @@ async function writeResponseToDownstreamClient(registryInfo, targetUrl, resToDow
|
|
|
162
165
|
if (!upstreamResponse.ok)
|
|
163
166
|
throw new Error("Only 2xx upstream response is supported");
|
|
164
167
|
try {
|
|
165
|
-
const contentType = upstreamResponse.headers.get("content-type")
|
|
168
|
+
const contentType = upstreamResponse.headers.get("content-type");
|
|
166
169
|
if (!contentType) {
|
|
167
170
|
logger.error(`Response from upstream content-type header is absent, ${targetUrl} `);
|
|
168
|
-
|
|
171
|
+
await gracefulShutdown();
|
|
169
172
|
}
|
|
170
|
-
if (contentType.includes('application/json')) { // JSON 处理逻辑
|
|
173
|
+
else if (contentType.includes('application/json')) { // JSON 处理逻辑
|
|
171
174
|
const data = await upstreamResponse.json();
|
|
172
175
|
if (data.versions) { // 处理node依赖包元数据
|
|
173
176
|
logger.info("Write package meta data application/json response from upstream to downstream", targetUrl);
|
|
174
|
-
const host = reqFromDownstreamClient.headers.host
|
|
177
|
+
const host = reqFromDownstreamClient.headers.host /*|| `[::1]:${proxyPort}`*/;
|
|
175
178
|
const baseUrl = `${proxyInfo.https ? 'https' : 'http'}://${host}${proxyInfo.basePath === '/' ? '' : proxyInfo.basePath}`;
|
|
176
179
|
for (const versionKey in data.versions) {
|
|
177
180
|
const packageVersion = data.versions[versionKey];
|
|
@@ -299,7 +302,6 @@ function getDownstreamClientIp(req) {
|
|
|
299
302
|
// 直接连接时,取 socket.remoteAddress
|
|
300
303
|
return req.socket.remoteAddress;
|
|
301
304
|
}
|
|
302
|
-
// deprecated 出于安全考虑只监听::1地址,废弃本注释:同时启动ipv6,ipv4监听,比如当客户端访问http://localhost:port时,无论客户端DNS解析到IPV4-127.0.0.1还是IPV6-::1地址,咱server都能轻松应对!
|
|
303
305
|
export async function startProxyServer(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath, port = 0) {
|
|
304
306
|
const proxyInfo = await loadProxyInfo(proxyConfigPath, localYarnConfigPath, globalYarnConfigPath);
|
|
305
307
|
const registryInfos = proxyInfo.registries;
|
|
@@ -358,7 +360,6 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
358
360
|
resToDownstreamClient.writeHead(404).end('All upstream registries failed');
|
|
359
361
|
}
|
|
360
362
|
};
|
|
361
|
-
// deprecated 废弃本注释:需要同时启动ipv6,ipv4监听,比如当客户端访问http://localhost:port时,无论客户端DNS解析到IPV4-127.0.0.1还是IPV6-::1地址,咱server都能轻松应对!
|
|
362
363
|
let server;
|
|
363
364
|
if (proxyInfo.https) {
|
|
364
365
|
const { key, cert } = proxyInfo.https;
|
|
@@ -370,7 +371,7 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
370
371
|
}
|
|
371
372
|
catch (e) {
|
|
372
373
|
logger.error(`HTTPS config error: key or cert file not found`, e);
|
|
373
|
-
|
|
374
|
+
await gracefulShutdown();
|
|
374
375
|
}
|
|
375
376
|
const httpsOptions = {
|
|
376
377
|
key: readFileSync(keyPath),
|
|
@@ -391,10 +392,10 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
391
392
|
logger.info(`Proxy server's initial timeout is ${server.timeout}ms, adjusting to ${serverTimeoutMs}ms`);
|
|
392
393
|
server.timeout = serverTimeoutMs;
|
|
393
394
|
const promisedServer = new Promise((resolve, reject) => {
|
|
394
|
-
const errHandler = (err) => {
|
|
395
|
+
const errHandler = async (err) => {
|
|
395
396
|
if (err.code === 'EADDRINUSE') {
|
|
396
397
|
logger.error(`Port ${port} is in use, please specify a different port or free it.`, err);
|
|
397
|
-
|
|
398
|
+
await gracefulShutdown();
|
|
398
399
|
}
|
|
399
400
|
logger.error('Server error:', err);
|
|
400
401
|
reject(err);
|
|
@@ -409,11 +410,10 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
409
410
|
// 为了代理服务器的安全性,暂时只监听本机ipv6地址【::1】,不能对本机之外暴露本代理服务地址避免造成安全隐患
|
|
410
411
|
// 注意:截止目前yarn客户端如果通过localhost:<port>来访问本服务,可能会报错ECONNREFUSED错误码,原因是yarn客户端环境解析“localhost”至多个地址,它会尝试轮询每个地址。
|
|
411
412
|
const listenOptions = { port, host: '::1', ipv6Only: true };
|
|
412
|
-
server.listen(listenOptions, () => {
|
|
413
|
+
server.listen(listenOptions, async () => {
|
|
413
414
|
const addressInfo = server.address();
|
|
414
415
|
port = addressInfo.port; // 回写上层局部变量
|
|
415
|
-
|
|
416
|
-
writeFile(portFile, addressInfo.port.toString()).catch(e => logger.error(`Failed to write port file: ${portFile}`, e));
|
|
416
|
+
await writePortFile(port);
|
|
417
417
|
logger.info(`Proxy server running on ${proxyInfo.https ? 'https' : 'http'}://localhost:${addressInfo.port}${basePathPrefixedWithSlash === '/' ? '' : basePathPrefixedWithSlash}`);
|
|
418
418
|
resolve(server);
|
|
419
419
|
});
|
|
@@ -421,14 +421,9 @@ export async function startProxyServer(proxyConfigPath, localYarnConfigPath, glo
|
|
|
421
421
|
return promisedServer;
|
|
422
422
|
}
|
|
423
423
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
424
|
-
// 确保进程捕获异常
|
|
425
|
-
process.on('uncaughtException', (err) => {
|
|
426
|
-
logger.error('Fatal error:', err);
|
|
427
|
-
process.exit(1);
|
|
428
|
-
});
|
|
429
424
|
const [, , configPath, localYarnPath, globalYarnPath, port] = process.argv;
|
|
430
|
-
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(err => {
|
|
425
|
+
startProxyServer(configPath, localYarnPath, globalYarnPath, parseInt(port, 10) || 0).catch(async (err) => {
|
|
431
426
|
logger.error('Failed to start server:', err);
|
|
432
|
-
|
|
427
|
+
await gracefulShutdown();
|
|
433
428
|
});
|
|
434
429
|
}
|
package/dist/port.d.ts
ADDED
package/dist/port.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { basename, join } from "node:path";
|
|
2
|
+
import { promises as nodeFsPromises } from 'node:fs';
|
|
3
|
+
import logger from "./utils/logger.js";
|
|
4
|
+
const { unlink, writeFile, } = nodeFsPromises;
|
|
5
|
+
export const PORT_FILE_NAME = '.registry-proxy-port';
|
|
6
|
+
export const portFile = join(process.env.PROJECT_ROOT || process.cwd(), PORT_FILE_NAME);
|
|
7
|
+
export async function writePortFile(port) {
|
|
8
|
+
await writeFile(portFile, port.toString()).catch(e => logger.error(`Failed to write port file: ${portFile}`, e));
|
|
9
|
+
}
|
|
10
|
+
export const deletePortFile = async () => {
|
|
11
|
+
await deleteFile(portFile);
|
|
12
|
+
};
|
|
13
|
+
async function deleteFile(filePath) {
|
|
14
|
+
try {
|
|
15
|
+
await unlink(filePath);
|
|
16
|
+
console.log(`文件 ${basename(filePath)} 已删除`);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (err.code === 'ENOENT') {
|
|
20
|
+
console.log('文件不存在');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.error('删除失败:', err.message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|