mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 00:34:33 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 855f591992 | |||
| d30eb2008c | |||
| e1ce0f3fa9 | |||
| 82514e65e1 | |||
| 31261190f0 |
@@ -206,6 +206,7 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body as {
|
||||
filename: string;
|
||||
@@ -223,6 +224,7 @@ export default (app: Router) => {
|
||||
await writeFileWithLock(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error saving script: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,7 +10,6 @@ import Logger from '../loaders/logger';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
import { DependenceTypes } from '../data/dependence';
|
||||
import { FormData } from 'undici';
|
||||
import os from 'os';
|
||||
|
||||
export * from './share';
|
||||
|
||||
@@ -591,162 +590,3 @@ export function getUninstallCommand(
|
||||
export function isDemoEnv() {
|
||||
return process.env.DeployEnv === 'demo';
|
||||
}
|
||||
|
||||
// OS detection for Linux mirror configuration
|
||||
let osType: 'Debian' | 'Ubuntu' | 'Alpine' | undefined;
|
||||
|
||||
async function getOSReleaseInfo(): Promise<string> {
|
||||
try {
|
||||
const osRelease = await fs.readFile('/etc/os-release', 'utf8');
|
||||
return osRelease;
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to read /etc/os-release: ${error}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function isDebian(osReleaseInfo: string): boolean {
|
||||
return osReleaseInfo.includes('Debian');
|
||||
}
|
||||
|
||||
function isUbuntu(osReleaseInfo: string): boolean {
|
||||
return osReleaseInfo.includes('Ubuntu');
|
||||
}
|
||||
|
||||
function isAlpine(osReleaseInfo: string): boolean {
|
||||
return osReleaseInfo.includes('Alpine');
|
||||
}
|
||||
|
||||
export async function detectOS(): Promise<
|
||||
'Debian' | 'Ubuntu' | 'Alpine' | undefined
|
||||
> {
|
||||
if (osType) return osType;
|
||||
const platform = os.platform();
|
||||
|
||||
if (platform === 'linux') {
|
||||
const osReleaseInfo = await getOSReleaseInfo();
|
||||
// Check Ubuntu before Debian since Ubuntu is based on Debian
|
||||
if (isUbuntu(osReleaseInfo)) {
|
||||
osType = 'Ubuntu';
|
||||
} else if (isDebian(osReleaseInfo)) {
|
||||
osType = 'Debian';
|
||||
} else if (isAlpine(osReleaseInfo)) {
|
||||
osType = 'Alpine';
|
||||
} else {
|
||||
Logger.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
|
||||
console.error(`Unknown Linux Distribution: ${osReleaseInfo}`);
|
||||
}
|
||||
} else if (platform === 'darwin') {
|
||||
osType = undefined;
|
||||
} else {
|
||||
Logger.error(`Unsupported platform: ${platform}`);
|
||||
console.error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
|
||||
return osType;
|
||||
}
|
||||
|
||||
async function getCurrentMirrorDomain(
|
||||
filePath: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const fileContent = await fs.readFile(filePath, 'utf8');
|
||||
const lines = fileContent.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const match = line.match(/https?:\/\/[^\/]+/);
|
||||
if (match) {
|
||||
return match[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
Logger.error(`Failed to read mirror configuration file ${filePath}: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
async function replaceDomainInFile(
|
||||
filePath: string,
|
||||
oldDomainWithScheme: string,
|
||||
newDomainWithScheme: string,
|
||||
): Promise<void> {
|
||||
// Ensure the new domain has a trailing slash before replacement
|
||||
if (!newDomainWithScheme.endsWith('/')) {
|
||||
newDomainWithScheme += '/';
|
||||
}
|
||||
|
||||
let fileContent = await fs.readFile(filePath, 'utf8');
|
||||
// Escape special regex characters in the old domain
|
||||
const escapedOldDomain = escapeRegExp(oldDomainWithScheme);
|
||||
let updatedContent = fileContent.replace(
|
||||
new RegExp(escapedOldDomain, 'g'),
|
||||
newDomainWithScheme,
|
||||
);
|
||||
|
||||
await writeFileWithLock(filePath, updatedContent);
|
||||
}
|
||||
|
||||
async function _updateLinuxMirror(
|
||||
osType: string,
|
||||
mirrorDomainWithScheme: string,
|
||||
): Promise<string> {
|
||||
let filePath: string, currentDomainWithScheme: string | null;
|
||||
switch (osType) {
|
||||
case 'Debian':
|
||||
filePath = '/etc/apt/sources.list.d/debian.sources';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
await replaceDomainInFile(
|
||||
filePath,
|
||||
currentDomainWithScheme,
|
||||
mirrorDomainWithScheme || 'http://deb.debian.org',
|
||||
);
|
||||
return 'apt-get update';
|
||||
} else {
|
||||
throw Error(`Current mirror domain not found.`);
|
||||
}
|
||||
case 'Ubuntu':
|
||||
filePath = '/etc/apt/sources.list.d/ubuntu.sources';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
await replaceDomainInFile(
|
||||
filePath,
|
||||
currentDomainWithScheme,
|
||||
mirrorDomainWithScheme || 'http://archive.ubuntu.com',
|
||||
);
|
||||
return 'apt-get update';
|
||||
} else {
|
||||
throw Error(`Current mirror domain not found.`);
|
||||
}
|
||||
case 'Alpine':
|
||||
filePath = '/etc/apk/repositories';
|
||||
currentDomainWithScheme = await getCurrentMirrorDomain(filePath);
|
||||
if (currentDomainWithScheme) {
|
||||
await replaceDomainInFile(
|
||||
filePath,
|
||||
currentDomainWithScheme,
|
||||
mirrorDomainWithScheme || 'http://dl-cdn.alpinelinux.org',
|
||||
);
|
||||
return 'apk update';
|
||||
} else {
|
||||
throw Error(`Current mirror domain not found.`);
|
||||
}
|
||||
default:
|
||||
throw Error('Unsupported OS type for updating mirrors.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLinuxMirrorFile(mirror: string): Promise<string> {
|
||||
const detectedOS = await detectOS();
|
||||
if (!detectedOS) {
|
||||
throw Error(`Unknown Linux Distribution`);
|
||||
}
|
||||
return await _updateLinuxMirror(detectedOS, mirror);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,17 @@ export class HttpServerService {
|
||||
metricsService.record('http_service_start', 1, {
|
||||
port: port.toString(),
|
||||
});
|
||||
|
||||
// Set server timeouts to prevent premature connection drops
|
||||
if (this.server) {
|
||||
// Timeout for receiving the entire request (including body) - 5 minutes
|
||||
this.server.requestTimeout = 300000;
|
||||
// Timeout for headers - 2 minutes
|
||||
this.server.headersTimeout = 120000;
|
||||
// Keep-alive timeout - 65 seconds (slightly more than typical load balancer timeout)
|
||||
this.server.keepAliveTimeout = 65000;
|
||||
}
|
||||
|
||||
resolve(this.server);
|
||||
});
|
||||
|
||||
|
||||
+24
-10
@@ -17,7 +17,6 @@ import {
|
||||
readDirs,
|
||||
rmPath,
|
||||
setSystemTimezone,
|
||||
updateLinuxMirrorFile,
|
||||
} from '../config/util';
|
||||
import {
|
||||
DependenceModel,
|
||||
@@ -215,11 +214,33 @@ export default class SystemService {
|
||||
onEnd?: () => void,
|
||||
) {
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
|
||||
let targetDomain = 'https://dl-cdn.alpinelinux.org';
|
||||
if (os.platform() !== 'linux') {
|
||||
return;
|
||||
}
|
||||
const command = await updateLinuxMirrorFile(info.linuxMirror || '');
|
||||
let hasError = false;
|
||||
const content = await fs.promises.readFile('/etc/apk/repositories', {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
|
||||
if (domainMatch) {
|
||||
defaultDomain = domainMatch[1];
|
||||
}
|
||||
if (info.linuxMirror) {
|
||||
targetDomain = info.linuxMirror;
|
||||
}
|
||||
const command = `sed -i 's/${defaultDomain.replace(
|
||||
/\//g,
|
||||
'\\/',
|
||||
)}/${targetDomain.replace(
|
||||
/\//g,
|
||||
'\\/',
|
||||
)}/g' /etc/apk/repositories && apk update -f`;
|
||||
|
||||
this.scheduleService.runTask(
|
||||
command,
|
||||
{
|
||||
@@ -233,15 +254,8 @@ export default class SystemService {
|
||||
message: 'update linux mirror end',
|
||||
});
|
||||
onEnd?.();
|
||||
if (!hasError) {
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
hasError = true;
|
||||
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
|
||||
},
|
||||
onLog: async (message: string) => {
|
||||
|
||||
+54
-16
@@ -1,8 +1,9 @@
|
||||
import { lock } from 'proper-lockfile';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { writeFile, open, chmod } from 'fs/promises';
|
||||
import { writeFile, open, chmod, FileHandle } from 'fs/promises';
|
||||
import { fileExist } from '../config/util';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
function getUniqueLockPath(filePath: string) {
|
||||
const sanitizedPath = filePath
|
||||
@@ -19,24 +20,61 @@ export async function writeFileWithLock(
|
||||
if (typeof options === 'string') {
|
||||
options = { encoding: options };
|
||||
}
|
||||
|
||||
// Ensure file exists before locking
|
||||
if (!(await fileExist(filePath))) {
|
||||
const fileHandle = await open(filePath, 'w');
|
||||
fileHandle.close();
|
||||
let fileHandle: FileHandle | undefined;
|
||||
try {
|
||||
fileHandle = await open(filePath, 'w');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to create file ${filePath}: ${errorMessage}`);
|
||||
} finally {
|
||||
if (fileHandle !== undefined) {
|
||||
try {
|
||||
await fileHandle.close();
|
||||
} catch (closeError) {
|
||||
// Log close error but don't throw to avoid masking the original error
|
||||
Logger.error(`Failed to close file handle for ${filePath}:`, closeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lockfilePath = getUniqueLockPath(filePath);
|
||||
let release: (() => Promise<void>) | undefined;
|
||||
|
||||
const release = await lock(filePath, {
|
||||
retries: {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 3000,
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
try {
|
||||
release = await lock(filePath, {
|
||||
retries: {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 3000,
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to acquire lock for ${filePath}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to write to file ${filePath}: ${errorMessage}`);
|
||||
} finally {
|
||||
if (release) {
|
||||
try {
|
||||
await release();
|
||||
} catch (error) {
|
||||
// Log but don't throw on release failure
|
||||
Logger.error(`Failed to release lock for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
await release();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user